initial swift data conversion

This commit is contained in:
Garth Vander Houwen 2026-04-16 12:10:00 -07:00
parent 183924d4dc
commit b2c72ae166
130 changed files with 2939 additions and 2269 deletions

View file

@ -6,20 +6,21 @@
//
import SwiftUI
import SwiftData
import Charts
import MeshtasticProtobufs
import OSLog
struct DetectionSensorLog: View {
@Environment(\.managedObjectContext) var context
@Environment(\.modelContext) private var context
@EnvironmentObject var accessoryManager: AccessoryManager
@State private var isPresentingClearLogConfirm: Bool = false
@State var isExporting = false
@State var exportString = ""
@ObservedObject var node: NodeInfoEntity
@FetchRequest(sortDescriptors: [NSSortDescriptor(key: "messageTimestamp", ascending: false)],
predicate: NSPredicate(format: "portNum == %d", Int32(PortNum.detectionSensorApp.rawValue)), animation: .none)
private var detections: FetchedResults<MessageEntity>
@Bindable var node: NodeInfoEntity
@Query(filter: #Predicate<MessageEntity> { $0.portNum == 10 },
sort: \MessageEntity.messageTimestamp, order: .reverse)
private var detections: [MessageEntity]
var body: some View {
let oneDayAgo = Calendar.current.date(byAdding: .day, value: -1, to: Date())
@ -142,15 +143,17 @@ struct DetectionSensorLog: View {
}
}
// TODO: Fix preview for SwiftData
/*
#Preview {
let context = PersistenceController.preview.container.viewContext
let node = NodeInfoEntity(context: context)
let node = NodeInfoEntity()
node.num = 123456789
let user = UserEntity(context: context)
let user = UserEntity()
user.longName = "Test Node"
user.shortName = "TN"
node.user = user
return DetectionSensorLog(node: node)
DetectionSensorLog(node: node)
.environmentObject(AccessoryManager.shared)
.environment(\.managedObjectContext, context)
.modelContainer(PersistenceController.preview.container)
}
*/

View file

@ -10,7 +10,7 @@ import OSLog
struct DeviceMetricsLog: View {
@Environment(\.managedObjectContext) var context
@Environment(\.modelContext) private var context
@EnvironmentObject var accessoryManager: AccessoryManager
private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom }
@ -21,7 +21,7 @@ struct DeviceMetricsLog: View {
@State private var batteryChartColor: Color = .blue
@State private var airtimeChartColor: Color = .yellow
@State private var channelUtilizationChartColor: Color = .green
@ObservedObject var node: NodeInfoEntity
@Bindable var node: NodeInfoEntity
@State private var sortOrder = [KeyPathComparator(\TelemetryEntity.time, order: .reverse)]
@State private var selection: TelemetryEntity.ID?
@State private var chartSelection: Date?
@ -30,7 +30,7 @@ struct DeviceMetricsLog: View {
VStack {
if node.hasDeviceMetrics {
let oneWeekAgo = Calendar.current.date(byAdding: .day, value: -7, to: Date())
let deviceMetrics = node.telemetries?.filtered(using: NSPredicate(format: "metricsType == 0")).reversed() as? [TelemetryEntity] ?? []
let deviceMetrics = node.telemetries.filter { $0.metricsType == 0 }.reversed()
let chartData = deviceMetrics
.filter { $0.time != nil && $0.time! >= oneWeekAgo! }
.sorted { $0.time! < $1.time! }
@ -255,15 +255,17 @@ struct DeviceMetricsLog: View {
}
}
// TODO: Fix preview for SwiftData
/*
#Preview {
let context = PersistenceController.preview.container.viewContext
let node = NodeInfoEntity(context: context)
let node = NodeInfoEntity()
node.num = 123456789
let user = UserEntity(context: context)
let user = UserEntity()
user.longName = "Test Node"
user.shortName = "TN"
node.user = user
return DeviceMetricsLog(node: node)
DeviceMetricsLog(node: node)
.environmentObject(AccessoryManager.shared)
.environment(\.managedObjectContext, context)
.modelContainer(PersistenceController.preview.container)
}
*/

View file

@ -7,33 +7,27 @@
import SwiftUI
import Charts
import OSLog
import CoreData
import SwiftData
struct EnvironmentMetricsLog: View {
@Environment(\.managedObjectContext) var context
@Environment(\.modelContext) private var context
@EnvironmentObject var accessoryManager: AccessoryManager
@State private var isPresentingClearLogConfirm: Bool = false
@State var isExporting = false
@State var exportString = ""
@ObservedObject var node: NodeInfoEntity
@Bindable var node: NodeInfoEntity
@StateObject var columnList = MetricsColumnList.environmentDefaultColumns
@StateObject var seriesList = MetricsSeriesList.environmentDefaultChartSeries
@State var isEditingColumnConfiguration = false
@FetchRequest private var chartData: FetchedResults<TelemetryEntity>
init(node: NodeInfoEntity) {
self.node = node
// Build fetch request:
let request: NSFetchRequest<TelemetryEntity> = TelemetryEntity.fetchRequest()
private var chartData: [TelemetryEntity] {
let oneWeekAgo = Calendar.current.date(byAdding: .day, value: -7, to: Date()) ?? Date.distantPast
request.predicate = NSPredicate(format: "nodeTelemetry == %@ AND metricsType == 1 AND time >= %@", node, oneWeekAgo as NSDate)
request.sortDescriptors = [NSSortDescriptor(key: "time", ascending: false)]
_chartData = FetchRequest(fetchRequest: request)
return (node.telemetries ?? [])
.filter { $0.metricsType == 1 && ($0.time ?? Date.distantPast) >= oneWeekAgo }
.sorted { ($0.time ?? .distantPast) > ($1.time ?? .distantPast) }
}
var body: some View {
@ -187,15 +181,17 @@ struct EnvironmentMetricsLog: View {
}
}
// TODO: Fix preview for SwiftData
/*
#Preview {
let context = PersistenceController.preview.container.viewContext
let node = NodeInfoEntity(context: context)
let node = NodeInfoEntity()
node.num = 123456789
let user = UserEntity(context: context)
let user = UserEntity()
user.longName = "Test Node"
user.shortName = "TN"
node.user = user
return EnvironmentMetricsLog(node: node)
EnvironmentMetricsLog(node: node)
.environmentObject(AccessoryManager.shared)
.environment(\.managedObjectContext, context)
.modelContainer(PersistenceController.preview.container)
}
*/

View file

@ -41,12 +41,14 @@ struct ClientHistoryButton: View {
}
}
// TODO: Fix preview for SwiftData
/*
#Preview {
let context = PersistenceController.preview.container.viewContext
let node = NodeInfoEntity(context: context)
let node = NodeInfoEntity()
node.num = 123456789
let connectedNode = NodeInfoEntity(context: context)
let connectedNode = NodeInfoEntity()
connectedNode.num = 987654321
return ClientHistoryButton(connectedNode: connectedNode, node: node)
ClientHistoryButton(connectedNode: connectedNode, node: node)
.environmentObject(AccessoryManager.shared)
}
*/

View file

@ -1,10 +1,10 @@
import CoreData
import SwiftData
import OSLog
import SwiftUI
struct DeleteNodeButton: View {
@Environment(\.managedObjectContext) var context
@Environment(\.modelContext) private var context
@EnvironmentObject var accessoryManager: AccessoryManager
var connectedNode: NodeInfoEntity
@ -65,13 +65,15 @@ struct DeleteNodeButton: View {
}
}
// TODO: Fix preview for SwiftData
/*
#Preview {
let context = PersistenceController.preview.container.viewContext
let connectedNode = NodeInfoEntity(context: context)
let connectedNode = NodeInfoEntity()
connectedNode.num = 987654321
let node = NodeInfoEntity(context: context)
let node = NodeInfoEntity()
node.num = 123456789
return DeleteNodeButton(connectedNode: connectedNode, node: node)
DeleteNodeButton(connectedNode: connectedNode, node: node)
.environmentObject(AccessoryManager.shared)
.environment(\.managedObjectContext, context)
.modelContainer(PersistenceController.preview.container)
}
*/

View file

@ -1,4 +1,4 @@
import CoreData
import SwiftData
import SwiftUI
struct ExchangePositionsButton: View {
@ -62,12 +62,14 @@ struct ExchangePositionsButton: View {
}
}
// TODO: Fix preview for SwiftData
/*
#Preview {
let context = PersistenceController.preview.container.viewContext
let node = NodeInfoEntity(context: context)
let node = NodeInfoEntity()
node.num = 123456789
let connectedNode = NodeInfoEntity(context: context)
let connectedNode = NodeInfoEntity()
connectedNode.num = 987654321
return ExchangePositionsButton(node: node, connectedNode: connectedNode)
ExchangePositionsButton(node: node, connectedNode: connectedNode)
.environmentObject(AccessoryManager.shared)
}
*/

View file

@ -1,4 +1,4 @@
import CoreData
import SwiftData
import SwiftUI
import OSLog
@ -60,12 +60,14 @@ struct ExchangeUserInfoButton: View {
}
}
// TODO: Fix preview for SwiftData
/*
#Preview {
let context = PersistenceController.preview.container.viewContext
let node = NodeInfoEntity(context: context)
let node = NodeInfoEntity()
node.num = 123456789
let connectedNode = NodeInfoEntity(context: context)
let connectedNode = NodeInfoEntity()
connectedNode.num = 987654321
return ExchangeUserInfoButton(node: node, connectedNode: connectedNode)
ExchangeUserInfoButton(node: node, connectedNode: connectedNode)
.environmentObject(AccessoryManager.shared)
}
*/

View file

@ -1,13 +1,13 @@
import CoreData
import SwiftData
import OSLog
import SwiftUI
struct FavoriteNodeButton: View {
@EnvironmentObject var accessoryManager: AccessoryManager
@Environment(\.managedObjectContext) var context
@Environment(\.modelContext) private var context
@ObservedObject var node: NodeInfoEntity
@Bindable var node: NodeInfoEntity
@State var isShowingClientBaseConfirmation = false
var body: some View {
@ -80,15 +80,17 @@ struct FavoriteNodeButton: View {
}
}
// TODO: Fix preview for SwiftData
/*
#Preview {
let context = PersistenceController.preview.container.viewContext
let node = NodeInfoEntity(context: context)
let node = NodeInfoEntity()
node.num = 123456789
let user = UserEntity(context: context)
let user = UserEntity()
user.longName = "Test Node"
user.shortName = "TN"
node.user = user
return FavoriteNodeButton(node: node)
FavoriteNodeButton(node: node)
.environmentObject(AccessoryManager.shared)
.environment(\.managedObjectContext, context)
.modelContainer(PersistenceController.preview.container)
}
*/

View file

@ -1,12 +1,12 @@
import CoreData
import SwiftData
import OSLog
import SwiftUI
struct IgnoreNodeButton: View {
@Environment(\.managedObjectContext) var context
@Environment(\.modelContext) private var context
@EnvironmentObject var accessoryManager: AccessoryManager
@ObservedObject
@Bindable
var node: NodeInfoEntity
var body: some View {
@ -52,11 +52,13 @@ struct IgnoreNodeButton: View {
}
}
// TODO: Fix preview for SwiftData
/*
#Preview {
let context = PersistenceController.preview.container.viewContext
let node = NodeInfoEntity(context: context)
let node = NodeInfoEntity()
node.num = 123456789
return IgnoreNodeButton(node: node)
IgnoreNodeButton(node: node)
.environmentObject(AccessoryManager.shared)
.environment(\.managedObjectContext, context)
.modelContainer(PersistenceController.preview.container)
}
*/

View file

@ -7,7 +7,7 @@
import SwiftUI
import CoreLocation
import CoreData
import SwiftData
import OSLog
struct NavigateToButton: View {
@ -21,11 +21,13 @@ struct NavigateToButton: View {
}
Logger.services.info("Fetching NodeInfoEntity for userNum: \(userNum, privacy: .public)")
let fetchRequest: NSFetchRequest<NodeInfoEntity> = NSFetchRequest(entityName: "NodeInfoEntity")
fetchRequest.predicate = NSPredicate(format: "num == %lld", Int64(userNum))
var descriptor = FetchDescriptor<NodeInfoEntity>(
predicate: #Predicate<NodeInfoEntity> { $0.num == userNum }
)
descriptor.fetchLimit = 1
do {
let fetchedNodes = try PersistenceController.shared.container.viewContext.fetch(fetchRequest)
let fetchedNodes = try PersistenceController.shared.context.fetch(descriptor)
guard let nodeInfo = fetchedNodes.first else {
Logger.services.error("NavigateToAction: Node with userNum \(userNum, privacy: .public) not found in Core Data")
return
@ -55,14 +57,16 @@ struct NavigateToButton: View {
}
}
// TODO: Fix preview for SwiftData
/*
#Preview {
let context = PersistenceController.preview.container.viewContext
let node = NodeInfoEntity(context: context)
let node = NodeInfoEntity()
node.num = 123456789
let user = UserEntity(context: context)
let user = UserEntity()
user.longName = "Test Node"
user.shortName = "TN"
user.num = 123456789
node.user = user
return NavigateToButton(node: node)
NavigateToButton(node: node)
}
*/

View file

@ -1,20 +1,19 @@
import CoreData
import SwiftData
import OSLog
import SwiftUI
struct NodeAlertsButton: View {
var context: NSManagedObjectContext
var context: ModelContext
@ObservedObject
@Bindable
var node: NodeInfoEntity
@ObservedObject
@Bindable
var user: UserEntity
var body: some View {
Button {
user.mute = !user.mute
context.refresh(node, mergeChanges: true)
do {
try context.save()
} catch {
@ -32,13 +31,15 @@ struct NodeAlertsButton: View {
}
}
// TODO: Fix preview for SwiftData
/*
#Preview {
let context = PersistenceController.preview.container.viewContext
let node = NodeInfoEntity(context: context)
let node = NodeInfoEntity()
node.num = 123456789
let user = UserEntity(context: context)
let user = UserEntity()
user.longName = "Test Node"
user.shortName = "TN"
node.user = user
return NodeAlertsButton(context: context, node: node, user: user)
NodeAlertsButton(context: context, node: node, user: user)
}
*/

View file

@ -44,14 +44,16 @@ struct TraceRouteButton: View {
}
}
// TODO: Fix preview for SwiftData
/*
#Preview {
let context = PersistenceController.preview.container.viewContext
let node = NodeInfoEntity(context: context)
let node = NodeInfoEntity()
node.num = 123456789
let user = UserEntity(context: context)
let user = UserEntity()
user.longName = "Test Node"
user.shortName = "TN"
node.user = user
return TraceRouteButton(node: node)
TraceRouteButton(node: node)
.environmentObject(AccessoryManager.shared)
}
*/

View file

@ -6,6 +6,7 @@
//
import SwiftUI
import SwiftData
import MapKit
import CoreLocation
import OSLog
@ -40,15 +41,16 @@ struct MeshMapContent: MapContent {
@AppStorage("mapOverlaysEnabled") private var showMapOverlays = false
@Binding var enabledOverlayConfigs: Set<UUID>
@FetchRequest(fetchRequest: PositionEntity.allPositionsFetchRequest(), animation: .easeIn)
var positions: FetchedResults<PositionEntity>
@FetchRequest(fetchRequest: WaypointEntity.allWaypointssFetchRequest(), animation: .none)
var waypoints: FetchedResults<WaypointEntity>
@FetchRequest(sortDescriptors: [NSSortDescriptor(key: "name", ascending: true)],
predicate: NSPredicate(format: "enabled == true", ""), animation: .none)
private var routes: FetchedResults<RouteEntity>
@Query(filter: #Predicate<PositionEntity> { $0.nodePosition != nil && $0.latest == true },
sort: \PositionEntity.time, order: .reverse)
var positions: [PositionEntity]
@Query(sort: \WaypointEntity.name, order: .reverse)
var waypoints: [WaypointEntity]
@Query(filter: #Predicate<RouteEntity> { $0.enabled == true },
sort: \RouteEntity.name)
private var routes: [RouteEntity]
@MapContentBuilder
var positionAnnotations: some MapContent {
@ -57,10 +59,10 @@ struct MeshMapContent: MapContent {
if (!showFavorites || (position.nodePosition?.favorite == true)) && !(position.nodePosition?.ignored == true) {
let coordinateForNodePin: CLLocationCoordinate2D = if position.isPreciseLocation {
// Precise location: place node pin at actual location.
position.coordinate
position.nodeCoordinate ?? LocationsHandler.DefaultLocation
} else {
// Imprecise location: fuzz slightly so overlapping nodes are visible and clickable at highest zoom levels.
position.fuzzedCoordinate
position.fuzzedNodeCoordinate ?? LocationsHandler.DefaultLocation
}
if 12...15 ~= position.precisionBits || position.precisionBits == 32 {
@ -131,7 +133,8 @@ struct MeshMapContent: MapContent {
@MapContentBuilder
var routeAnnotations: some MapContent {
ForEach(routes) { route in
if let routeLocations = route.locations, let locations = Array(routeLocations) as? [LocationEntity] {
if !route.locations.isEmpty {
let locations = route.locations
let routeCoords = locations.compactMap {(loc) -> CLLocationCoordinate2D in
return loc.locationCoordinate ?? LocationsHandler.DefaultLocation
}
@ -167,7 +170,7 @@ struct MeshMapContent: MapContent {
var waypointAnnotations: some MapContent {
if waypoints.count > 0, showWaypoints, let waypoints = Array(waypoints) as? [WaypointEntity] {
ForEach(waypoints, id: \.self) { waypoint in
Annotation(waypoint.name ?? "?", coordinate: waypoint.coordinate) {
Annotation(waypoint.name ?? "?", coordinate: waypoint.mapCoordinate) {
LazyVStack {
ZStack {
CircleText(text: String(UnicodeScalar(Int(waypoint.icon)) ?? "📍"), color: Color.orange, circleSize: 40)

View file

@ -6,11 +6,11 @@
//
import SwiftUI
import MapKit
import CoreData
import SwiftData
struct NodeMapContent: MapContent {
@ObservedObject var node: NodeInfoEntity
@Bindable var node: NodeInfoEntity
/// Map State User Defaults
@AppStorage("meshMapShowNodeHistory") private var showNodeHistory = false
@AppStorage("meshMapShowRouteLines") private var showRouteLines = false
@ -22,7 +22,7 @@ struct NodeMapContent: MapContent {
@MapContentBuilder
var nodeMap: some MapContent {
let positionArray = node.positions?.array as? [PositionEntity] ?? []
let positionArray = node.positions
/// Node Color from node.num
let nodeColor = UIColor(hex: UInt32(node.num))
@ -43,7 +43,7 @@ struct NodeMapContent: MapContent {
let pp = PositionPrecision(rawValue: Int(position.precisionBits))
let radius: CLLocationDistance = pp?.precisionMeters ?? 0
if radius > 0.0 {
MapCircle(center: position.coordinate, radius: radius)
MapCircle(center: position.nodeCoordinate ?? LocationsHandler.DefaultLocation, radius: radius)
.foregroundStyle(Color(nodeColor).opacity(0.25))
.stroke(.white, lineWidth: 2)
}
@ -51,7 +51,7 @@ struct NodeMapContent: MapContent {
/// Lastest Position Pin
if position.latest {
/// Node Annotations
Annotation(position.latest ? node.user?.shortName ?? "?": "", coordinate: position.coordinate) {
Annotation(position.latest ? node.user?.shortName ?? "?": "", coordinate: position.nodeCoordinate ?? LocationsHandler.DefaultLocation) {
LazyVStack {
ZStack {
if pf.contains(.Heading) {
@ -100,7 +100,7 @@ struct NodeMapContent: MapContent {
// Having showNodeHistory enabled can be quite slow if there are thousands of history points.
if position.latest == false && node.favorite {
let headingDegrees = Angle.degrees(Double(position.heading))
Annotation("", coordinate: position.coordinate) {
Annotation("", coordinate: position.nodeCoordinate ?? LocationsHandler.DefaultLocation) {
if pf.contains(.Heading) {
Image(uiImage: prerenderedHistoryPointArrowImage)
.renderingMode(.original)
@ -154,7 +154,7 @@ struct NodeMapContent: MapContent {
@MapContentBuilder
var body: some MapContent {
if node.positions?.count ?? 0 > 0 {
if node.positions.count > 0 {
nodeMap
}
}

View file

@ -3,7 +3,7 @@ import UniformTypeIdentifiers
import OSLog
struct MapDataFiles: View {
@Environment(\.managedObjectContext) var context
@Environment(\.modelContext) private var context
@EnvironmentObject var accessoryManager: AccessoryManager
@ObservedObject private var mapDataManager = MapDataManager.shared

View file

@ -6,6 +6,7 @@
//
import SwiftUI
import SwiftData
import CoreLocation
import MapKit
@ -30,10 +31,10 @@ private struct NodeMapContentEquatableWrapper<Content: View>: View, Equatable {
}
struct NodeMapSwiftUI: View {
@Environment(\.managedObjectContext) var context
@Environment(\.modelContext) private var context
@EnvironmentObject var accessoryManager: AccessoryManager
/// Parameters
@ObservedObject var node: NodeInfoEntity
@Bindable var node: NodeInfoEntity
@State var showUserLocation: Bool = false
@State var positions: [PositionEntity] = []
/// Map State User Defaults
@ -58,11 +59,8 @@ struct NodeMapSwiftUI: View {
@State private var mapRegion = MKCoordinateRegion.init()
@FetchRequest(sortDescriptors: [NSSortDescriptor(key: "name", ascending: false)],
predicate: NSPredicate(
format: "expire == nil || expire >= %@", Date() as NSDate
), animation: .none)
private var waypoints: FetchedResults<WaypointEntity>
@Query(sort: \WaypointEntity.name, order: .reverse)
private var waypoints: [WaypointEntity]
var body: some View {
if node.hasPositions {
@ -78,7 +76,7 @@ struct NodeMapSwiftUI: View {
configuredMap
}
}
.navigationBarTitle(String((node.user?.shortName ?? "Unknown".localized) + (" \(node.positions?.count ?? 0) points")), displayMode: .inline)
.navigationBarTitle(String((node.user?.shortName ?? "Unknown".localized) + (" \(node.positions.count) points")), displayMode: .inline)
.navigationBarItems(trailing:
ZStack {
ConnectedDevice(
@ -123,8 +121,8 @@ struct NodeMapSwiftUI: View {
}
private var mapContentSignature: NodeMapContentSignature {
let positionCount = node.positions?.count ?? 0
let lastPositionTime = (node.positions?.lastObject as? PositionEntity)?.time
let positionCount = node.positions.count
let lastPositionTime = node.positions.last?.time
return NodeMapContentSignature(nodeNum: node.num, positionCount: positionCount, lastPositionTime: lastPositionTime, showNodeHistory: showNodeHistory, showRouteLines: showRouteLines, showConvexHull: showConvexHull, favorite: node.favorite)
}
@ -208,7 +206,7 @@ struct NodeMapSwiftUI: View {
.glassButtonStyle()
}
if node.positions?.count ?? 0 > 1 {
if node.positions.count > 1 {
Button(action: {
if isLookingAround {
isLookingAround = false
@ -241,15 +239,15 @@ struct NodeMapSwiftUI: View {
private func handleNodeChange() {
isLookingAround = false
isShowingAltitude = false
let newMostRecent = node.positions?.lastObject as? PositionEntity
if node.positions?.count ?? 0 > 1 {
let newMostRecent = node.positions.last
if node.positions.count > 1 {
position = .automatic
} else if let mrCoord = newMostRecent?.coordinate {
} else if let mrCoord = newMostRecent?.nodeCoordinate {
position = .camera(MapCamera(centerCoordinate: mrCoord, distance: distance, heading: 0, pitch: 0))
}
if let newMostRecent {
if let newMostRecent, let coord = newMostRecent.nodeCoordinate {
Task {
scene = try? await fetchScene(for: newMostRecent.coordinate)
scene = try? await fetchScene(for: coord)
}
}
}
@ -257,13 +255,13 @@ struct NodeMapSwiftUI: View {
private func handleAppear() {
UIApplication.shared.isIdleTimerDisabled = true
updateMapStyle(for: selectedMapLayer)
let mostRecent = node.positions?.lastObject as? PositionEntity
if node.positions?.count ?? 0 > 1 {
let mostRecent = node.positions.last
if node.positions.count > 1 {
position = .automatic
} else if let mrCoord = mostRecent?.coordinate {
} else if let mrCoord = mostRecent?.nodeCoordinate {
position = .camera(MapCamera(centerCoordinate: mrCoord, distance: distance, heading: 0, pitch: 0))
}
if scene == nil, let mrCoord = mostRecent?.coordinate {
if scene == nil, let mrCoord = mostRecent?.nodeCoordinate {
Task {
scene = try? await fetchScene(for: mrCoord)
}

View file

@ -16,16 +16,12 @@ struct PositionAltitude {
struct PositionAltitudeChart: View {
@Environment(\.dismiss) private var dismiss
@ObservedObject var node: NodeInfoEntity
@Bindable var node: NodeInfoEntity
@State private var lineWidth = 2.0
var data: [PositionAltitude] {
let fiveYearsAgo = Calendar.current.date(byAdding: .year, value: -5, to: Date())
guard let nodePositions = node.positions,
let positions = Array(nodePositions) as? [PositionEntity]
else {
return []
}
let positions = node.positions
let filteredPositions = positions.filter({$0.time != nil && ($0.time ?? fiveYearsAgo!) > fiveYearsAgo!})
return filteredPositions.map {

View file

@ -11,7 +11,7 @@ import MapKit
struct PositionPopover: View {
@ObservedObject var locationsHandler = LocationsHandler.shared
@Environment(\.managedObjectContext) var context
@Environment(\.modelContext) private var context
@EnvironmentObject var appState: AppState
private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom }
@Environment(\.dismiss) private var dismiss
@ -81,7 +81,7 @@ struct PositionPopover: View {
.padding(.bottom, 5)
/// Coordinate
Label {
Text("\(String(format: "%.6f", position.coordinate.latitude)), \(String(format: "%.6f", position.coordinate.longitude))")
Text("\(String(format: "%.6f", position.nodeCoordinate?.latitude ?? 0)), \(String(format: "%.6f", position.nodeCoordinate?.longitude ?? 0))")
.textSelection(.enabled)
.foregroundColor(.primary)
.font(idiom == .phone ? .callout : .body)
@ -169,7 +169,8 @@ struct PositionPopover: View {
if let lastLocation = locationsHandler.locationsArray.last {
/// Distance
if lastLocation.distance(from: CLLocation(latitude: LocationsHandler.DefaultLocation.latitude, longitude: LocationsHandler.DefaultLocation.longitude)) > 0.0 {
let metersAway = position.coordinate.distance(from: CLLocationCoordinate2D(latitude: lastLocation.coordinate.latitude, longitude: lastLocation.coordinate.longitude))
let posCoord = position.nodeCoordinate ?? LocationsHandler.DefaultLocation
let metersAway = posCoord.distance(from: CLLocationCoordinate2D(latitude: lastLocation.coordinate.latitude, longitude: lastLocation.coordinate.longitude))
Label {
Text("Distance".localized + ": \(distanceFormatter.string(fromDistance: Double(metersAway)))")
.foregroundColor(.primary)
@ -263,7 +264,7 @@ struct PositionPopover: View {
.presentationBackgroundInteraction(.enabled(upThrough: .large))
.navigationDestination(isPresented: $navigateToCompass) {
CompassView(
waypointLocation: position.coordinate,
waypointLocation: position.nodeCoordinate ?? LocationsHandler.DefaultLocation,
waypointLongName: position.nodePosition?.user?.longName ?? "Unknown node",
waypointShortName: position.nodePosition?.user?.shortName ?? "???",
color: (position.nodePosition?.user?.num != nil && position.nodePosition?.user?.num != 0) ? Color(UIColor(hex: UInt32(position.nodePosition!.user!.num))) : .orange

View file

@ -10,12 +10,12 @@ import MapKit
import MeshtasticProtobufs
import OSLog
import SwiftUI
import CoreData
import SwiftData
struct WaypointForm: View {
@EnvironmentObject var accessoryManager: AccessoryManager
@Environment(\.managedObjectContext) var context
@Environment(\.modelContext) private var context
@Environment(\.dismiss) private var dismiss
@State var waypoint: WaypointEntity
let distanceFormatter = MKDistanceFormatter()
@ -43,20 +43,20 @@ struct WaypointForm: View {
Divider()
Form {
if let cl = LocationsHandler.currentLocation {
let distance = CLLocation(latitude: cl.latitude, longitude: cl.longitude).distance(from: CLLocation(latitude: waypoint.coordinate.latitude, longitude: waypoint.coordinate.longitude ))
let distance = CLLocation(latitude: cl.latitude, longitude: cl.longitude).distance(from: CLLocation(latitude: waypoint.mapCoordinate.latitude, longitude: waypoint.mapCoordinate.longitude ))
Section(header: Text("Coordinate") ) {
HStack {
Text("Location:")
.foregroundColor(.secondary)
Text("\(String(format: "%.5f", waypoint.coordinate.latitude) + "," + String(format: "%.5f", waypoint.coordinate.longitude))")
Text("\(String(format: "%.5f", waypoint.mapCoordinate.latitude) + "," + String(format: "%.5f", waypoint.mapCoordinate.longitude))")
.textSelection(.enabled)
.foregroundColor(.secondary)
.font(.caption)
}
Button {
waypoint.coordinate.longitude = cl.longitude
waypoint.coordinate.latitude = cl.latitude
waypoint.longitudeI = Int32(cl.longitude * 1e7)
waypoint.latitudeI = Int32(cl.latitude * 1e7)
} label: {
HStack {
Text("Use my Location")
@ -65,7 +65,7 @@ struct WaypointForm: View {
}
.accessibilityLabel("Set to current location")
HStack {
if waypoint.coordinate.latitude != 0 && waypoint.coordinate.longitude != 0 {
if waypoint.mapCoordinate.latitude != 0 && waypoint.mapCoordinate.longitude != 0 {
DistanceText(meters: distance)
.foregroundColor(Color.gray)
}
@ -288,7 +288,7 @@ struct WaypointForm: View {
Text(waypoint.name ?? "?")
.font(.largeTitle)
Spacer()
if waypoint.locked > 0 && waypoint.locked != UInt32(accessoryManager.activeDeviceNum ?? 0) {
if waypoint.locked {
Image(systemName: "lock.fill")
.font(.largeTitle)
} else {
@ -359,7 +359,7 @@ struct WaypointForm: View {
Label {
Text("Coordinates:")
.foregroundColor(.primary)
Text("\(String(format: "%.6f", waypoint.coordinate.latitude)), \(String(format: "%.6f", waypoint.coordinate.longitude))")
Text("\(String(format: "%.6f", waypoint.mapCoordinate.latitude)), \(String(format: "%.6f", waypoint.mapCoordinate.longitude))")
.textSelection(.enabled)
.foregroundColor(.secondary)
.font(.caption2)
@ -369,7 +369,7 @@ struct WaypointForm: View {
.padding(.bottom)
// Drop Maps Pin
Button(action: {
if let url = URL(string: "http://maps.apple.com/?ll=\(waypoint.coordinate.latitude),\(waypoint.coordinate.longitude)&q=\(waypoint.name ?? "Dropped Pin")") {
if let url = URL(string: "http://maps.apple.com/?ll=\(waypoint.mapCoordinate.latitude),\(waypoint.mapCoordinate.longitude)&q=\(waypoint.name ?? "Dropped Pin")") {
UIApplication.shared.open(url)
}
}) {
@ -410,7 +410,7 @@ struct WaypointForm: View {
/// Distance
if let cl = LocationsHandler.currentLocation {
if cl.distance(from: cl) > 0.0 {
let metersAway = waypoint.coordinate.distance(from: cl)
let metersAway = waypoint.mapCoordinate.distance(from: cl)
Label {
Text("Distance".localized + ": \(distanceFormatter.string(fromDistance: Double(metersAway)))")
.foregroundColor(.primary)
@ -479,9 +479,8 @@ struct WaypointForm: View {
} else {
expires = false
}
if waypoint.locked > 0 {
if waypoint.locked {
locked = true
lockedTo = waypoint.locked
}
} else {
name = ""
@ -490,8 +489,8 @@ struct WaypointForm: View {
expires = false
expire = Date.now.addingTimeInterval(60 * 480)
icon = "📍"
latitude = waypoint.coordinate.latitude
longitude = waypoint.coordinate.longitude
latitude = waypoint.mapCoordinate.latitude
longitude = waypoint.mapCoordinate.longitude
}
}
.presentationBackgroundInteraction(.enabled(upThrough: .fraction(0.85)))
@ -501,12 +500,14 @@ struct WaypointForm: View {
private func fetchNodeInfo() async {
// --- Fetch createdBy node ---
if waypoint.createdBy != 0 {
let createdByFetch: NSFetchRequest<NodeInfoEntity> = NodeInfoEntity.fetchRequest()
createdByFetch.predicate = NSPredicate(format: "num == %lld", Int64(waypoint.createdBy))
createdByFetch.fetchLimit = 1
let createdByNum = Int64(waypoint.createdBy)
var createdByDescriptor = FetchDescriptor<NodeInfoEntity>(
predicate: #Predicate<NodeInfoEntity> { $0.num == createdByNum }
)
createdByDescriptor.fetchLimit = 1
do {
let nodes = try context.fetch(createdByFetch)
let nodes = try context.fetch(createdByDescriptor)
createdByNode = nodes.first
} catch {
Logger.services.warning("Error fetching createdBy node: \(error.localizedDescription)")
@ -516,12 +517,14 @@ struct WaypointForm: View {
// --- Fetch lastUpdatedBy node (only if different from createdBy) ---
if waypoint.lastUpdatedBy != 0,
waypoint.lastUpdatedBy != waypoint.createdBy {
let updatedByFetch: NSFetchRequest<NodeInfoEntity> = NodeInfoEntity.fetchRequest()
updatedByFetch.predicate = NSPredicate(format: "num == %lld", Int64(waypoint.lastUpdatedBy))
updatedByFetch.fetchLimit = 1
let updatedByNum = Int64(waypoint.lastUpdatedBy)
var updatedByDescriptor = FetchDescriptor<NodeInfoEntity>(
predicate: #Predicate<NodeInfoEntity> { $0.num == updatedByNum }
)
updatedByDescriptor.fetchLimit = 1
do {
let nodes = try context.fetch(updatedByFetch)
let nodes = try context.fetch(updatedByDescriptor)
lastUpdatedByNode = nodes.first
} catch {
Logger.services.warning("Error fetching lastUpdatedBy node: \(error.localizedDescription)")

View file

@ -19,13 +19,13 @@ struct NodeDetail: View {
var modemPreset: ModemPresets = ModemPresets(
rawValue: UserDefaults.modemPreset
) ?? ModemPresets.longFast
@Environment(\.managedObjectContext) var context
@Environment(\.modelContext) private var context
@EnvironmentObject var accessoryManager: AccessoryManager
@State private var showingShutdownConfirm: Bool = false
@State private var showingRebootConfirm: Bool = false
@State private var dateFormatRelative: Bool = true
var connectedNode: NodeInfoEntity?
@ObservedObject var node: NodeInfoEntity
@Bindable var node: NodeInfoEntity
@State private var environmentSectionHeight: CGFloat = 0
@State var showingCompassSheet = false
@ -69,7 +69,7 @@ struct NodeDetail: View {
}
.accessibilityElement(children: .combine)
}
if node.telemetries?.count ?? 0 > 0 {
if node.telemetries.count > 0 {
Spacer()
BatteryGauge(node: node)
}
@ -134,9 +134,7 @@ struct NodeDetail: View {
}
Spacer()
Button(action: {
context.perform {
UIPasteboard.general.string = publicKey
}
UIPasteboard.general.string = publicKey
}) {
HStack {
Image(systemName: "key.horizontal.fill")
@ -185,7 +183,7 @@ struct NodeDetail: View {
}
.accessibilityElement(children: .combine)
}
if let dm = node.telemetries?.filtered(using: NSPredicate(format: "metricsType == 0")).lastObject as? TelemetryEntity, let uptimeSeconds = dm.uptimeSeconds {
if let dm = node.telemetries.filter({ $0.metricsType == 0 }).last, let uptimeSeconds = dm.uptimeSeconds {
HStack {
Label {
Text("\("Uptime".localized)")
@ -401,7 +399,7 @@ struct NodeDetail: View {
.symbolRenderingMode(.multicolor)
}
}
.disabled(node.traceRoutes?.count ?? 0 == 0)
.disabled(node.traceRoutes.count == 0)
NavigationLink {
PowerMetricsLog(node: node)
} label: {

View file

@ -11,7 +11,7 @@ import MapKit
struct NodeInfoItem: View {
@ObservedObject var node: NodeInfoEntity
@Bindable var node: NodeInfoEntity
var body: some View {
@ -48,10 +48,10 @@ struct NodeInfoItem: View {
.font(.caption2)
}
}
let deviceMetrics = node.telemetries?.filtered(using: NSPredicate(format: "metricsType == 0"))
if deviceMetrics?.count ?? 0 >= 1 {
let deviceMetrics = node.telemetries.filter { $0.metricsType == 0 }
if deviceMetrics.count >= 1 {
Divider()
let mostRecent = deviceMetrics?.lastObject as? TelemetryEntity
let mostRecent = deviceMetrics.last
VStack(alignment: .center) {
BatteryGauge(batteryLevel: Double(mostRecent?.batteryLevel ?? 0))
if mostRecent?.voltage ?? 0 > 0 {

View file

@ -11,7 +11,7 @@ import MapKit
struct NodeInfoItem: View {
@ObservedObject var node: NodeInfoEntity
@Bindable var node: NodeInfoEntity
@State private var currentDevice: DeviceHardware?
var body: some View {

View file

@ -88,7 +88,7 @@ struct NodeListItem: View {
return desc
}
@ObservedObject var node: NodeInfoEntity
@Bindable var node: NodeInfoEntity
var isDirectlyConnected: Bool
var connectedNode: Int64
var modemPreset: ModemPresets = ModemPresets(rawValue: UserDefaults.modemPreset) ?? ModemPresets.longFast
@ -109,7 +109,7 @@ struct NodeListItem: View {
}
var locationData: (PositionEntity, CLLocation)? {
guard let lastPostion = node.positions?.lastObject as? PositionEntity else {
guard let lastPostion = node.positions.last else {
return nil
}
guard let currentLocation = LocationsHandler.shared.locationsArray.last else {
@ -172,7 +172,7 @@ struct NodeListItem: View {
text: "Store & Forward".localized)
}
if node.positions?.count ?? 0 > 0 && connectedNode != node.num {
if node.positions.count > 0 && connectedNode != node.num {
HStack {
if let (lastPostion, myCoord) = locationData {
let nodeCoord = CLLocation(latitude: lastPostion.nodeCoordinate!.latitude, longitude: lastPostion.nodeCoordinate!.longitude)
@ -298,9 +298,8 @@ struct IconAndText: View {
IconAndText(systemName: "antenna.radiowaves.left.and.right.circle.fill", text: "foo")
IconAndText(systemName: "antenna.radiowaves.left.and.right.circle", text: "bar")
NodeListItem(node: {
let context = PersistenceController.preview.container.viewContext
let nodeInfo = NodeInfoEntity(context: context)
let user = UserEntity(context: context)
let nodeInfo = NodeInfoEntity()
let user = UserEntity()
user.longName = "Test User"
user.shortName = "TU"
nodeInfo.user = user

View file

@ -8,7 +8,7 @@ import CoreImage.CIFilterBuiltins
#if canImport(UIKit)
import UIKit
#endif
import CoreData
import SwiftData
import MeshtasticProtobufs
import OSLog

View file

@ -6,7 +6,7 @@
//
import SwiftUI
import CoreData
import SwiftData
import CoreLocation
import Foundation
import OSLog
@ -14,7 +14,7 @@ import MapKit
struct MeshMap: View {
@Environment(\.managedObjectContext) var context
@Environment(\.modelContext) private var context
@EnvironmentObject var accessoryManager: AccessoryManager
@ObservedObject
@ -101,7 +101,7 @@ struct MeshMap: View {
centerMapAt(coordinate: coordinate)
newWaypointCoord = coordinate
editingWaypoint = WaypointEntity(context: context)
editingWaypoint = WaypointEntity()
editingWaypoint!.name = "Waypoint Pin"
editingWaypoint!.expire = Date.now.addingTimeInterval(60 * 480)
editingWaypoint!.latitudeI = Int32((newWaypointCoord?.latitude ?? 0) * 1e7)

View file

@ -7,14 +7,14 @@
import SwiftUI
import CoreLocation
import OSLog
import CoreData
import SwiftData
import Foundation
struct NodeList: View {
/// Debounce delay for node selection changes (100ms)
private static let nodeSelectionDebounceNs: UInt64 = 100_000_000
@Environment(\.managedObjectContext) var context
@Environment(\.modelContext) private var context
@EnvironmentObject var accessoryManager: AccessoryManager
@StateObject var router: Router
@State private var selectedNode: NodeInfoEntity?
@ -27,6 +27,7 @@ struct NodeList: View {
@State private var shareContactNode: NodeInfoEntity?
@StateObject var filters = NodeFilterParameters()
@State var isEditingFilters = false
@State private var filteredNodeCount: Int = 0
@SceneStorage("selectedDetailView") var selectedDetailView: String?
var connectedNode: NodeInfoEntity? {
@ -45,7 +46,8 @@ struct NodeList: View {
connectedNode: connectedNode,
isPresentingDeleteNodeAlert: $isPresentingDeleteNodeAlert,
deleteNodeId: $deleteNodeId,
shareContactNode: $shareContactNode
shareContactNode: $shareContactNode,
filteredNodeCount: $filteredNodeCount
)
.sheet(isPresented: $isEditingFilters) {
NodeListFilter(
@ -72,7 +74,7 @@ struct NodeList: View {
.searchable(text: $filters.searchText, placement: .navigationBarDrawer(displayMode: .always), prompt: "Find a node")
.autocorrectionDisabled(true)
.scrollDismissesKeyboard(.immediately)
.navigationTitle(String.localizedStringWithFormat("Nodes (%@)".localized, String(getNodeCount())))
.navigationTitle(String.localizedStringWithFormat("Nodes (%@)".localized, String(filteredNodeCount)))
.listStyle(.plain)
.alert("Position Exchange Requested", isPresented: $isPresentingPositionSentAlert) {
Button("OK") { }.keyboardShortcut(.defaultAction)
@ -152,12 +154,6 @@ struct NodeList: View {
}
}
// Helper to get the count of nodes for the navigation title
private func getNodeCount() -> Int {
let request: NSFetchRequest<NodeInfoEntity> = NodeInfoEntity.fetchRequest()
request.predicate = filters.buildPredicate()
return (try? context.count(for: request)) ?? 0
}
}
//
@ -166,8 +162,9 @@ struct NodeList: View {
//
fileprivate struct FilteredNodeList: View {
@EnvironmentObject var accessoryManager: AccessoryManager
@FetchRequest private var nodes: FetchedResults<NodeInfoEntity>
@Environment(\.managedObjectContext) var context
@Query(sort: \NodeInfoEntity.lastHeard, order: .reverse)
private var allNodes: [NodeInfoEntity]
@Environment(\.modelContext) private var context
var router: Router
@Binding var selectedNode: NodeInfoEntity?
@ -175,6 +172,8 @@ fileprivate struct FilteredNodeList: View {
@Binding var isPresentingDeleteNodeAlert: Bool
@Binding var deleteNodeId: Int64
@Binding var shareContactNode: NodeInfoEntity?
@Binding var filteredNodeCount: Int
private var filters: NodeFilterParameters
// The initializer for the FetchRequest
init(
@ -184,26 +183,21 @@ fileprivate struct FilteredNodeList: View {
connectedNode: NodeInfoEntity?,
isPresentingDeleteNodeAlert: Binding<Bool>,
deleteNodeId: Binding<Int64>,
shareContactNode: Binding<NodeInfoEntity?>
shareContactNode: Binding<NodeInfoEntity?>,
filteredNodeCount: Binding<Int>
) {
self.router = router
let request: NSFetchRequest<NodeInfoEntity> = NodeInfoEntity.fetchRequest()
request.sortDescriptors = [
NSSortDescriptor(key: "ignored", ascending: true),
NSSortDescriptor(key: "favorite", ascending: false),
NSSortDescriptor(key: "lastHeard", ascending: false),
NSSortDescriptor(key: "user.longName", ascending: true)
]
request.predicate = withFilters.buildPredicate()
request.fetchBatchSize = 50
request.relationshipKeyPathsForPrefetching = ["user"]
self._nodes = FetchRequest(fetchRequest: request)
self.filters = withFilters
self._selectedNode = selectedNode
self.connectedNode = connectedNode
self._isPresentingDeleteNodeAlert = isPresentingDeleteNodeAlert
self._deleteNodeId = deleteNodeId
self._shareContactNode = shareContactNode
self._filteredNodeCount = filteredNodeCount
}
private var nodes: [NodeInfoEntity] {
allNodes.filter { filters.matches(node: $0) }
}
// The body of the view
@ -243,9 +237,11 @@ fileprivate struct FilteredNodeList: View {
}
.onAppear {
router.updateNodeIndex(from: nodes)
filteredNodeCount = nodes.count
}
.onChange(of: nodes.count) { _, _ in
.onChange(of: nodes.count) { _, newCount in
router.updateNodeIndex(from: nodes)
filteredNodeCount = newCount
}
}
@ -330,6 +326,73 @@ fileprivate struct FilteredNodeList: View {
//
fileprivate extension NodeFilterParameters {
func matches(node: NodeInfoEntity) -> Bool {
// Search text
if !searchText.isEmpty {
let text = searchText.lowercased()
let fields = [node.user?.userId, node.user?.numString, node.user?.hwModel,
node.user?.hwDisplayName, node.user?.longName, node.user?.shortName]
let matchesSearch = fields.compactMap { $0?.lowercased() }.contains { $0.contains(text) }
if !matchesSearch { return false }
}
// Favorite
if isFavorite && !node.favorite { return false }
// Via Lora/MQTT
if viaLora && !viaMqtt && node.viaMqtt { return false }
if !viaLora && viaMqtt && !node.viaMqtt { return false }
// Roles
if roleFilter && !deviceRoles.isEmpty {
let userRole = Int(node.user?.role ?? 0)
if !deviceRoles.contains(userRole) { return false }
}
// Hops Away
if hopsAway == 0 && node.hopsAway != 0 { return false }
if hopsAway > 0 && (node.hopsAway <= 0 || node.hopsAway > Int32(hopsAway)) { return false }
// Online
if isOnline {
let twoHoursAgo = Calendar.current.date(byAdding: .minute, value: -120, to: Date()) ?? Date.distantPast
if let lastHeard = node.lastHeard, lastHeard < twoHoursAgo { return false }
if node.lastHeard == nil { return false }
}
// Encrypted
if isPkiEncrypted && node.user?.pkiEncrypted != true { return false }
// Ignored
if isIgnored {
if !node.ignored { return false }
} else {
if node.ignored { return false }
}
// Environment
if isEnvironment {
let hasEnvTelemetry = (node.telemetries ?? []).contains { $0.metricsType == 1 }
if !hasEnvTelemetry { return false }
}
// Distance
if distanceFilter {
if let poi = LocationsHandler.currentLocation,
poi.latitude != LocationsHandler.DefaultLocation.latitude,
poi.longitude != LocationsHandler.DefaultLocation.longitude {
let d = maxDistance * 1.1
let r: Double = 6371009
let meanLat = poi.latitude * .pi / 180
let deltaLat = d / r * 180 / .pi
let deltaLon = d / (r * cos(meanLat)) * 180 / .pi
let minLat = poi.latitude - deltaLat
let maxLat = poi.latitude + deltaLat
let minLon = poi.longitude - deltaLon
let maxLon = poi.longitude + deltaLon
let hasNearbyPosition = (node.positions ?? []).contains { pos in
guard pos.latest else { return false }
let lon = Double(pos.longitudeI) / 1e7
let lat = Double(pos.latitudeI) / 1e7
return lon >= minLon && lon <= maxLon && lat >= minLat && lat <= maxLat
}
if !hasNearbyPosition { return false }
}
}
return true
}
func buildPredicate() -> NSPredicate? {
var predicates: [NSPredicate] = []

View file

@ -11,7 +11,7 @@ import OSLog
struct PaxCounterLog: View {
@Environment(\.managedObjectContext) var context
@Environment(\.modelContext) private var context
@EnvironmentObject var accessoryManager: AccessoryManager
@State private var isPresentingClearLogConfirm: Bool = false
@ -21,66 +21,70 @@ struct PaxCounterLog: View {
@State private var bleChartColor: Color = .blue
@State private var wifiChartColor: Color = .orange
@State private var paxChartColor: Color = .green
@ObservedObject var node: NodeInfoEntity
@Bindable var node: NodeInfoEntity
@ViewBuilder
private func paxChart(chartData: [PaxCounterEntity], maxValue: Int32) -> some View {
Chart {
ForEach(chartData, id: \.self) { point in
Plot {
PointMark(
x: .value("x", point.time!),
y: .value("y", (point.wifi + point.ble))
)
}
.accessibilityLabel("Total PAX")
.accessibilityValue("X: \(point.time!), Y: \(point.wifi + point.ble)")
.foregroundStyle(paxChartColor)
.interpolationMethod(.cardinal)
Plot {
PointMark(
x: .value("x", point.time!),
y: .value("y", point.wifi)
)
}
.accessibilityLabel("WiFi")
.accessibilityValue("X: \(point.time!), Y: \(point.wifi)")
.foregroundStyle(wifiChartColor)
Plot {
PointMark(
x: .value("x", point.time!),
y: .value("y", point.ble)
)
}
.accessibilityLabel("BLE")
.accessibilityValue("X: \(point.time!), Y: \(point.ble)")
.foregroundStyle(bleChartColor)
}
}
.chartXAxis(content: {
AxisMarks(position: .top)
})
.chartXAxis(.automatic)
.chartYScale(domain: 0...maxValue)
.chartForegroundStyleScale([
"BLE".localized: .blue,
"WiFi".localized: .orange,
"Total PAX".localized: .green
])
.chartLegend(position: .automatic, alignment: .bottom)
}
var body: some View {
VStack {
if node.hasPax {
let oneWeekAgo = Calendar.current.date(byAdding: .day, value: -7, to: Date())
let pax = node.pax?.reversed() as? [PaxCounterEntity] ?? []
let pax = Array(node.pax.reversed())
let chartData = pax
.filter { $0.time != nil && $0.time! >= oneWeekAgo! }
.sorted { $0.time! < $1.time! }
let maxValue = (chartData.map { $0.wifi }.max() ?? 0) + (chartData.map { $0.ble }.max() ?? 0) + 5
if chartData.count > 0 {
GroupBox(label: Label("\(pax.count) Readings Total", systemImage: "chart.xyaxis.line")) {
Chart {
ForEach(chartData, id: \.self) { point in
Plot {
PointMark(
x: .value("x", point.time!),
y: .value("y", (point.wifi + point.ble))
)
}
.accessibilityLabel("Total PAX")
.accessibilityValue("X: \(point.time!), Y: \(point.wifi + point.ble)")
.foregroundStyle(paxChartColor)
.interpolationMethod(.cardinal)
Plot {
PointMark(
x: .value("x", point.time!),
y: .value("y", point.wifi)
)
}
.accessibilityLabel("WiFi")
.accessibilityValue("X: \(point.time!), Y: \(point.wifi)")
.foregroundStyle(wifiChartColor)
Plot {
PointMark(
x: .value("x", point.time!),
y: .value("y", point.ble)
)
}
.accessibilityLabel("BLE")
.accessibilityValue("X: \(point.time!), Y: \(point.ble)")
.foregroundStyle(bleChartColor)
}
}
.chartXAxis(content: {
AxisMarks(position: .top)
})
.chartXAxis(.automatic)
.chartYScale(domain: 0...maxValue)
.chartForegroundStyleScale([
"BLE".localized: .blue,
"WiFi".localized: .orange,
"Total PAX".localized: .green
])
.chartLegend(position: .automatic, alignment: .bottom)
paxChart(chartData: chartData, maxValue: maxValue)
}
.frame(minHeight: 250)
}
@ -225,15 +229,17 @@ struct PaxCounterLog: View {
}
}
// TODO: Fix preview for SwiftData
/*
#Preview {
let context = PersistenceController.preview.container.viewContext
let node = NodeInfoEntity(context: context)
let node = NodeInfoEntity()
node.num = 123456789
let user = UserEntity(context: context)
let user = UserEntity()
user.longName = "Test Node"
user.shortName = "TN"
node.user = user
return PaxCounterLog(node: node)
PaxCounterLog(node: node)
.environmentObject(AccessoryManager.shared)
.environment(\.managedObjectContext, context)
.modelContainer(PersistenceController.preview.container)
}
*/

View file

@ -8,7 +8,7 @@ import SwiftUI
import OSLog
struct PositionLog: View {
@Environment(\.managedObjectContext) var context
@Environment(\.modelContext) private var context
@EnvironmentObject var accessoryManager: AccessoryManager
@Environment(\.verticalSizeClass) var verticalSizeClass: UserInterfaceSizeClass?
@Environment(\.horizontalSizeClass) var horizontalSizeClass: UserInterfaceSizeClass?
@ -18,7 +18,7 @@ struct PositionLog: View {
}
@State var isExporting = false
@State var exportString = ""
@ObservedObject var node: NodeInfoEntity
@Bindable var node: NodeInfoEntity
@State private var isPresentingClearLogConfirm = false
@State private var sortOrder = [KeyPathComparator(\PositionEntity.time)]
@ -29,7 +29,7 @@ struct PositionLog: View {
let dateFormatString = (localeDateFormat ?? "MM/dd/YY j:mma").replacingOccurrences(of: ",", with: "")
if UIDevice.current.userInterfaceIdiom == .pad && !useGrid || UIDevice.current.userInterfaceIdiom == .mac {
// Add a table for mac and ipad
let positions = node.positions?.reversed() as? [PositionEntity] ?? []
let positions = node.positions.reversed()
Table(positions, sortOrder: $sortOrder) {
TableColumn("Latitude") { position in
Text(String(format: "%.5f", position.latitude ?? 0))
@ -93,8 +93,7 @@ struct PositionLog: View {
.font(.caption2)
.fontWeight(.bold)
}
if let positions = node.positions?.reversed() as? [PositionEntity] {
ForEach(positions, id: \.self) { (mappin: PositionEntity) in
ForEach(node.positions.reversed(), id: \.self) { (mappin: PositionEntity) in
let altitude = Measurement(value: Double(mappin.altitude), unit: UnitLength.meters)
GridRow {
Text(String(format: "%.5f", mappin.latitude ?? 0))
@ -109,7 +108,6 @@ struct PositionLog: View {
.font(.caption2)
}
}
}
}
}
.padding(.leading)
@ -141,7 +139,7 @@ struct PositionLog: View {
}
}
Button {
exportString = positionToCsvFile(positions: node.positions!.array as? [PositionEntity] ?? [])
exportString = positionToCsvFile(positions: node.positions)
isExporting = true
} label: {
Label("Save", systemImage: "square.and.arrow.down")
@ -172,7 +170,7 @@ struct PositionLog: View {
ContentUnavailableView("No Positions", systemImage: "mappin.slash")
}
}
.navigationTitle("Position Log \(node.positions?.count ?? 0) Points")
.navigationTitle("Position Log \(node.positions.count) Points")
.navigationBarItems(
trailing:
ZStack {
@ -182,15 +180,17 @@ struct PositionLog: View {
}
}
// TODO: Fix preview for SwiftData
/*
#Preview {
let context = PersistenceController.preview.container.viewContext
let node = NodeInfoEntity(context: context)
let node = NodeInfoEntity()
node.num = 123456789
let user = UserEntity(context: context)
let user = UserEntity()
user.longName = "Test Node"
user.shortName = "TN"
node.user = user
return PositionLog(node: node)
PositionLog(node: node)
.environmentObject(AccessoryManager.shared)
.environment(\.managedObjectContext, context)
.modelContainer(PersistenceController.preview.container)
}
*/

View file

@ -12,9 +12,9 @@ import OSLog
struct PowerMetricsLog: View {
@Environment(\.managedObjectContext) var context
@Environment(\.modelContext) private var context
@EnvironmentObject var accessoryManager: AccessoryManager
@ObservedObject var node: NodeInfoEntity
@Bindable var node: NodeInfoEntity
private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom }
@State private var sortOrder = [KeyPathComparator(\TelemetryEntity.time, order: .reverse)]
@State private var selection: TelemetryEntity.ID?
@ -27,8 +27,7 @@ struct PowerMetricsLog: View {
@State private var channelSelection = 0
var powerMetrics: [TelemetryEntity] {
let telemetries = node.telemetries?.filtered(using: NSPredicate(format: "metricsType == 2"))
return (telemetries?.reversed() as? [TelemetryEntity]) ?? []
return node.telemetries.filter { $0.metricsType == 2 }.reversed()
}
var minMax: (min: Double, max: Double) {
@ -298,15 +297,17 @@ struct PowerMetricsLog: View {
}
}
// TODO: Fix preview for SwiftData
/*
#Preview {
let context = PersistenceController.preview.container.viewContext
let node = NodeInfoEntity(context: context)
let node = NodeInfoEntity()
node.num = 123456789
let user = UserEntity(context: context)
let user = UserEntity()
user.longName = "Test Node"
user.shortName = "TN"
node.user = user
return PowerMetricsLog(node: node)
PowerMetricsLog(node: node)
.environmentObject(AccessoryManager.shared)
.environment(\.managedObjectContext, context)
.modelContainer(PersistenceController.preview.container)
}
*/

View file

@ -6,19 +6,19 @@
//
import SwiftUI
import CoreData
import SwiftData
import OSLog
import MapKit
struct TraceRouteLog: View {
private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom }
@ObservedObject var locationsHandler = LocationsHandler.shared
@Environment(\.managedObjectContext) var context
@Environment(\.modelContext) private var context
@EnvironmentObject var accessoryManager: AccessoryManager
@State private var isPresentingClearLogConfirm: Bool = false
@State var isExporting = false
@State var exportString = ""
@ObservedObject var node: NodeInfoEntity
@Bindable var node: NodeInfoEntity
@State private var selectedRoute: TraceRouteEntity?
// Map Configuration
@Namespace var mapScope
@ -35,7 +35,7 @@ struct TraceRouteLog: View {
HStack(alignment: .top) {
VStack {
VStack {
List(node.traceRoutes?.reversed() as? [TraceRouteEntity] ?? [], id: \.self, selection: $selectedRoute) { route in
List(node.traceRoutes.reversed(), id: \.self, selection: $selectedRoute) { route in
Label {
let routeTime = route.time?.formatted() ?? "Unknown".localized
if route.response && route.hopsTowards == route.hopsBack {
@ -139,7 +139,7 @@ struct TraceRouteLog: View {
// Set the view rotation animation after the view appeared,
// to avoid animating initial rotation
DispatchQueue.main.async {
indexes = (selectedRoute?.hops?.array.count ?? 0) * 2
indexes = (selectedRoute?.hops.count ?? 0) * 2
animation = .easeInOut(duration: 1.0)
withAnimation(.easeInOut(duration: 2.0)) {
angle = (angle == .degrees(-90) ? .degrees(-90) : .degrees(-90))
@ -165,7 +165,7 @@ struct TraceRouteLog: View {
// .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 {
// if selectedRoute?.node?.positions?.count ?? 0 > 0, let mostRecent = selectedRoute?.node?.positions?.last {
// let traceRouteCoords: [CLLocationCoordinate2D] = [selectedRoute?.coordinate ?? LocationsHandler.DefaultLocation, mostRecent.coordinate]
// Annotation(selectedRoute?.node?.user?.shortName ?? "???", coordinate: mostRecent.nodeCoordinate ?? LocationHelper.DefaultLocation) {
// ZStack {
@ -190,7 +190,7 @@ struct TraceRouteLog: View {
// /// Distance
// if selectedRoute?.node?.positions?.count ?? 0 > 0,
// selectedRoute?.coordinate != nil,
// let mostRecent = selectedRoute?.node?.positions?.lastObject as? PositionEntity {
// let mostRecent = selectedRoute?.node?.positions?.last {
// 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))
@ -224,7 +224,7 @@ struct TraceRouteLog: View {
@ViewBuilder func contents(animation: Animation? = nil) -> some View {
ForEach(0..<indexes, id: \.self) { idx in
TraceRouteComponent(animation: animation) {
let hops = selectedRoute?.hops?.array as? [TraceRouteHopEntity] ?? [] // getTraceRouteHops(context: PersistenceController.preview.container.viewContext)//
let hops = selectedRoute?.hops ?? []
if idx % 2 == 0 {
let i = idx / 2
let snrColor = getSnrColor(snr: hops[i].snr, preset: modemPreset)
@ -250,31 +250,31 @@ struct TraceRouteLog: View {
}
}
func getTraceRouteHops(context: NSManagedObjectContext) -> [TraceRouteHopEntity] {
/// static let context = PersistenceController.preview.container.viewContext
func getTraceRouteHops(context: ModelContext) -> [TraceRouteHopEntity] {
/// static let context = PersistenceController.preview.context
var array = [TraceRouteHopEntity]()
let trh1 = TraceRouteHopEntity(context: context)
let trh1 = TraceRouteHopEntity()
trh1.num = 366311664
trh1.snr = 12.5
let trh2 = TraceRouteHopEntity(context: context)
let trh2 = TraceRouteHopEntity()
trh2.num = 3662955168
trh2.snr = -115.00
let trh3 = TraceRouteHopEntity(context: context)
let trh3 = TraceRouteHopEntity()
trh3.num = 3663982804
trh3.snr = 17.5
let trh4 = TraceRouteHopEntity(context: context)
let trh4 = TraceRouteHopEntity()
trh4.num = 4202719792
trh4.snr = 7.0
let trh5 = TraceRouteHopEntity(context: context)
let trh5 = TraceRouteHopEntity()
trh5.num = 603700594
trh5.snr = 8.9
let trh6 = TraceRouteHopEntity(context: context)
let trh6 = TraceRouteHopEntity()
trh6.num = 836212501
trh6.snr = -24.0
let trh7 = TraceRouteHopEntity(context: context)
let trh7 = TraceRouteHopEntity()
trh7.num = 3663116644
trh7.snr = -6.0
let trh8 = TraceRouteHopEntity(context: context)
let trh8 = TraceRouteHopEntity()
trh8.num = 8362955168
trh8.snr = 7.5
array.append(trh1)
@ -288,15 +288,17 @@ func getTraceRouteHops(context: NSManagedObjectContext) -> [TraceRouteHopEntity]
return array
}
// TODO: Fix preview for SwiftData
/*
#Preview {
let context = PersistenceController.preview.container.viewContext
let node = NodeInfoEntity(context: context)
let node = NodeInfoEntity()
node.num = 123456789
let user = UserEntity(context: context)
let user = UserEntity()
user.longName = "Test Node"
user.shortName = "TN"
node.user = user
return TraceRouteLog(node: node)
TraceRouteLog(node: node)
.environmentObject(AccessoryManager.shared)
.environment(\.managedObjectContext, context)
.modelContainer(PersistenceController.preview.container)
}
*/