From 02cca19f263a5e266240353eb6c14eeb5ffe7dff Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sat, 23 Mar 2024 09:01:44 -0700 Subject: [PATCH] Assorted updates --- Meshtastic.xcodeproj/project.pbxproj | 14 +- Meshtastic/Enums/MessagingEnums.swift | 19 +- .../CoreData/PositionEntityExtension.swift | 11 + Meshtastic/Helpers/MeshPackets.swift | 18 -- Meshtastic/Persistence/UpdateCoreData.swift | 15 +- .../Protobufs/meshtastic/deviceonly.pb.swift | 16 ++ Meshtastic/Protobufs/meshtastic/mesh.pb.swift | 16 ++ .../Views/Messages/UserMessageList.swift | 2 +- .../Map/MapContent/MeshMapContent.swift | 162 ++++++++++++++ .../Map/{ => MapContent}/NodeMapContent.swift | 0 .../Views/Nodes/Helpers/NodeListItem.swift | 2 +- Meshtastic/Views/Nodes/MeshMap.swift | 200 ++++++------------ de.lproj/Localizable.strings | 1 + en.lproj/Localizable.strings | 1 + fr.lproj/Localizable.strings | 1 + he.lproj/Localizable.strings | 1 + pl.lproj/Localizable.strings | 1 + protobufs | 2 +- zh-Hans.lproj/Localizable.strings | 1 + zh-Hant-TW.lproj/Localizable.strings | 1 + 20 files changed, 318 insertions(+), 166 deletions(-) create mode 100644 Meshtastic/Views/Nodes/Helpers/Map/MapContent/MeshMapContent.swift rename Meshtastic/Views/Nodes/Helpers/Map/{ => MapContent}/NodeMapContent.swift (100%) diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index 87b0ed30..ebc7efee 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -185,6 +185,7 @@ DDDB445029F8AC9C00EE2349 /* UIImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDDB444F29F8AC9C00EE2349 /* UIImage.swift */; }; DDDB445229F8ACF900EE2349 /* Date.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDDB445129F8ACF900EE2349 /* Date.swift */; }; DDDB445429F8AD1600EE2349 /* Data.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDDB445329F8AD1600EE2349 /* Data.swift */; }; + DDDC22382BA92344002C44F1 /* MeshMapContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDDC22372BA92344002C44F1 /* MeshMapContent.swift */; }; DDDE59F529AF163D00490C6C /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DD41A61C29AE7E8E003C5A37 /* WidgetKit.framework */; }; DDDE59F629AF163D00490C6C /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DD41A61E29AE7E8F003C5A37 /* SwiftUI.framework */; }; DDDE59F929AF163D00490C6C /* WidgetsBundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDDE59F829AF163D00490C6C /* WidgetsBundle.swift */; }; @@ -456,6 +457,7 @@ DDDB445329F8AD1600EE2349 /* Data.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Data.swift; sourceTree = ""; }; DDDC22312BA76701002C44F1 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/Localizable.strings; sourceTree = ""; }; DDDC22322BA76961002C44F1 /* zh-Hant-TW */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hant-TW"; path = "zh-Hant-TW.lproj/Localizable.strings"; sourceTree = ""; }; + DDDC22372BA92344002C44F1 /* MeshMapContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeshMapContent.swift; sourceTree = ""; }; DDDD527729B5B83F0045BC3C /* MeshtasticDataModelV9.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV9.xcdatamodel; sourceTree = ""; }; DDDE59F429AF163D00490C6C /* WidgetsExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = WidgetsExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; DDDE59F829AF163D00490C6C /* WidgetsBundle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetsBundle.swift; sourceTree = ""; }; @@ -747,7 +749,7 @@ DDAD49EB2AFAE82500B4425D /* Map */ = { isa = PBXGroup; children = ( - DD93800A2BA3F968008BEC06 /* NodeMapContent.swift */, + DDDC22362BA9232C002C44F1 /* MapContent */, DD94B73F2ACCE3BE00DCD1D1 /* MapSettingsForm.swift */, DDB6CCFA2AAF805100945AF6 /* NodeMapSwiftUI.swift */, DD13AA482AB73BF400BA0C98 /* PositionPopover.swift */, @@ -986,6 +988,15 @@ path = Extensions; sourceTree = ""; }; + DDDC22362BA9232C002C44F1 /* MapContent */ = { + isa = PBXGroup; + children = ( + DDDC22372BA92344002C44F1 /* MeshMapContent.swift */, + DD93800A2BA3F968008BEC06 /* NodeMapContent.swift */, + ); + path = MapContent; + sourceTree = ""; + }; DDDE59F729AF163D00490C6C /* Widgets */ = { isa = PBXGroup; children = ( @@ -1214,6 +1225,7 @@ DD5E523F298F5A9E00D21B61 /* AirQualityIndexCompact.swift in Sources */, DD964FBF296E76EF007C176F /* WaypointFormMapKit.swift in Sources */, DD3501892852FC3B000FC853 /* Settings.swift in Sources */, + DDDC22382BA92344002C44F1 /* MeshMapContent.swift in Sources */, DDDB443629F6287000EE2349 /* MapButtons.swift in Sources */, DD5D0A9C2931B9F200F7EA61 /* EthernetModes.swift in Sources */, 6DEDA55A2A957B8E00321D2E /* DetectionSensorLog.swift in Sources */, diff --git a/Meshtastic/Enums/MessagingEnums.swift b/Meshtastic/Enums/MessagingEnums.swift index 010c324a..193060fa 100644 --- a/Meshtastic/Enums/MessagingEnums.swift +++ b/Meshtastic/Enums/MessagingEnums.swift @@ -13,17 +13,20 @@ enum BubblePosition { enum Tapbacks: Int, CaseIterable, Identifiable { - case heart = 0 - case thumbsUp = 1 - case thumbsDown = 2 - case haHa = 3 - case exclamation = 4 - case question = 5 - case poop = 6 + case wave = 0 + case heart = 1 + case thumbsUp = 2 + case thumbsDown = 3 + case haHa = 4 + case exclamation = 5 + case question = 6 + case poop = 7 var id: Int { self.rawValue } var emojiString: String { switch self { + case .wave: + return "👋" case .heart: return "❤️" case .thumbsUp: @@ -42,6 +45,8 @@ enum Tapbacks: Int, CaseIterable, Identifiable { } var description: String { switch self { + case .wave: + return "tapback.wave".localized case .heart: return "tapback.heart".localized case .thumbsUp: diff --git a/Meshtastic/Extensions/CoreData/PositionEntityExtension.swift b/Meshtastic/Extensions/CoreData/PositionEntityExtension.swift index d9079158..4b9ce0c2 100644 --- a/Meshtastic/Extensions/CoreData/PositionEntityExtension.swift +++ b/Meshtastic/Extensions/CoreData/PositionEntityExtension.swift @@ -11,6 +11,17 @@ import MapKit import SwiftUI extension PositionEntity { + + static func allPositionsFetchRequest() -> NSFetchRequest { + let request: NSFetchRequest = PositionEntity.fetchRequest() + // request.fetchLimit = 100 + //request.fetchBatchSize = 2 + //request.includesSubentities = false + request.returnsDistinctResults = true + request.sortDescriptors = [NSSortDescriptor(key: "time", ascending: true)] + request.predicate = NSPredicate(format: "nodePosition != nil && latest == true", Calendar.current.date(byAdding: .day, value: -7, to: Date())! as NSDate) + return request + } var latitude: Double? { diff --git a/Meshtastic/Helpers/MeshPackets.swift b/Meshtastic/Helpers/MeshPackets.swift index c02835e7..e89aeb82 100644 --- a/Meshtastic/Helpers/MeshPackets.swift +++ b/Meshtastic/Helpers/MeshPackets.swift @@ -724,24 +724,6 @@ func telemetryPacket(packet: MeshPacket, connectedNode: Int64, context: NSManage ) ] manager.schedule() - -// let content = UNMutableNotificationContent() -// content.title = "Critically Low Battery!" -// content.body = "Time to charge your radio, there is \(telemetry.batteryLevel)% battery remaining." -// content.userInfo["target"] = "node" -// content.userInfo["path"] = "meshtastic://node/\(telemetry.nodeTelemetry?.num ?? 0)" -// let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 1, repeats: false) -// let uuidString = UUID().uuidString -// let request = UNNotificationRequest(identifier: uuidString, content: content, trigger: trigger) -// let notificationCenter = UNUserNotificationCenter.current() -// notificationCenter.add(request) { (error) in -// if error != nil { -// // Handle any errors. -// print("Error creating local low battery notification: \(error?.localizedDescription ?? "no description")") -// } else { -// print("Created local low battery notification.") -// } -// } } // Update our live activity if there is one running, not available on mac iOS >= 16.2 #if !targetEnvironment(macCatalyst) diff --git a/Meshtastic/Persistence/UpdateCoreData.swift b/Meshtastic/Persistence/UpdateCoreData.swift index 121cab33..98561c0d 100644 --- a/Meshtastic/Persistence/UpdateCoreData.swift +++ b/Meshtastic/Persistence/UpdateCoreData.swift @@ -148,7 +148,9 @@ func upsertNodeInfoPacket (packet: MeshPacket, context: NSManagedObjectContext) newNode.channel = Int32(packet.channel) } if let nodeInfoMessage = try? NodeInfo(serializedData: packet.decoded.payload) { - newNode.hopsAway = Int32(truncatingIfNeeded: nodeInfoMessage.hopsAway) + newNode.hopsAway = Int32(nodeInfoMessage.hopsAway) + } else if packet.hopStart != 0 && packet.hopLimit <= packet.hopStart { + newNode.hopsAway = Int32(packet.hopStart - packet.hopLimit) } if let newUserMessage = try? User(serializedData: packet.decoded.payload) { @@ -218,8 +220,8 @@ func upsertNodeInfoPacket (packet: MeshPacket, context: NSManagedObjectContext) } if let nodeInfoMessage = try? NodeInfo(serializedData: packet.decoded.payload) { - fetchedNode[0].channel = Int32(nodeInfoMessage.channel) - fetchedNode[0].hopsAway = Int32(truncatingIfNeeded: nodeInfoMessage.hopsAway) + + fetchedNode[0].hopsAway = Int32(nodeInfoMessage.hopsAway) if nodeInfoMessage.hasDeviceMetrics { let telemetry = TelemetryEntity(context: context) telemetry.batteryLevel = Int32(nodeInfoMessage.deviceMetrics.batteryLevel) @@ -231,6 +233,7 @@ func upsertNodeInfoPacket (packet: MeshPacket, context: NSManagedObjectContext) fetchedNode[0].telemetries? = NSOrderedSet(array: newTelemetries) } if nodeInfoMessage.hasUser { + fetchedNode[0].user!.vip = nodeInfoMessage.isFavorite /// Seeing Some crashes here ? fetchedNode[0].user!.userId = nodeInfoMessage.user.id fetchedNode[0].user!.num = Int64(nodeInfoMessage.num) @@ -239,6 +242,8 @@ func upsertNodeInfoPacket (packet: MeshPacket, context: NSManagedObjectContext) fetchedNode[0].user!.role = Int32(nodeInfoMessage.user.role.rawValue) fetchedNode[0].user!.hwModel = String(describing: nodeInfoMessage.user.hwModel).uppercased() } + } else if packet.hopStart != 0 && packet.hopLimit <= packet.hopStart { + fetchedNode[0].hopsAway = Int32(packet.hopStart - packet.hopLimit) } if (fetchedNode[0].user == nil) { let newUser = UserEntity(context: context) @@ -317,9 +322,9 @@ func upsertPositionPacket (packet: MeshPacket, context: NSManagedObjectContext) return } /// Don't save nearly the same position over and over. If the next position is less than 10 meters from the new position, delete the previous position and save the new one. - if mutablePositions.count > 0 && position.precisionBits == 32 { + if mutablePositions.count > 0 && (position.precisionBits == 32 || position.precisionBits == 0) { let mostRecent = mutablePositions.lastObject as! PositionEntity - if mostRecent.coordinate.distance(from: position.coordinate) < 15 { + if mostRecent.coordinate.distance(from: position.coordinate) < 15.0 { mutablePositions.remove(mostRecent) } } else if mutablePositions.count > 0 && 11...16 ~= position.precisionBits { diff --git a/Meshtastic/Protobufs/meshtastic/deviceonly.pb.swift b/Meshtastic/Protobufs/meshtastic/deviceonly.pb.swift index 950c577e..f25a5cd1 100644 --- a/Meshtastic/Protobufs/meshtastic/deviceonly.pb.swift +++ b/Meshtastic/Protobufs/meshtastic/deviceonly.pb.swift @@ -269,6 +269,14 @@ struct NodeInfoLite { set {_uniqueStorage()._hopsAway = newValue} } + /// + /// True if node is in our favorites list + /// Persists between NodeDB internal clean ups + var isFavorite: Bool { + get {return _storage._isFavorite} + set {_uniqueStorage()._isFavorite = newValue} + } + var unknownFields = SwiftProtobuf.UnknownStorage() init() {} @@ -600,6 +608,7 @@ extension NodeInfoLite: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat 7: .same(proto: "channel"), 8: .standard(proto: "via_mqtt"), 9: .standard(proto: "hops_away"), + 10: .standard(proto: "is_favorite"), ] fileprivate class _StorageClass { @@ -612,6 +621,7 @@ extension NodeInfoLite: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat var _channel: UInt32 = 0 var _viaMqtt: Bool = false var _hopsAway: UInt32 = 0 + var _isFavorite: Bool = false static let defaultInstance = _StorageClass() @@ -627,6 +637,7 @@ extension NodeInfoLite: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat _channel = source._channel _viaMqtt = source._viaMqtt _hopsAway = source._hopsAway + _isFavorite = source._isFavorite } } @@ -654,6 +665,7 @@ extension NodeInfoLite: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat case 7: try { try decoder.decodeSingularUInt32Field(value: &_storage._channel) }() case 8: try { try decoder.decodeSingularBoolField(value: &_storage._viaMqtt) }() case 9: try { try decoder.decodeSingularUInt32Field(value: &_storage._hopsAway) }() + case 10: try { try decoder.decodeSingularBoolField(value: &_storage._isFavorite) }() default: break } } @@ -693,6 +705,9 @@ extension NodeInfoLite: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat if _storage._hopsAway != 0 { try visitor.visitSingularUInt32Field(value: _storage._hopsAway, fieldNumber: 9) } + if _storage._isFavorite != false { + try visitor.visitSingularBoolField(value: _storage._isFavorite, fieldNumber: 10) + } } try unknownFields.traverse(visitor: &visitor) } @@ -711,6 +726,7 @@ extension NodeInfoLite: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat if _storage._channel != rhs_storage._channel {return false} if _storage._viaMqtt != rhs_storage._viaMqtt {return false} if _storage._hopsAway != rhs_storage._hopsAway {return false} + if _storage._isFavorite != rhs_storage._isFavorite {return false} return true } if !storagesAreEqual {return false} diff --git a/Meshtastic/Protobufs/meshtastic/mesh.pb.swift b/Meshtastic/Protobufs/meshtastic/mesh.pb.swift index 2890db6c..18da455b 100644 --- a/Meshtastic/Protobufs/meshtastic/mesh.pb.swift +++ b/Meshtastic/Protobufs/meshtastic/mesh.pb.swift @@ -1861,6 +1861,14 @@ struct NodeInfo { set {_uniqueStorage()._hopsAway = newValue} } + /// + /// True if node is in our favorites list + /// Persists between NodeDB internal clean ups + var isFavorite: Bool { + get {return _storage._isFavorite} + set {_uniqueStorage()._isFavorite = newValue} + } + var unknownFields = SwiftProtobuf.UnknownStorage() init() {} @@ -3647,6 +3655,7 @@ extension NodeInfo: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB 7: .same(proto: "channel"), 8: .standard(proto: "via_mqtt"), 9: .standard(proto: "hops_away"), + 10: .standard(proto: "is_favorite"), ] fileprivate class _StorageClass { @@ -3659,6 +3668,7 @@ extension NodeInfo: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB var _channel: UInt32 = 0 var _viaMqtt: Bool = false var _hopsAway: UInt32 = 0 + var _isFavorite: Bool = false static let defaultInstance = _StorageClass() @@ -3674,6 +3684,7 @@ extension NodeInfo: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB _channel = source._channel _viaMqtt = source._viaMqtt _hopsAway = source._hopsAway + _isFavorite = source._isFavorite } } @@ -3701,6 +3712,7 @@ extension NodeInfo: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB case 7: try { try decoder.decodeSingularUInt32Field(value: &_storage._channel) }() case 8: try { try decoder.decodeSingularBoolField(value: &_storage._viaMqtt) }() case 9: try { try decoder.decodeSingularUInt32Field(value: &_storage._hopsAway) }() + case 10: try { try decoder.decodeSingularBoolField(value: &_storage._isFavorite) }() default: break } } @@ -3740,6 +3752,9 @@ extension NodeInfo: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB if _storage._hopsAway != 0 { try visitor.visitSingularUInt32Field(value: _storage._hopsAway, fieldNumber: 9) } + if _storage._isFavorite != false { + try visitor.visitSingularBoolField(value: _storage._isFavorite, fieldNumber: 10) + } } try unknownFields.traverse(visitor: &visitor) } @@ -3758,6 +3773,7 @@ extension NodeInfo: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB if _storage._channel != rhs_storage._channel {return false} if _storage._viaMqtt != rhs_storage._viaMqtt {return false} if _storage._hopsAway != rhs_storage._hopsAway {return false} + if _storage._isFavorite != rhs_storage._isFavorite {return false} return true } if !storagesAreEqual {return false} diff --git a/Meshtastic/Views/Messages/UserMessageList.swift b/Meshtastic/Views/Messages/UserMessageList.swift index 7f0660ee..c62def59 100644 --- a/Meshtastic/Views/Messages/UserMessageList.swift +++ b/Meshtastic/Views/Messages/UserMessageList.swift @@ -73,7 +73,7 @@ struct UserMessageList: View { if message.realACK { Text("\(ackErrorVal?.display ?? "Empty Ack Error")").font(.caption2).foregroundColor(.gray) } else { - Text("Implicit ACK from another node").font(.caption2).foregroundColor(.orange) + Text("Acknowledged by another node").font(.caption2).foregroundColor(.orange) } } else if currentUser && message.ackError == 0 { // Empty Error diff --git a/Meshtastic/Views/Nodes/Helpers/Map/MapContent/MeshMapContent.swift b/Meshtastic/Views/Nodes/Helpers/Map/MapContent/MeshMapContent.swift new file mode 100644 index 00000000..0e0c519f --- /dev/null +++ b/Meshtastic/Views/Nodes/Helpers/Map/MapContent/MeshMapContent.swift @@ -0,0 +1,162 @@ +// +// MeshMapContent.swift +// Meshtastic +// +// Created by Garth Vander Houwen on 3/17/24. +// + +import SwiftUI +import MapKit + +import SwiftUI +import MapKit + +@available(iOS 17.0, macOS 14.0, *) +struct MeshMapContent: MapContent { + + @State var positions: [PositionEntity] = [] + @State var waypoints: [WaypointEntity] = [] + @State var routes: [RouteEntity] = [] + /// Parameters + @Binding var showUserLocation: Bool + @Binding var showNodeHistory: Bool + @Binding var showRouteLines: Bool + @Binding var showConvexHull: Bool + @Binding var showTraffic: Bool + @Binding var showPointsOfInterest: Bool + @Binding var selectedMapLayer: MapLayer + // Map Configuration + ///@Namespace var mapScope + + @State var mapStyle: MapStyle = MapStyle.standard(elevation: .realistic, emphasis: MapStyle.StandardEmphasis.muted ,pointsOfInterest: .excludingAll, showsTraffic: false) + //@State var position = MapCameraPosition.automatic + //@State var scene: MKLookAroundScene? + //@State var isLookingAround = false + //@State var isEditingSettings = false + @Binding var selectedPosition: PositionEntity? + @Binding var showWaypoints: Bool + //@Binding var editingWaypoint: WaypointEntity? + @Binding var selectedWaypoint: WaypointEntity? + + var delay: Double = 0 + @State private var scale: CGFloat = 0.5 + + @MapContentBuilder + var meshMap: some MapContent { + let lineCoords = positions.compactMap({(position) -> CLLocationCoordinate2D in + return position.nodeCoordinate ?? LocationsHandler.DefaultLocation + }) + /// Convex Hull + if showConvexHull { + if lineCoords.count > 0 { + let hull = lineCoords.getConvexHull() + MapPolygon(coordinates: hull) + .stroke(.blue, lineWidth: 3) + .foregroundStyle(.indigo.opacity(0.4)) + } + } + /// Position Annotations + ForEach(Array(positions), id: \.id) { position in + /// Node color from node.num + let nodeColor = UIColor(hex: UInt32(position.nodePosition?.num ?? 0)) + Annotation(position.nodePosition?.user?.longName ?? "?", coordinate: position.coordinate) { + LazyVStack { + ZStack { + let nodeColor = UIColor(hex: UInt32(position.nodePosition?.num ?? 0)) + if position.nodePosition?.isOnline ?? false { + Circle() + .fill(Color(nodeColor.lighter()).opacity(0.4).shadow(.drop(color: Color(nodeColor).isLight() ? .black : .white, radius: 5))) + .foregroundStyle(Color(nodeColor.lighter()).opacity(0.3)) +// .scaleEffect(scale) +// .animation( +// Animation.easeInOut(duration: 0.6) +// .repeatForever().delay(delay), value: scale +// ) +// .onAppear { +// self.scale = 1 +// } +// .frame(width: 60, height: 60) + } + if position.nodePosition?.hasDetectionSensorMetrics ?? false { + Image(systemName: "sensor.fill") + .symbolRenderingMode(.palette) + .symbolEffect(.variableColor) + .padding() + .foregroundStyle(.white) + .background(Color(nodeColor)) + .clipShape(Circle()) + } else { + CircleText(text: position.nodePosition?.user?.shortName ?? "?", color: Color(nodeColor), circleSize: 40) + } + } + } + .onTapGesture { location in + selectedPosition = (selectedPosition == position ? nil : position) + } + } + /// Reduced Precision Map Circles + if 11...16 ~= position.precisionBits { + let pp = PositionPrecision(rawValue: Int(position.precisionBits)) + let radius : CLLocationDistance = pp?.precisionMeters ?? 0 + if radius > 0.0 { + MapCircle(center: position.coordinate, radius: radius) + .foregroundStyle(Color(nodeColor).opacity(0.25)) + .stroke(.white, lineWidth: 2) + } + } + /// Routes + ForEach(Array(routes), id: \.id) { route in + let routeLocations = Array(route.locations!) as! [LocationEntity] + let routeCoords = routeLocations.compactMap({(loc) -> CLLocationCoordinate2D in + return loc.locationCoordinate ?? LocationHelper.DefaultLocation + }) + Annotation("Start", coordinate: routeCoords.first ?? LocationHelper.DefaultLocation) { + ZStack { + Circle() + .fill(Color(.green)) + .strokeBorder(.white, lineWidth: 3) + .frame(width: 15, height: 15) + } + } + .annotationTitles(.automatic) + Annotation("Finish", coordinate: routeCoords.last ?? LocationHelper.DefaultLocation) { + ZStack { + Circle() + .fill(Color(.black)) + .strokeBorder(.white, lineWidth: 3) + .frame(width: 15, height: 15) + } + } + .annotationTitles(.automatic) + let solid = StrokeStyle( + lineWidth: 3, + lineCap: .round, lineJoin: .round + ) + MapPolyline(coordinates: routeCoords) + .stroke(Color(UIColor(hex: UInt32(route.color))), style: solid) + + } + } + + /// Waypoint Annotations + if waypoints.count > 0 && showWaypoints { + ForEach(Array(waypoints), id: \.id) { waypoint in + Annotation(waypoint.name ?? "?", coordinate: waypoint.coordinate) { + LazyVStack { + CircleText(text: String(UnicodeScalar(Int(waypoint.icon)) ?? "📍"), color: Color.orange, circleSize: 40) + .onTapGesture(perform: { location in + selectedWaypoint = (selectedWaypoint == waypoint ? nil : waypoint) + }) + } + } + } + } + } + + @MapContentBuilder + var body: some MapContent { + if positions.count > 0 { + meshMap + } + } +} diff --git a/Meshtastic/Views/Nodes/Helpers/Map/NodeMapContent.swift b/Meshtastic/Views/Nodes/Helpers/Map/MapContent/NodeMapContent.swift similarity index 100% rename from Meshtastic/Views/Nodes/Helpers/Map/NodeMapContent.swift rename to Meshtastic/Views/Nodes/Helpers/Map/MapContent/NodeMapContent.swift diff --git a/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift b/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift index c880d27b..feb528f6 100644 --- a/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift +++ b/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift @@ -130,7 +130,7 @@ struct NodeListItem: View { .font(UIDevice.current.userInterfaceIdiom == .phone ? .callout : .caption) } } - + if node.viaMqtt && connectedNode != node.num { Image(systemName: "network") .symbolRenderingMode(.hierarchical) diff --git a/Meshtastic/Views/Nodes/MeshMap.swift b/Meshtastic/Views/Nodes/MeshMap.swift index 4394efce..ddc66314 100644 --- a/Meshtastic/Views/Nodes/MeshMap.swift +++ b/Meshtastic/Views/Nodes/MeshMap.swift @@ -13,6 +13,8 @@ import Foundation import MapKit #endif + + @available(iOS 17.0, macOS 14.0, *) struct MeshMap: View { @@ -32,22 +34,36 @@ struct MeshMap: View { @Namespace var mapScope @State var mapStyle: MapStyle = MapStyle.standard(elevation: .realistic, emphasis: MapStyle.StandardEmphasis.muted ,pointsOfInterest: .all, showsTraffic: true) @State var position = MapCameraPosition.automatic - @State var scene: MKLookAroundScene? - @State var isLookingAround = false + //@State var scene: MKLookAroundScene? + //@State var isLookingAround = false @State var isEditingSettings = false @State var selectedPosition: PositionEntity? @State var showWaypoints = true @State var editingWaypoint: WaypointEntity? @State var selectedWaypoint: WaypointEntity? - @State var newWaypointCoord :CLLocationCoordinate2D? + @State var newWaypointCoord: CLLocationCoordinate2D? @State var isMeshMap = true - var delay: Double = 0 - @State private var scale: CGFloat = 0.5 + let positionRequest: NSFetchRequest = { + // Create a fetch request. + let request = PositionEntity.fetchRequest() + + // Limit the maximum number of items that the request returns. + request.fetchLimit = 100 + + // Filter the request results, such as to only return unchecked items. + request.predicate = NSPredicate(format: "nodePosition != nil && latest == true && time >= %@", Calendar.current.date(byAdding: .hour, value: -6, to: Date())! as NSDate) + + // Sort the fetched results + request.sortDescriptors = [NSSortDescriptor(key: "time", ascending: true)] + + + return request + }() + /// && time >= %@ - @FetchRequest(sortDescriptors: [NSSortDescriptor(key: "time", ascending: true)], - predicate: NSPredicate(format: "nodePosition != nil && latest == true", Calendar.current.date(byAdding: .day, value: -7, to: Date())! as NSDate), animation: .none) - private var positions: FetchedResults + @FetchRequest(fetchRequest: PositionEntity.allPositionsFetchRequest(), animation: .none) + var positions: FetchedResults @FetchRequest(sortDescriptors: [NSSortDescriptor(key: "name", ascending: false)], predicate: NSPredicate( @@ -61,118 +77,46 @@ struct MeshMap: View { var body: some View { - let lineCoords = Array(positions).compactMap({(position) -> CLLocationCoordinate2D in - return position.nodeCoordinate ?? LocationHelper.DefaultLocation - }) NavigationStack { ZStack { MapReader { reader in Map(position: $position, bounds: MapCameraBounds(minimumDistance: 1, maximumDistance: .infinity), scope: mapScope) { - /// Convex Hull - if showConvexHull { - if lineCoords.count > 0 { - let hull = lineCoords.getConvexHull() - MapPolygon(coordinates: hull) - .stroke(.blue, lineWidth: 3) - .foregroundStyle(.indigo.opacity(0.4)) - } - } - /// Position Annotations - ForEach(Array(positions), id: \.id) { position in - /// Node color from node.num - let nodeColor = UIColor(hex: UInt32(position.nodePosition?.num ?? 0)) - Annotation(position.nodePosition?.user?.longName ?? "?", coordinate: position.coordinate) { - LazyVStack { - ZStack { - let nodeColor = UIColor(hex: UInt32(position.nodePosition?.num ?? 0)) - if position.nodePosition?.isOnline ?? false { - Circle() - .fill(Color(nodeColor.lighter()).opacity(0.4).shadow(.drop(color: Color(nodeColor).isLight() ? .black : .white, radius: 5))) - .foregroundStyle(Color(nodeColor.lighter()).opacity(0.3)) - .scaleEffect(scale) - .animation( - Animation.easeInOut(duration: 0.6) - .repeatForever().delay(delay), value: scale - ) - .onAppear { - self.scale = 1 - } - .frame(width: 60, height: 60) - } - if position.nodePosition?.hasDetectionSensorMetrics ?? false { - Image(systemName: "sensor.fill") - .symbolRenderingMode(.palette) - .symbolEffect(.variableColor) - .padding() - .foregroundStyle(.white) - .background(Color(nodeColor)) - .clipShape(Circle()) - } else { - CircleText(text: position.nodePosition?.user?.shortName ?? "?", color: Color(nodeColor), circleSize: 40) - } - } - } - .onTapGesture { location in - selectedPosition = (selectedPosition == position ? nil : position) - } - } - /// Reduced Precision Map Circles - if 11...16 ~= position.precisionBits { - let pp = PositionPrecision(rawValue: Int(position.precisionBits)) - let radius : CLLocationDistance = pp?.precisionMeters ?? 0 - if radius > 0.0 { - MapCircle(center: position.coordinate, radius: radius) - .foregroundStyle(Color(nodeColor).opacity(0.25)) - .stroke(.white, lineWidth: 2) - } - } - /// Routes - ForEach(Array(routes), id: \.id) { route in - let routeLocations = Array(route.locations!) as! [LocationEntity] - let routeCoords = routeLocations.compactMap({(loc) -> CLLocationCoordinate2D in - return loc.locationCoordinate ?? LocationHelper.DefaultLocation - }) - Annotation("Start", coordinate: routeCoords.first ?? LocationHelper.DefaultLocation) { - ZStack { - Circle() - .fill(Color(.green)) - .strokeBorder(.white, lineWidth: 3) - .frame(width: 15, height: 15) - } - } - .annotationTitles(.automatic) - Annotation("Finish", coordinate: routeCoords.last ?? LocationHelper.DefaultLocation) { - ZStack { - Circle() - .fill(Color(.black)) - .strokeBorder(.white, lineWidth: 3) - .frame(width: 15, height: 15) - } - } - .annotationTitles(.automatic) - let solid = StrokeStyle( - lineWidth: 3, - lineCap: .round, lineJoin: .round - ) - MapPolyline(coordinates: routeCoords) - .stroke(Color(UIColor(hex: UInt32(route.color))), style: solid) - - } - } - - /// Waypoint Annotations - if waypoints.count > 0 && showWaypoints { - ForEach(Array(waypoints), id: \.id) { waypoint in - Annotation(waypoint.name ?? "?", coordinate: waypoint.coordinate) { - LazyVStack { - CircleText(text: String(UnicodeScalar(Int(waypoint.icon)) ?? "📍"), color: Color.orange, circleSize: 40) - .onTapGesture(perform: { location in - selectedWaypoint = (selectedWaypoint == waypoint ? nil : waypoint) - }) - } - } - } - } + MeshMapContent(positions: Array(positions), waypoints: Array(waypoints), routes: Array(routes), showUserLocation: $showUserLocation, showNodeHistory: $showNodeHistory, showRouteLines: $showRouteLines, showConvexHull: $showConvexHull, showTraffic: $showTraffic, showPointsOfInterest: $showPointsOfInterest, selectedMapLayer: $selectedMapLayer, selectedPosition: $selectedPosition, showWaypoints: $showWaypoints, selectedWaypoint: $selectedWaypoint) + +// /// Routes +// ForEach(Array(routes), id: \.id) { route in +// let routeLocations = Array(route.locations!) as! [LocationEntity] +// let routeCoords = routeLocations.compactMap({(loc) -> CLLocationCoordinate2D in +// return loc.locationCoordinate ?? LocationHelper.DefaultLocation +// }) +// Annotation("Start", coordinate: routeCoords.first ?? LocationHelper.DefaultLocation) { +// ZStack { +// Circle() +// .fill(Color(.green)) +// .strokeBorder(.white, lineWidth: 3) +// .frame(width: 15, height: 15) +// } +// } +// .annotationTitles(.automatic) +// Annotation("Finish", coordinate: routeCoords.last ?? LocationHelper.DefaultLocation) { +// ZStack { +// Circle() +// .fill(Color(.black)) +// .strokeBorder(.white, lineWidth: 3) +// .frame(width: 15, height: 15) +// } +// } +// .annotationTitles(.automatic) +// let solid = StrokeStyle( +// lineWidth: 3, +// lineCap: .round, lineJoin: .round +// ) +// MapPolyline(coordinates: routeCoords) +// .stroke(Color(UIColor(hex: UInt32(route.color))), style: solid) +// +// } +// } +// } .mapScope(mapScope) .mapStyle(mapStyle) @@ -185,8 +129,14 @@ struct MeshMap: View { .mapControlVisibility(.automatic) } .controlSize(.regular) + .onTapGesture(count: 1, perform: { + position in + print(position) + // tapText = "map tap" + newWaypointCoord = reader.convert(position, from: .local) ?? CLLocationCoordinate2D.init() + }) .onTapGesture(count: 1, perform: { location in - newWaypointCoord = reader.convert(location , from: .local) + // newWaypointCoord = reader.convert(location , from: .local) }) .onLongPressGesture(minimumDuration: 0.5, maximumDistance: 10) { editingWaypoint = WaypointEntity(context: context) @@ -286,26 +236,12 @@ struct MeshMap: View { .foregroundColor(.accentColor) .buttonStyle(.borderedProminent) } - /// 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) - } } .controlSize(.regular) .padding(5) } } - .navigationTitle("Mesh Map") + .navigationTitle("\(positions.count) Nodes") .navigationBarItems(leading: MeshtasticLogo(), trailing: ZStack { ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "?") }) diff --git a/de.lproj/Localizable.strings b/de.lproj/Localizable.strings index ada6bdc1..2599d7b2 100644 --- a/de.lproj/Localizable.strings +++ b/de.lproj/Localizable.strings @@ -309,6 +309,7 @@ "tapback.exclamation"="Ausrufezeichen"; "tapback.question"="Fragezeichen"; "tapback.poop"="Kacke"; +"tapback.wave"="Wave"; "telemetry"="Telemetrie (Sensoren)"; "telemetry.config"="Telemetrie Einstellungen"; "timeout"="Zeitlimit erreicht"; diff --git a/en.lproj/Localizable.strings b/en.lproj/Localizable.strings index 69961f21..251e1b0d 100644 --- a/en.lproj/Localizable.strings +++ b/en.lproj/Localizable.strings @@ -326,6 +326,7 @@ "tapback.exclamation"="Exclamation Mark"; "tapback.question"="Question Mark"; "tapback.poop"="Poop"; +"tapback.wave"="Wave"; "telemetry"="Telemetry (Sensors)"; "telemetry.config"="Telemetry Config"; "timeout"="Timeout"; diff --git a/fr.lproj/Localizable.strings b/fr.lproj/Localizable.strings index 4a5ea20d..31642beb 100644 --- a/fr.lproj/Localizable.strings +++ b/fr.lproj/Localizable.strings @@ -292,6 +292,7 @@ "tapback.exclamation"="Point d'exclamation"; "tapback.question"="Point d'interrogation"; "tapback.poop"="Caca"; +"tapback.wave"="Wave"; "telemetry"="Télémetrie (Capteurs)"; "telemetry.config"="Configuration de télémetrie"; "timeout"="Délai d'expiration"; diff --git a/he.lproj/Localizable.strings b/he.lproj/Localizable.strings index c4418a90..22e5a2df 100644 --- a/he.lproj/Localizable.strings +++ b/he.lproj/Localizable.strings @@ -316,6 +316,7 @@ "tapback.exclamation"="סימן קריאה"; "tapback.question"="סימן שאלה"; "tapback.poop"="חרא"; +"tapback.wave"="Wave"; "telemetry"="טלמטריה (חיישנים)"; "telemetry.config"="הגדרות טלמטריה"; "timeout"="זמן קצוב"; diff --git a/pl.lproj/Localizable.strings b/pl.lproj/Localizable.strings index b50eff01..2c5aa853 100644 --- a/pl.lproj/Localizable.strings +++ b/pl.lproj/Localizable.strings @@ -310,6 +310,7 @@ "tapback.exclamation"="Wykrzyknik"; "tapback.question"="Znak zapytania"; "tapback.poop"="Kupa"; +"tapback.wave"="Wave"; "telemetry"="Telemetria (czujniki)"; "telemetry.config"="Konfiguracja telemetrii"; "timeout"="Limit czasu"; diff --git a/protobufs b/protobufs index 556e49ba..bcfb49c4 160000 --- a/protobufs +++ b/protobufs @@ -1 +1 @@ -Subproject commit 556e49ba619e2f4d8fa3c2dee2a94129a43d5f08 +Subproject commit bcfb49c4988b1539fc35e568a58b9f2f5b60738a diff --git a/zh-Hans.lproj/Localizable.strings b/zh-Hans.lproj/Localizable.strings index 67c9ac65..6c8f6b0a 100644 --- a/zh-Hans.lproj/Localizable.strings +++ b/zh-Hans.lproj/Localizable.strings @@ -309,6 +309,7 @@ "tapback.exclamation"="感叹号"; "tapback.question"="问号"; "tapback.poop"="便便"; +"tapback.wave"="Wave"; "telemetry"="遥测(传感器)"; "telemetry.config"="遥测配置"; "timeout"="超时"; diff --git a/zh-Hant-TW.lproj/Localizable.strings b/zh-Hant-TW.lproj/Localizable.strings index 6f36c512..d0f8e876 100644 --- a/zh-Hant-TW.lproj/Localizable.strings +++ b/zh-Hant-TW.lproj/Localizable.strings @@ -309,6 +309,7 @@ "tapback.exclamation"="驚嘆號"; "tapback.question"="問號"; "tapback.poop"="便便"; +"tapback.wave"="Wave"; "telemetry"="遠測(傳感器)"; "telemetry.config"="遠側設定"; "timeout"="超時";