mirror of
https://github.com/meshtastic/Meshtastic-Apple.git
synced 2026-04-20 22:13:56 +00:00
Map annotation popover and convex hull!
This commit is contained in:
parent
41fc4574ba
commit
3a5f192ac1
13 changed files with 344 additions and 89 deletions
|
|
@ -16,6 +16,7 @@
|
|||
DD007BB02AA5981000F5FA12 /* NodeInfoEntityExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD007BAF2AA5981000F5FA12 /* NodeInfoEntityExtension.swift */; };
|
||||
DD0D3D222A55CEB10066DB71 /* CocoaMQTT in Frameworks */ = {isa = PBXBuildFile; productRef = DD0D3D212A55CEB10066DB71 /* CocoaMQTT */; };
|
||||
DD0F791B28713C8A00A6FDAD /* AdminMessageList.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD0F791A28713C8A00A6FDAD /* AdminMessageList.swift */; };
|
||||
DD13AA492AB73BF400BA0C98 /* PositionPopover.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD13AA482AB73BF400BA0C98 /* PositionPopover.swift */; };
|
||||
DD1925B728CDA5A400720036 /* CannedMessagesConfigEnums.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1925B628CDA5A400720036 /* CannedMessagesConfigEnums.swift */; };
|
||||
DD1925B928CDA93900720036 /* SerialConfigEnums.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1925B828CDA93900720036 /* SerialConfigEnums.swift */; };
|
||||
DD1BF2F92776FE2E008C8D2F /* UserMessageList.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1BF2F82776FE2E008C8D2F /* UserMessageList.swift */; };
|
||||
|
|
@ -218,6 +219,7 @@
|
|||
DD007BAF2AA5981000F5FA12 /* NodeInfoEntityExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeInfoEntityExtension.swift; sourceTree = "<group>"; };
|
||||
DD0E9C222A30CE3A00580CBB /* MeshtasticDataModelV14.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV14.xcdatamodel; sourceTree = "<group>"; };
|
||||
DD0F791A28713C8A00A6FDAD /* AdminMessageList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdminMessageList.swift; sourceTree = "<group>"; };
|
||||
DD13AA482AB73BF400BA0C98 /* PositionPopover.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PositionPopover.swift; sourceTree = "<group>"; };
|
||||
DD14E72C2A80738F006E39BC /* MeshtasticDataModelV15.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV15.xcdatamodel; sourceTree = "<group>"; };
|
||||
DD1925B628CDA5A400720036 /* CannedMessagesConfigEnums.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CannedMessagesConfigEnums.swift; sourceTree = "<group>"; };
|
||||
DD1925B828CDA93900720036 /* SerialConfigEnums.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SerialConfigEnums.swift; sourceTree = "<group>"; };
|
||||
|
|
@ -819,6 +821,7 @@
|
|||
DDDB26412AABF655003AFCB7 /* NodeListItem.swift */,
|
||||
DDDB26472AACD6D1003AFCB7 /* NodeMapControl.swift */,
|
||||
DDB6CCFA2AAF805100945AF6 /* NodeMapSwiftUI.swift */,
|
||||
DD13AA482AB73BF400BA0C98 /* PositionPopover.swift */,
|
||||
);
|
||||
path = Helpers;
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -1106,6 +1109,7 @@
|
|||
DD4A911E2708C65400501B7E /* AppSettings.swift in Sources */,
|
||||
DD5E5209298EE33B00D21B61 /* module_config.pb.swift in Sources */,
|
||||
DD2160AF28C5552500C17253 /* MQTTConfig.swift in Sources */,
|
||||
DD13AA492AB73BF400BA0C98 /* PositionPopover.swift in Sources */,
|
||||
6DEDA55C2A9592F900321D2E /* MessageEntityExtension.swift in Sources */,
|
||||
DDDB444229F8A88700EE2349 /* Double.swift in Sources */,
|
||||
DD5E520F298EE33B00D21B61 /* cannedmessages.pb.swift in Sources */,
|
||||
|
|
@ -1410,7 +1414,7 @@
|
|||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 2.2.6;
|
||||
MARKETING_VERSION = 2.2.7;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SUPPORTS_MACCATALYST = YES;
|
||||
|
|
@ -1444,7 +1448,7 @@
|
|||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 2.2.6;
|
||||
MARKETING_VERSION = 2.2.7;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SUPPORTS_MACCATALYST = YES;
|
||||
|
|
@ -1566,7 +1570,7 @@
|
|||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 2.2.6;
|
||||
MARKETING_VERSION = 2.2.7;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
|
|
@ -1599,7 +1603,7 @@
|
|||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 2.2.6;
|
||||
MARKETING_VERSION = 2.2.7;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ extension UserDefaults {
|
|||
case meshMapRecentering
|
||||
case meshMapShowNodeHistory
|
||||
case meshMapShowRouteLines
|
||||
case enableMapConvexHull
|
||||
case enableMapTraffic
|
||||
case enableMapPointsOfInterest
|
||||
case enableOfflineMaps
|
||||
|
|
@ -98,6 +99,14 @@ extension UserDefaults {
|
|||
UserDefaults.standard.set(newValue, forKey: "meshMapShowRouteLines")
|
||||
}
|
||||
}
|
||||
static var enableMapConvexHull: Bool {
|
||||
get {
|
||||
UserDefaults.standard.bool(forKey: "enableMapConvexHull")
|
||||
}
|
||||
set {
|
||||
UserDefaults.standard.set(newValue, forKey: "enableMapConvexHull")
|
||||
}
|
||||
}
|
||||
static var enableMapTraffic: Bool {
|
||||
get {
|
||||
UserDefaults.standard.bool(forKey: "enableMapTraffic")
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
|
|||
// Scan for nearby BLE devices using the Meshtastic BLE service ID
|
||||
func startScanning() {
|
||||
if isSwitchedOn {
|
||||
centralManager.scanForPeripherals(withServices: [meshtasticServiceCBUUID], options: [CBCentralManagerScanOptionAllowDuplicatesKey: true])
|
||||
centralManager.scanForPeripherals(withServices: [meshtasticServiceCBUUID], options: [CBCentralManagerScanOptionAllowDuplicatesKey: false])
|
||||
print("✅ Scanning Started")
|
||||
}
|
||||
}
|
||||
|
|
@ -486,18 +486,14 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
|
|||
}
|
||||
// Config
|
||||
if decodedInfo.config.isInitialized && !invalidVersion && connectedPeripheral != nil {
|
||||
|
||||
nowKnown = true
|
||||
localConfig(config: decodedInfo.config, context: context!, nodeNum: self.connectedPeripheral.num, nodeLongName: self.connectedPeripheral.longName)
|
||||
}
|
||||
// Module Config
|
||||
if decodedInfo.moduleConfig.isInitialized && !invalidVersion {
|
||||
|
||||
nowKnown = true
|
||||
moduleConfig(config: decodedInfo.moduleConfig, context: context!, nodeNum: self.connectedPeripheral.num, nodeLongName: self.connectedPeripheral.longName)
|
||||
|
||||
if decodedInfo.moduleConfig.payloadVariant == ModuleConfig.OneOf_PayloadVariant.cannedMessage(decodedInfo.moduleConfig.cannedMessage) {
|
||||
|
||||
if decodedInfo.moduleConfig.cannedMessage.enabled {
|
||||
_ = self.getCannedMessageModuleMessages(destNum: self.connectedPeripheral.num, wantResponse: true)
|
||||
}
|
||||
|
|
@ -508,9 +504,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
|
|||
nowKnown = true
|
||||
deviceMetadataPacket(metadata: decodedInfo.metadata, fromNum: connectedPeripheral.num, context: context!)
|
||||
connectedPeripheral.firmwareVersion = decodedInfo.metadata.firmwareVersion
|
||||
|
||||
let lastDotIndex = decodedInfo.metadata.firmwareVersion.lastIndex(of: ".")
|
||||
|
||||
if lastDotIndex == nil {
|
||||
invalidVersion = true
|
||||
connectedVersion = "0.0.0"
|
||||
|
|
@ -520,19 +514,15 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
|
|||
connectedVersion = String(version.dropLast())
|
||||
appState.firmwareVersion = connectedVersion
|
||||
}
|
||||
|
||||
let supportedVersion = connectedVersion == "0.0.0" || self.minimumVersion.compare(connectedVersion, options: .numeric) == .orderedAscending || minimumVersion.compare(connectedVersion, options: .numeric) == .orderedSame
|
||||
|
||||
if !supportedVersion {
|
||||
invalidVersion = true
|
||||
lastConnectionError = "🚨" + "update.firmware".localized
|
||||
return
|
||||
|
||||
}
|
||||
}
|
||||
// Log any other unknownApp calls
|
||||
if !nowKnown { MeshLogger.log("🕸️ MESH PACKET received for Unknown App UNHANDLED \((try? decodedInfo.packet.jsonString()) ?? "JSON Decode Failure")") }
|
||||
|
||||
case .textMessageApp, .detectionSensorApp:
|
||||
textMessageAppPacket(packet: decodedInfo.packet, blockRangeTest: UserDefaults.blockRangeTest, connectedNode: (self.connectedPeripheral != nil ? connectedPeripheral.num : 0), context: context!)
|
||||
case .remoteHardwareApp:
|
||||
|
|
|
|||
|
|
@ -22,6 +22,6 @@ struct BluetoothConnectionTip: Tip {
|
|||
Text("tip.bluetooth.connect.message")
|
||||
}
|
||||
var image: Image? {
|
||||
Image(systemName: "questionmark.circle")
|
||||
Image(systemName: "flipphone")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,6 +22,6 @@
|
|||
Text("tip.channels.share.message")
|
||||
}
|
||||
var image: Image? {
|
||||
Image(systemName: "questionmark.circle")
|
||||
Image(systemName: "qrcode")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,6 +22,25 @@ struct MessagesTip: Tip {
|
|||
Text("tip.messages.message")
|
||||
}
|
||||
var image: Image? {
|
||||
Image(systemName: "questionmark.circle")
|
||||
Image(systemName: "bubble.left.and.bubble.right")
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 17.0, macOS 14.0, *)
|
||||
struct ContactsTip: Tip {
|
||||
|
||||
var id: String {
|
||||
return "tip.messages.contacts"
|
||||
}
|
||||
var title: Text {
|
||||
//Text("tip.messages.contacts.title")
|
||||
Text("Contacts")
|
||||
}
|
||||
var message: Text? {
|
||||
//Text("tip.messages.contacts.message")
|
||||
Text("Each node shows as an available contact. Nodes with recent messages and favorites show up at the top of the list. Select a node to send or view messages. Long press to favorite or mute the node, send a trace route or delete the conversation.")
|
||||
}
|
||||
var image: Image? {
|
||||
Image(systemName: "person.circle")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,9 @@
|
|||
|
||||
import SwiftUI
|
||||
import CoreData
|
||||
#if canImport(TipKit)
|
||||
import TipKit
|
||||
#endif
|
||||
|
||||
struct UserList: View {
|
||||
|
||||
|
|
@ -37,6 +40,9 @@ struct UserList: View {
|
|||
let dateFormatString = (localeDateFormat ?? "MM/dd/YY")
|
||||
VStack {
|
||||
List {
|
||||
if #available(iOS 17.0, macOS 14.0, *) {
|
||||
TipView(ContactsTip(), arrowEdge: .bottom)
|
||||
}
|
||||
ForEach(users) { (user: UserEntity) in
|
||||
let mostRecent = user.messageList.last
|
||||
let lastMessageTime = Date(timeIntervalSince1970: TimeInterval(Int64((mostRecent?.messageTimestamp ?? 0 ))))
|
||||
|
|
@ -96,47 +102,47 @@ struct UserList: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
.frame(height: 62)
|
||||
.contextMenu {
|
||||
Button {
|
||||
user.vip = !user.vip
|
||||
do {
|
||||
try context.save()
|
||||
} catch {
|
||||
context.rollback()
|
||||
print("💥 Save User VIP Error")
|
||||
}
|
||||
} label: {
|
||||
Label(user.vip ? "Un-Favorite" : "Favorite", systemImage: user.vip ? "star.slash.fill" : "star.fill")
|
||||
.frame(height: 62)
|
||||
.contextMenu {
|
||||
Button {
|
||||
user.vip = !user.vip
|
||||
do {
|
||||
try context.save()
|
||||
} catch {
|
||||
context.rollback()
|
||||
print("💥 Save User VIP Error")
|
||||
}
|
||||
Button {
|
||||
user.mute = !user.mute
|
||||
do {
|
||||
try context.save()
|
||||
} catch {
|
||||
context.rollback()
|
||||
print("💥 Save User Mute Error")
|
||||
}
|
||||
} label: {
|
||||
Label(user.mute ? "Show Alerts" : "Hide Alerts", systemImage: user.mute ? "bell" : "bell.slash")
|
||||
} label: {
|
||||
Label(user.vip ? "Un-Favorite" : "Favorite", systemImage: user.vip ? "star.slash.fill" : "star.fill")
|
||||
}
|
||||
Button {
|
||||
user.mute = !user.mute
|
||||
do {
|
||||
try context.save()
|
||||
} catch {
|
||||
context.rollback()
|
||||
print("💥 Save User Mute Error")
|
||||
}
|
||||
Button {
|
||||
let success = bleManager.sendTraceRouteRequest(destNum: user.num, wantResponse: true)
|
||||
if success {
|
||||
isPresentingTraceRouteSentAlert = true
|
||||
}
|
||||
} label: {
|
||||
Label("Trace Route", systemImage: "signpost.right.and.left")
|
||||
} label: {
|
||||
Label(user.mute ? "Show Alerts" : "Hide Alerts", systemImage: user.mute ? "bell" : "bell.slash")
|
||||
}
|
||||
Button {
|
||||
let success = bleManager.sendTraceRouteRequest(destNum: user.num, wantResponse: true)
|
||||
if success {
|
||||
isPresentingTraceRouteSentAlert = true
|
||||
}
|
||||
if user.messageList.count > 0 {
|
||||
Button(role: .destructive) {
|
||||
isPresentingDeleteUserMessagesConfirm = true
|
||||
userSelection = user
|
||||
} label: {
|
||||
Label("Delete Messages", systemImage: "trash")
|
||||
}
|
||||
} label: {
|
||||
Label("Trace Route", systemImage: "signpost.right.and.left")
|
||||
}
|
||||
if user.messageList.count > 0 {
|
||||
Button(role: .destructive) {
|
||||
isPresentingDeleteUserMessagesConfirm = true
|
||||
userSelection = user
|
||||
} label: {
|
||||
Label("Delete Messages", systemImage: "trash")
|
||||
}
|
||||
}
|
||||
}
|
||||
.alert(
|
||||
"Trace Route Sent",
|
||||
isPresented: $isPresentingTraceRouteSentAlert
|
||||
|
|
|
|||
|
|
@ -30,9 +30,16 @@ struct NodeListItem: View {
|
|||
}
|
||||
}
|
||||
VStack(alignment: .leading) {
|
||||
Text(node.user?.longName ?? "unknown".localized)
|
||||
.fontWeight(.medium)
|
||||
.font(.callout)
|
||||
HStack {
|
||||
Text(node.user?.longName ?? "unknown".localized)
|
||||
.fontWeight(.medium)
|
||||
.font(.callout)
|
||||
if node.user?.vip ?? false {
|
||||
Spacer()
|
||||
Image(systemName: "star.fill")
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
if connected {
|
||||
HStack {
|
||||
Image(systemName: "antenna.radiowaves.left.and.right.circle.fill")
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import MapKit
|
|||
import WeatherKit
|
||||
|
||||
@available(iOS 17.0, macOS 14.0, *)
|
||||
|
||||
struct NodeMapSwiftUI: View {
|
||||
@Environment(\.managedObjectContext) var context
|
||||
@EnvironmentObject var bleManager: BLEManager
|
||||
|
|
@ -27,6 +28,8 @@ struct NodeMapSwiftUI: View {
|
|||
@State private var isLookingAround = false
|
||||
@State private var isEditingSettings = false
|
||||
@State private var showUserLocation: Bool = false
|
||||
|
||||
@State private var showConvexHull = true
|
||||
@State var selected: PositionEntity?
|
||||
/// Data
|
||||
@ObservedObject var node: NodeInfoEntity
|
||||
|
|
@ -36,6 +39,8 @@ struct NodeMapSwiftUI: View {
|
|||
), animation: .none)
|
||||
private var waypoints: FetchedResults<WaypointEntity>
|
||||
|
||||
@State private var showingPopover = false
|
||||
|
||||
var body: some View {
|
||||
let nodeColor = UIColor(hex: UInt32(node.num))
|
||||
let positionArray = node.positions?.array as? [PositionEntity] ?? []
|
||||
|
|
@ -48,17 +53,26 @@ struct NodeMapSwiftUI: View {
|
|||
ZStack {
|
||||
Map(position: $position, bounds: MapCameraBounds(minimumDistance: 1, maximumDistance: .infinity), scope: mapScope) {
|
||||
/// Route Lines
|
||||
if showRouteLines {
|
||||
let gradient = LinearGradient(
|
||||
colors: [Color(nodeColor.lighter().lighter().lighter()), Color(nodeColor.lighter()), Color(nodeColor)],
|
||||
startPoint: .leading, endPoint: .trailing
|
||||
)
|
||||
let stroke = StrokeStyle(
|
||||
lineWidth: 5,
|
||||
lineCap: .round, lineJoin: .round, dash: [10, 10]
|
||||
)
|
||||
MapPolyline(coordinates: lineCoords)
|
||||
.stroke(gradient, style: stroke)
|
||||
if showRouteLines {
|
||||
if showRouteLines {
|
||||
let gradient = LinearGradient(
|
||||
colors: [Color(nodeColor.lighter().lighter().lighter()), Color(nodeColor.lighter()), Color(nodeColor)],
|
||||
startPoint: .leading, endPoint: .trailing
|
||||
)
|
||||
let dashed = StrokeStyle(
|
||||
lineWidth: 5,
|
||||
lineCap: .round, lineJoin: .round, dash: [10, 10]
|
||||
)
|
||||
MapPolyline(coordinates: lineCoords)
|
||||
.stroke(gradient, style: dashed)
|
||||
}
|
||||
}
|
||||
/// Convex Hull
|
||||
if showConvexHull {
|
||||
let hull = getConvexHull(input: lineCoords)
|
||||
MapPolygon(coordinates: hull)
|
||||
.stroke(Color(nodeColor.darker()), lineWidth: 5)
|
||||
.foregroundStyle(Color(nodeColor).opacity(0.4))
|
||||
}
|
||||
/// Node Annotations
|
||||
ForEach(positionArray.reversed(), id: \.id) { position in
|
||||
|
|
@ -79,10 +93,16 @@ struct NodeMapSwiftUI: View {
|
|||
.background(Color(UIColor(hex: UInt32(node.num)).darker()))
|
||||
.clipShape(Circle())
|
||||
.rotationEffect(.degrees(Double(position.heading)))
|
||||
// .onTapGesture {
|
||||
// selected = (selected == position ? nil : position) // <-- here
|
||||
// print("tapity tap tap \(position.time)")
|
||||
// }
|
||||
.onTapGesture {
|
||||
showingPopover = true
|
||||
selected = (selected == position ? nil : position) // <-- here
|
||||
}
|
||||
.popover(isPresented: $showingPopover, arrowEdge: .bottom) {
|
||||
PositionPopover(position: position)
|
||||
.padding()
|
||||
.opacity(0.8)
|
||||
.presentationCompactAdaptation(.popover)
|
||||
}
|
||||
} else {
|
||||
Image(systemName: "flipphone")
|
||||
.symbolEffect(.pulse.byLayer)
|
||||
|
|
@ -90,10 +110,16 @@ struct NodeMapSwiftUI: View {
|
|||
.foregroundStyle(Color(nodeColor).isLight() ? .black : .white)
|
||||
.background(Color(UIColor(hex: UInt32(node.num)).darker()))
|
||||
.clipShape(Circle())
|
||||
// .onTapGesture {
|
||||
// selected = (selected == position ? nil : position) // <-- here
|
||||
// print("tapity tap tap \(position.time)")
|
||||
// }
|
||||
.onTapGesture {
|
||||
showingPopover = true
|
||||
selected = (selected == position ? nil : position) // <-- here
|
||||
}
|
||||
.popover(isPresented: $showingPopover, arrowEdge: .bottom) {
|
||||
PositionPopover(position: position)
|
||||
.padding()
|
||||
.opacity(0.8)
|
||||
.presentationCompactAdaptation(.popover)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if showNodeHistory {
|
||||
|
|
@ -186,6 +212,14 @@ struct NodeMapSwiftUI: View {
|
|||
self.showRouteLines.toggle()
|
||||
UserDefaults.enableMapRouteLines = self.showRouteLines
|
||||
}
|
||||
Toggle(isOn: $showConvexHull) {
|
||||
Label("Convex Hull", systemImage: "button.angledbottom.horizontal.right")
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
.onTapGesture {
|
||||
self.showConvexHull.toggle()
|
||||
UserDefaults.enableMapConvexHull = self.showConvexHull
|
||||
}
|
||||
Toggle(isOn: $showTraffic) {
|
||||
Label("Traffic", systemImage: "car")
|
||||
}
|
||||
|
|
@ -216,13 +250,13 @@ struct NodeMapSwiftUI: View {
|
|||
.padding()
|
||||
#endif
|
||||
}
|
||||
//.presentationDetents([.fraction(0.4)])
|
||||
//.presentationDetents([.fraction(0.5)])
|
||||
.presentationDetents([.medium])
|
||||
.presentationDragIndicator(.visible)
|
||||
}
|
||||
.onChange(of: node) {
|
||||
let mostRecent = node.positions?.lastObject as? PositionEntity
|
||||
position = .camera(MapCamera(centerCoordinate: mostRecent!.coordinate, distance: 1500, heading: 0, pitch: 0))
|
||||
position = MapCameraPosition.automatic//.camera(MapCamera(centerCoordinate: mostRecent!.coordinate, distance: 1500, heading: 0, pitch: 0))
|
||||
if let mostRecent {
|
||||
Task {
|
||||
scene = try? await fetchScene(for: mostRecent.coordinate)
|
||||
|
|
@ -306,4 +340,49 @@ struct NodeMapSwiftUI: View {
|
|||
let lookAroundScene = MKLookAroundSceneRequest(coordinate: coordinate)
|
||||
return try await lookAroundScene.scene
|
||||
}
|
||||
|
||||
func getConvexHull(input: [CLLocationCoordinate2D]) -> [CLLocationCoordinate2D] {
|
||||
|
||||
// X = longitude
|
||||
// Y = latitudeß
|
||||
|
||||
// 2D cross product of OA and OB vectors, i.e. z-component of their 3D cross product.
|
||||
// Returns a positive value, if OAB makes a counter-clockwise turn,
|
||||
// negative for clockwise turn, and zero if the points are collinear.
|
||||
func cross(P: CLLocationCoordinate2D, A: CLLocationCoordinate2D, B: CLLocationCoordinate2D) -> Double {
|
||||
let part1 = (A.longitude - P.longitude) * (B.latitude - P.latitude)
|
||||
let part2 = (A.latitude - P.latitude) * (B.longitude - P.longitude)
|
||||
return part1 - part2;
|
||||
}
|
||||
|
||||
// Sort points lexicographically
|
||||
let points = input.sorted() {
|
||||
$0.longitude == $1.longitude ? $0.latitude < $1.latitude : $0.longitude < $1.longitude
|
||||
}
|
||||
|
||||
// Build the lower hull
|
||||
var lower: [CLLocationCoordinate2D] = []
|
||||
for p in points {
|
||||
while lower.count >= 2 && cross(P: lower[lower.count - 2], A: lower[lower.count - 1], B: p) <= 0 {
|
||||
lower.removeLast()
|
||||
}
|
||||
lower.append(p)
|
||||
}
|
||||
|
||||
// Build upper hull
|
||||
var upper: [CLLocationCoordinate2D] = []
|
||||
for p in points.reversed() {
|
||||
while upper.count >= 2 && cross(P: upper[upper.count-2], A: upper[upper.count-1], B: p) <= 0 {
|
||||
upper.removeLast()
|
||||
}
|
||||
upper.append(p)
|
||||
}
|
||||
|
||||
// Last point of upper list is omitted because it is repeated at the
|
||||
// beginning of the lower list.
|
||||
upper.removeLast()
|
||||
|
||||
// Concatenation of the lower and upper hulls gives the convex hull.
|
||||
return (upper + lower)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,116 @@
|
|||
// PositionPopover.swift
|
||||
// Meshtastic
|
||||
//
|
||||
// Created by Garth Vander Houwen on 9/17/23.
|
||||
// Copyright(c) Garth Vander Houwen 9/17/23.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
import MapKit
|
||||
|
||||
struct PositionPopover: View {
|
||||
var position: PositionEntity
|
||||
let distanceFormatter = MKDistanceFormatter()
|
||||
var body: some View {
|
||||
VStack {
|
||||
HStack {
|
||||
CircleText(text: position.nodePosition?.user?.shortName ?? "?", color: Color(UIColor(hex: UInt32(position.nodePosition?.user?.num ?? 0))))
|
||||
Text(position.nodePosition?.user?.longName ?? "Unknown")
|
||||
.font(.title3)
|
||||
}
|
||||
Divider()
|
||||
VStack (alignment: .leading) {
|
||||
/// Time
|
||||
Label {
|
||||
Text(position.time?.formatted() ?? "Unknown")
|
||||
.foregroundColor(.primary)
|
||||
} icon: {
|
||||
Image(systemName: "clock.badge.checkmark")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
.frame(width: 35)
|
||||
}
|
||||
.padding(.bottom, 5)
|
||||
/// Coordinate
|
||||
Label {
|
||||
Text("\(String(format: "%.6f", position.coordinate.latitude)), \(String(format: "%.6f", position.coordinate.longitude))")
|
||||
.font(.footnote)
|
||||
.textSelection(.enabled)
|
||||
.foregroundColor(.primary)
|
||||
} icon: {
|
||||
Image(systemName: "mappin.and.ellipse")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
.frame(width: 35)
|
||||
}
|
||||
.padding(.bottom, 5)
|
||||
/// Altitude
|
||||
Label {
|
||||
Text("Altitude: \(distanceFormatter.string(fromDistance: Double(position.altitude)))")
|
||||
// .font(.footnote)
|
||||
.foregroundColor(.primary)
|
||||
} icon: {
|
||||
Image(systemName: "mountain.2.fill")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
.frame(width: 35)
|
||||
}
|
||||
.padding(.bottom, 5)
|
||||
let pf = PositionFlags(rawValue: Int(position.nodePosition?.metadata?.positionFlags ?? 3))
|
||||
/// Sats in view
|
||||
if pf.contains(.Satsinview) {
|
||||
Label {
|
||||
Text("Sats in view: \(String(position.satsInView))")
|
||||
// .font(.footnote)
|
||||
.foregroundColor(.primary)
|
||||
} icon: {
|
||||
Image(systemName: "sparkles")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
.frame(width: 35)
|
||||
}
|
||||
.padding(.bottom, 5)
|
||||
}
|
||||
/// Sequence Number
|
||||
if pf.contains(.SeqNo) {
|
||||
Label {
|
||||
Text("Sequence: \(String(position.seqNo))")
|
||||
// .font(.footnote)
|
||||
.foregroundColor(.primary)
|
||||
} icon: {
|
||||
Image(systemName: "number")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
.frame(width: 35)
|
||||
}
|
||||
.padding(.bottom, 5)
|
||||
}
|
||||
/// Heading
|
||||
// if pf.contains(.Heading) {
|
||||
// Text("Heading: \(Int32(position.heading))")
|
||||
// }
|
||||
/// Speed
|
||||
if pf.contains(.Speed) {
|
||||
let formatter = MeasurementFormatter()
|
||||
Label {
|
||||
Text("Speed: \(formatter.string(from: Measurement(value: Double(position.speed), unit: UnitSpeed.kilometersPerHour)))")
|
||||
// .font(.footnote)
|
||||
.foregroundColor(.primary)
|
||||
} icon: {
|
||||
Image(systemName: "gauge.with.dots.needle.33percent")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
.frame(width: 35)
|
||||
}
|
||||
.padding(.bottom, 5)
|
||||
}
|
||||
/// Distance
|
||||
if LocationHelper.currentLocation.distance(from: LocationHelper.DefaultLocation) > 0.0 {
|
||||
let metersAway = position.coordinate.distance(from: LocationHelper.currentLocation)
|
||||
Label {
|
||||
Text("distance".localized + ": \(distanceFormatter.string(fromDistance: Double(metersAway)))")
|
||||
// .font(.footnote)
|
||||
.foregroundColor(.primary)
|
||||
} icon: {
|
||||
Image(systemName: "lines.measurement.horizontal")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
.frame(width: 35)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ struct NodeList: View {
|
|||
sortDescriptors: [NSSortDescriptor(key: "user.vip", ascending: false), NSSortDescriptor(key: "lastHeard", ascending: false)],
|
||||
animation: .default)
|
||||
|
||||
private var nodes: FetchedResults<NodeInfoEntity>
|
||||
var nodes: FetchedResults<NodeInfoEntity>
|
||||
|
||||
|
||||
|
||||
|
|
@ -42,7 +42,39 @@ struct NodeList: View {
|
|||
let connectedNode = nodes.first(where: { $0.num == connectedNodeNum })
|
||||
List(nodes, id: \.self, selection: $selectedNode) { node in
|
||||
|
||||
NodeListItem(node: node, connected: bleManager.connectedPeripheral != nil && bleManager.connectedPeripheral?.num ?? -1 == node.num, connectedNode: (bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral?.num ?? -1 : -1), modemPreset: Int(connectedNode?.loRaConfig?.modemPreset ?? 0))
|
||||
NodeListItem(node: node,
|
||||
connected: bleManager.connectedPeripheral != nil && bleManager.connectedPeripheral?.num ?? -1 == node.num,
|
||||
connectedNode: (bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral?.num ?? -1 : -1),
|
||||
modemPreset: Int(connectedNode?.loRaConfig?.modemPreset ?? 0))
|
||||
.contextMenu {
|
||||
if node.user != nil {
|
||||
Button {
|
||||
node.user!.vip = !node.user!.vip
|
||||
context.refresh(node, mergeChanges: true)
|
||||
do {
|
||||
try context.save()
|
||||
} catch {
|
||||
context.rollback()
|
||||
print("💥 Save User VIP Error")
|
||||
}
|
||||
} label: {
|
||||
Label(node.user?.vip ?? false ? "Un-Favorite" : "Favorite", systemImage: node.user?.vip ?? false ? "star.slash.fill" : "star.fill")
|
||||
}
|
||||
Button {
|
||||
node.user!.mute = !node.user!.mute
|
||||
context.refresh(node, mergeChanges: true)
|
||||
do {
|
||||
try context.save()
|
||||
} catch {
|
||||
context.rollback()
|
||||
print("💥 Save User Mute Error")
|
||||
}
|
||||
} label: {
|
||||
Label(node.user!.mute ? "Show Alerts" : "Hide Alerts", systemImage: node.user!.mute ? "bell" : "bell.slash")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
.searchable(text: nodesQuery, prompt: "Find a node")
|
||||
.navigationTitle(String.localizedStringWithFormat("nodes %@".localized, String(nodes.count)))
|
||||
|
|
|
|||
|
|
@ -51,16 +51,16 @@ struct ShareChannels: View {
|
|||
var qrCodeImage = QrCodeImage()
|
||||
|
||||
var body: some View {
|
||||
|
||||
if #available(iOS 17.0, macOS 14.0, *) {
|
||||
VStack {
|
||||
TipView(ShareChannelsTip(), arrowEdge: .bottom)
|
||||
}
|
||||
}
|
||||
GeometryReader { bounds in
|
||||
let smallest = min(bounds.size.width, bounds.size.height)
|
||||
ScrollView {
|
||||
if node != nil && node?.myInfo != nil {
|
||||
|
||||
if #available(iOS 17.0, macOS 14.0, *) {
|
||||
VStack {
|
||||
TipView(ShareChannelsTip(), arrowEdge: .top)
|
||||
}
|
||||
}
|
||||
Grid {
|
||||
GridRow {
|
||||
Spacer()
|
||||
|
|
|
|||
|
|
@ -276,7 +276,7 @@
|
|||
"telemetry.config"="Telemetry Config";
|
||||
"timeout"="Timeout";
|
||||
"timestamp"="Timestamp";
|
||||
"tip.bluetooth.connect.title"="Connected LoRa Radio";
|
||||
"tip.bluetooth.connect.title"="Connected Radio";
|
||||
"tip.bluetooth.connect.message"="Shows information for the Lora radio currently connected via bluetooth. You can swipe left to disconnect the radio and long press to view stats or start the live activity.";
|
||||
"tip.channels.share.title"="Sharing Meshtastic Channels";
|
||||
"tip.channels.share.message"="In a Meshtastic LoRa Mesh there are up to 8 channels. The first one is the Primary channel where most activity happens and is required. If you don't share your primary channel your first shared channel becomes the primary channel on the other network. It talks on its primary and your secondary channel. A channel with the name 'admin' controls nodes remotely. Other channels are for private groups, each with its own key.";
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue