Assorted updates

This commit is contained in:
Garth Vander Houwen 2024-03-23 09:01:44 -07:00
parent 2b9a5e3f81
commit 02cca19f26
20 changed files with 318 additions and 166 deletions

View file

@ -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 = "<group>"; };
DDDC22312BA76701002C44F1 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/Localizable.strings; sourceTree = "<group>"; };
DDDC22322BA76961002C44F1 /* zh-Hant-TW */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hant-TW"; path = "zh-Hant-TW.lproj/Localizable.strings"; sourceTree = "<group>"; };
DDDC22372BA92344002C44F1 /* MeshMapContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeshMapContent.swift; sourceTree = "<group>"; };
DDDD527729B5B83F0045BC3C /* MeshtasticDataModelV9.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV9.xcdatamodel; sourceTree = "<group>"; };
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 = "<group>"; };
@ -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 = "<group>";
};
DDDC22362BA9232C002C44F1 /* MapContent */ = {
isa = PBXGroup;
children = (
DDDC22372BA92344002C44F1 /* MeshMapContent.swift */,
DD93800A2BA3F968008BEC06 /* NodeMapContent.swift */,
);
path = MapContent;
sourceTree = "<group>";
};
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 */,

View file

@ -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:

View file

@ -11,6 +11,17 @@ import MapKit
import SwiftUI
extension PositionEntity {
static func allPositionsFetchRequest() -> NSFetchRequest<PositionEntity> {
let request: NSFetchRequest<PositionEntity> = 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? {

View file

@ -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)

View file

@ -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 {

View file

@ -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}

View file

@ -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}

View file

@ -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

View file

@ -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
}
}
}

View file

@ -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)

View file

@ -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<PositionEntity>
@FetchRequest(fetchRequest: PositionEntity.allPositionsFetchRequest(), animation: .none)
var positions: FetchedResults<PositionEntity>
@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 : "?")
})

View file

@ -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";

View file

@ -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";

View file

@ -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";

View file

@ -316,6 +316,7 @@
"tapback.exclamation"="סימן קריאה";
"tapback.question"="סימן שאלה";
"tapback.poop"="חרא";
"tapback.wave"="Wave";
"telemetry"="טלמטריה (חיישנים)";
"telemetry.config"="הגדרות טלמטריה";
"timeout"="זמן קצוב";

View file

@ -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";

@ -1 +1 @@
Subproject commit 556e49ba619e2f4d8fa3c2dee2a94129a43d5f08
Subproject commit bcfb49c4988b1539fc35e568a58b9f2f5b60738a

View file

@ -309,6 +309,7 @@
"tapback.exclamation"="感叹号";
"tapback.question"="问号";
"tapback.poop"="便便";
"tapback.wave"="Wave";
"telemetry"="遥测(传感器)";
"telemetry.config"="遥测配置";
"timeout"="超时";

View file

@ -309,6 +309,7 @@
"tapback.exclamation"="驚嘆號";
"tapback.question"="問號";
"tapback.poop"="便便";
"tapback.wave"="Wave";
"telemetry"="遠測(傳感器)";
"telemetry.config"="遠側設定";
"timeout"="超時";