mirror of
https://github.com/meshtastic/Meshtastic-Apple.git
synced 2026-04-20 22:13:56 +00:00
Altitude graph for the node map
This commit is contained in:
parent
621bb0a13e
commit
a89f33b9f5
5 changed files with 179 additions and 16 deletions
|
|
@ -19,6 +19,8 @@
|
|||
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 */; };
|
||||
DD1933762B0835D500771CD5 /* PositionAltitudeChart.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1933752B0835D500771CD5 /* PositionAltitudeChart.swift */; };
|
||||
DD1933782B084F4200771CD5 /* Measurement.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1933772B084F4200771CD5 /* Measurement.swift */; };
|
||||
DD1BF2F92776FE2E008C8D2F /* UserMessageList.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1BF2F82776FE2E008C8D2F /* UserMessageList.swift */; };
|
||||
DD2160AF28C5552500C17253 /* MQTTConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD2160AE28C5552500C17253 /* MQTTConfig.swift */; };
|
||||
DD23A50F26FD1B4400D9B90C /* PeripheralModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD23A50E26FD1B4400D9B90C /* PeripheralModel.swift */; };
|
||||
|
|
@ -224,6 +226,8 @@
|
|||
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>"; };
|
||||
DD1933752B0835D500771CD5 /* PositionAltitudeChart.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PositionAltitudeChart.swift; sourceTree = "<group>"; };
|
||||
DD1933772B084F4200771CD5 /* Measurement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Measurement.swift; sourceTree = "<group>"; };
|
||||
DD1BF2F82776FE2E008C8D2F /* UserMessageList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserMessageList.swift; sourceTree = "<group>"; };
|
||||
DD2160AE28C5552500C17253 /* MQTTConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MQTTConfig.swift; sourceTree = "<group>"; };
|
||||
DD23A50E26FD1B4400D9B90C /* PeripheralModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PeripheralModel.swift; sourceTree = "<group>"; };
|
||||
|
|
@ -636,6 +640,7 @@
|
|||
DDB6CCFA2AAF805100945AF6 /* NodeMapSwiftUI.swift */,
|
||||
DD13AA482AB73BF400BA0C98 /* PositionPopover.swift */,
|
||||
DD46401F2AFF10F4002A5ECB /* WaypointForm.swift */,
|
||||
DD1933752B0835D500771CD5 /* PositionAltitudeChart.swift */,
|
||||
);
|
||||
path = Map;
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -856,6 +861,7 @@
|
|||
DDDB443F29F79AB000EE2349 /* UserDefaults.swift */,
|
||||
DDB75A0E2A05920E006ED576 /* FileManager.swift */,
|
||||
DDB75A102A059258006ED576 /* Url.swift */,
|
||||
DD1933772B084F4200771CD5 /* Measurement.swift */,
|
||||
);
|
||||
path = Extensions;
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -1093,6 +1099,7 @@
|
|||
DDDB444C29F8AAA600EE2349 /* Color.swift in Sources */,
|
||||
DDB8F4122A9EE5DD00230ECE /* UserList.swift in Sources */,
|
||||
DDB75A0F2A05920E006ED576 /* FileManager.swift in Sources */,
|
||||
DD1933782B084F4200771CD5 /* Measurement.swift in Sources */,
|
||||
DD4F23CD28779A3C001D37CB /* EnvironmentMetricsLog.swift in Sources */,
|
||||
DD41A61529AB0035003C5A37 /* NodeWeatherForecast.swift in Sources */,
|
||||
DDB6ABD628AE742000384BA1 /* BluetoothConfig.swift in Sources */,
|
||||
|
|
@ -1192,6 +1199,7 @@
|
|||
DDB6ABE228B13FB500384BA1 /* PositionConfigEnums.swift in Sources */,
|
||||
DD5E520E298EE33B00D21B61 /* mqtt.pb.swift in Sources */,
|
||||
DD994B69295F88B60013760A /* IntervalEnums.swift in Sources */,
|
||||
DD1933762B0835D500771CD5 /* PositionAltitudeChart.swift in Sources */,
|
||||
DD415828285859C4009B0E59 /* TelemetryConfig.swift in Sources */,
|
||||
DDDB443D29F6592F00EE2349 /* NetworkManager.swift in Sources */,
|
||||
DDB6CCFB2AAF805100945AF6 /* NodeMapSwiftUI.swift in Sources */,
|
||||
|
|
@ -1427,7 +1435,7 @@
|
|||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 2.2.11;
|
||||
MARKETING_VERSION = 2.2.12;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SUPPORTS_MACCATALYST = YES;
|
||||
|
|
@ -1461,7 +1469,7 @@
|
|||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 2.2.11;
|
||||
MARKETING_VERSION = 2.2.12;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SUPPORTS_MACCATALYST = YES;
|
||||
|
|
@ -1583,7 +1591,7 @@
|
|||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 2.2.11;
|
||||
MARKETING_VERSION = 2.2.12;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
|
|
@ -1616,7 +1624,7 @@
|
|||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 2.2.11;
|
||||
MARKETING_VERSION = 2.2.12;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
|
|
|
|||
28
Meshtastic/Extensions/Measurement.swift
Normal file
28
Meshtastic/Extensions/Measurement.swift
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
//
|
||||
// Measurement.swift
|
||||
// Meshtastic
|
||||
//
|
||||
// Copyright(c) Garth Vander Houwen 11/17/23.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Charts
|
||||
|
||||
struct PlottableMeasurement<UnitType: Unit> {
|
||||
var measurement: Measurement<UnitType>
|
||||
}
|
||||
|
||||
extension PlottableMeasurement: Plottable where UnitType == UnitLength {
|
||||
var primitivePlottable: Double {
|
||||
self.measurement.converted(to: .meters).value
|
||||
}
|
||||
|
||||
init?(primitivePlottable: Double) {
|
||||
self.init(
|
||||
measurement: Measurement(
|
||||
value: primitivePlottable,
|
||||
unit: .meters
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -33,6 +33,7 @@ struct NodeMapSwiftUI: View {
|
|||
@State var position = MapCameraPosition.automatic
|
||||
@State var scene: MKLookAroundScene?
|
||||
@State var isLookingAround = false
|
||||
@State var isShowingAltitude = false
|
||||
@State var isEditingSettings = false
|
||||
@State var selectedPosition: PositionEntity?
|
||||
@State var showWaypoints = false
|
||||
|
|
@ -75,10 +76,12 @@ struct NodeMapSwiftUI: View {
|
|||
}
|
||||
/// Convex Hull
|
||||
if showConvexHull {
|
||||
let hull = lineCoords.getConvexHull()
|
||||
MapPolygon(coordinates: hull)
|
||||
.stroke(Color(nodeColor.darker()), lineWidth: 3)
|
||||
.foregroundStyle(Color(nodeColor).opacity(0.4))
|
||||
if lineCoords.count > 0 {
|
||||
let hull = lineCoords.getConvexHull()
|
||||
MapPolygon(coordinates: hull)
|
||||
.stroke(Color(nodeColor.darker()), lineWidth: 3)
|
||||
.foregroundStyle(Color(nodeColor).opacity(0.4))
|
||||
}
|
||||
}
|
||||
|
||||
/// Waypoint Annotations
|
||||
|
|
@ -88,8 +91,6 @@ struct NodeMapSwiftUI: View {
|
|||
ZStack {
|
||||
CircleText(text: String(UnicodeScalar(Int(waypoint.icon)) ?? "📍"), color: Color.orange, circleSize: 35)
|
||||
.onTapGesture(coordinateSpace: .named("nodemap")) { location in
|
||||
print("Tapped at \(location)")
|
||||
let pinLocation = reader.convert(location, from: .local)
|
||||
selectedWaypoint = (selectedWaypoint == waypoint ? nil : waypoint)
|
||||
}
|
||||
}
|
||||
|
|
@ -193,6 +194,14 @@ struct NodeMapSwiftUI: View {
|
|||
.padding(.horizontal, 20)
|
||||
}
|
||||
}
|
||||
.overlay(alignment: .bottom) {
|
||||
if !isLookingAround && isShowingAltitude {
|
||||
PositionAltitudeChart(node: node)
|
||||
.frame(height: UIDevice.current.userInterfaceIdiom == .phone ? 250 : 400)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 12))
|
||||
.padding(.horizontal, 20)
|
||||
}
|
||||
}
|
||||
.sheet(item: $selectedWaypoint) { selection in
|
||||
WaypointForm(waypoint: selection)
|
||||
.padding()
|
||||
|
|
@ -273,9 +282,10 @@ struct NodeMapSwiftUI: View {
|
|||
/// Look Around Button
|
||||
if self.scene != nil {
|
||||
Button(action: {
|
||||
withAnimation {
|
||||
isLookingAround = !isLookingAround
|
||||
if isShowingAltitude {
|
||||
isShowingAltitude = false
|
||||
}
|
||||
isLookingAround = !isLookingAround
|
||||
}) {
|
||||
Image(systemName: isLookingAround ? "binoculars.fill" : "binoculars")
|
||||
.padding(.vertical, 5)
|
||||
|
|
@ -284,6 +294,21 @@ struct NodeMapSwiftUI: View {
|
|||
.foregroundColor(.accentColor)
|
||||
.buttonStyle(.borderedProminent)
|
||||
}
|
||||
/// Altitude Button
|
||||
if node.positions?.count ?? 0 > 1 {
|
||||
Button(action: {
|
||||
if isLookingAround {
|
||||
isLookingAround = false
|
||||
}
|
||||
isShowingAltitude = !isShowingAltitude
|
||||
}) {
|
||||
Image(systemName: isShowingAltitude ? "mountain.2.fill" : "mountain.2")
|
||||
.padding(.vertical, 5)
|
||||
}
|
||||
.tint(Color(UIColor.secondarySystemBackground))
|
||||
.foregroundColor(.accentColor)
|
||||
.buttonStyle(.borderedProminent)
|
||||
}
|
||||
#if targetEnvironment(macCatalyst)
|
||||
/// Hide non fuctional catalyst controls
|
||||
// MapZoomStepper(scope: mapScope)
|
||||
|
|
|
|||
100
Meshtastic/Views/Nodes/Helpers/Map/PositionAltitudeChart.swift
Normal file
100
Meshtastic/Views/Nodes/Helpers/Map/PositionAltitudeChart.swift
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
//
|
||||
// PositionAltitudeChart.swift
|
||||
// Meshtastic
|
||||
//
|
||||
// Copyright(c) Garth Vander Houwen 11/17/23.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Charts
|
||||
#if canImport(MapKit)
|
||||
import MapKit
|
||||
#endif
|
||||
|
||||
|
||||
@available(iOS 17.0, macOS 14.0, *)
|
||||
struct PositionAltitudeChart: View {
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
@ObservedObject var node: NodeInfoEntity
|
||||
|
||||
@State private var lineWidth = 2.0
|
||||
@State private var interpolationMethod: ChartInterpolationMethod = .linear
|
||||
@State private var chartColor: Color = .accentColor
|
||||
@State private var showSymbols = true
|
||||
|
||||
var body: some View {
|
||||
let nodePositions = Array(node.positions!) as! [PositionEntity]
|
||||
let data = nodePositions.map { PositionAltitude(time: $0.time ?? Date(), altitude: Measurement(value: Double($0.altitude), unit: .meters) ) }
|
||||
HStack {
|
||||
Chart(data, id: \.time) {
|
||||
LineMark(
|
||||
x: .value("Time", $0.time),
|
||||
y: .value("Altitude", PlottableMeasurement(measurement: $0.altitude))
|
||||
)
|
||||
.accessibilityLabel($0.time.formatted(date: .abbreviated, time: .shortened))
|
||||
.accessibilityValue("\($0.altitude) ft high")
|
||||
.lineStyle(StrokeStyle(lineWidth: lineWidth))
|
||||
.foregroundStyle(chartColor.gradient)
|
||||
.interpolationMethod(interpolationMethod.mode)
|
||||
.symbol(Circle().strokeBorder(lineWidth: lineWidth))
|
||||
.symbolSize(showSymbols ? 60 : 0)
|
||||
}
|
||||
.chartYAxis {
|
||||
AxisMarks { value in
|
||||
AxisGridLine()
|
||||
AxisValueLabel("""
|
||||
\(value.as(PlottableMeasurement.self)!
|
||||
.measurement
|
||||
.converted(to: .meters),
|
||||
format: .measurement(
|
||||
width: .wide,
|
||||
numberFormatStyle: .number.precision(
|
||||
.fractionLength(0))
|
||||
)
|
||||
)
|
||||
""")
|
||||
}
|
||||
}
|
||||
.chartXAxis(.visible)
|
||||
}
|
||||
.padding()
|
||||
.background(Color(UIColor.secondarySystemBackground))
|
||||
.opacity(/*@START_MENU_TOKEN@*/0.8/*@END_MENU_TOKEN@*/)
|
||||
}
|
||||
}
|
||||
|
||||
struct PositionAltitude {
|
||||
let time: Date
|
||||
var altitude: Measurement<UnitLength>
|
||||
}
|
||||
|
||||
enum ChartInterpolationMethod: Identifiable, CaseIterable {
|
||||
case linear
|
||||
case monotone
|
||||
case catmullRom
|
||||
case cardinal
|
||||
case stepStart
|
||||
case stepCenter
|
||||
case stepEnd
|
||||
|
||||
var id: String { mode.description }
|
||||
|
||||
var mode: InterpolationMethod {
|
||||
switch self {
|
||||
case .linear:
|
||||
return .linear
|
||||
case .monotone:
|
||||
return .monotone
|
||||
case .stepStart:
|
||||
return .stepStart
|
||||
case .stepCenter:
|
||||
return .stepCenter
|
||||
case .stepEnd:
|
||||
return .stepEnd
|
||||
case .catmullRom:
|
||||
return .catmullRom
|
||||
case .cardinal:
|
||||
return .cardinal
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -78,10 +78,12 @@ struct MeshMap: View {
|
|||
}
|
||||
/// Convex Hull
|
||||
if showConvexHull {
|
||||
let hull = lineCoords.getConvexHull()
|
||||
MapPolygon(coordinates: hull)
|
||||
.stroke(.blue, lineWidth: 3)
|
||||
.foregroundStyle(.indigo.opacity(0.4))
|
||||
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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue