mirror of
https://github.com/meshtastic/Meshtastic-Apple.git
synced 2026-04-20 22:13:56 +00:00
Assorted updates
This commit is contained in:
parent
2b9a5e3f81
commit
02cca19f26
20 changed files with 318 additions and 166 deletions
|
|
@ -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 */,
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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? {
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 : "?")
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -316,6 +316,7 @@
|
|||
"tapback.exclamation"="סימן קריאה";
|
||||
"tapback.question"="סימן שאלה";
|
||||
"tapback.poop"="חרא";
|
||||
"tapback.wave"="Wave";
|
||||
"telemetry"="טלמטריה (חיישנים)";
|
||||
"telemetry.config"="הגדרות טלמטריה";
|
||||
"timeout"="זמן קצוב";
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -309,6 +309,7 @@
|
|||
"tapback.exclamation"="感叹号";
|
||||
"tapback.question"="问号";
|
||||
"tapback.poop"="便便";
|
||||
"tapback.wave"="Wave";
|
||||
"telemetry"="遥测(传感器)";
|
||||
"telemetry.config"="遥测配置";
|
||||
"timeout"="超时";
|
||||
|
|
|
|||
|
|
@ -309,6 +309,7 @@
|
|||
"tapback.exclamation"="驚嘆號";
|
||||
"tapback.question"="問號";
|
||||
"tapback.poop"="便便";
|
||||
"tapback.wave"="Wave";
|
||||
"telemetry"="遠測(傳感器)";
|
||||
"telemetry.config"="遠側設定";
|
||||
"timeout"="超時";
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue