Location updates and route recorder mock up (does everything but save the route) Improve speed and heading formatting, Updated about view

This commit is contained in:
Garth Vander Houwen 2023-12-24 22:42:07 -08:00
parent c523b05d23
commit 91407e65ea
10 changed files with 270 additions and 150 deletions

View file

@ -0,0 +1,23 @@
{
"images" : [
{
"filename" : "solarnode.jpeg",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "solarnode 1.jpeg",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "solar_node.jpeg",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 206 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

View file

@ -16,12 +16,16 @@ import CoreLocation
static let shared = LocationsHandler() // Create a single, shared instance of the object.
private let manager: CLLocationManager
private var background: CLBackgroundActivitySession?
var locationsArray: [CLLocation]
var enableSmartPosition: Bool
//@Published var lastLocation = CLLocation()
@Published var locationsArray: [CLLocation]
@Published var isStationary = false
@Published var count = 0
@Published var isRecording = false
@Published var isRecordingPaused = false
@Published var recordingStarted: Date?
@Published var distanceTraveled = 0.0
@Published var elevationGain = 0.0
@Published
var updatesStarted: Bool = UserDefaults.standard.bool(forKey: "liveUpdatesStarted") {
@ -95,6 +99,16 @@ import CoreLocation
print("Bad Location \(self.count): Horizontal Accuracy: \(location.horizontalAccuracy) \(location)")
return false
}
if isRecording {
if let lastLocation = locationsArray.last {
let distance = location.distance(from: lastLocation)
let gain = location.altitude - lastLocation.altitude
distanceTraveled += distance
if gain > 0 {
elevationGain += gain
}
}
}
locationsArray.append(location)
return true
}
@ -103,7 +117,7 @@ import CoreLocation
static var satsInView: Int {
var sats = 0
if let newLocation = shared.locationsArray.last{
if let newLocation = shared.locationsArray.last {
sats = 1
if newLocation.verticalAccuracy > 0 {
sats = 4

View file

@ -47,12 +47,12 @@ struct PositionLog: View {
}
TableColumn("Speed") { position in
let speed = Measurement(value: Double(position.speed), unit: UnitSpeed.kilometersPerHour)
Text(speed.formatted())
Text(speed.formatted(.measurement(width: .abbreviated, numberFormatStyle: .number.precision(.fractionLength(0)))))
}
TableColumn("Heading") { position in
let degrees = Angle.degrees(Double(position.heading))
let heading = Measurement(value: degrees.degrees, unit: UnitAngle.degrees)
Text("\(heading.formatted())")
Text(heading.formatted(.measurement(width: .narrow, numberFormatStyle: .number.precision(.fractionLength(0)))))
}
TableColumn("SNR") { position in
Text("\(String(format: "%.2f", position.snr)) dB")

View file

@ -17,11 +17,32 @@ struct AboutMeshtastic: View {
List {
Section(header: Text("What is Meshtastic?")) {
Text("An open source, off-grid, decentralized, mesh network built to run on affordable, low-power devices.")
Text("An open source, off-grid, decentralized, mesh network that runs on affordable, low-power radios.")
.font(.title3)
}
Section(header: Text("Apple Apps")) {
if locale.region?.identifier ?? "US" == "US" {
HStack {
Image("SOLAR_NODE")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 75)
.cornerRadius(5)
.padding()
VStack(alignment: .leading) {
Link("Buy Complete Radios", destination: URL(string: "http://garthvh.com")!)
.font(.title2)
Text("Get custom waterproof solar and detection sensor router nodes, aluminium desktop nodes and rugged handsets.")
.font(.callout)
}
}
}
Link("Sponsor App Development", destination: URL(string: "https://github.com/sponsors/garthvh")!)
.font(.title2)
Link("GitHub Repository", destination: URL(string: "https://github.com/meshtastic/Meshtastic-Apple")!)
.font(.title2)
Button("Review the app") {
if let scene = UIApplication.shared.connectedScenes
.first(where: { $0.activationState == .foregroundActive }) as? UIWindowScene {
@ -29,17 +50,8 @@ struct AboutMeshtastic: View {
}
}
.font(.title2)
Link("Sponsor App Development", destination: URL(string: "https://github.com/sponsors/garthvh")!)
.font(.title2)
Link("GitHub Repository", destination: URL(string: "https://github.com/meshtastic/Meshtastic-Apple")!)
.font(.title2)
}
if locale.region?.identifier ?? "no locale" == "US" {
Section(header: Text("Get Devices")) {
Link("Buy Complete Radios", destination: URL(string: "http://garthvh.com")!)
.font(.title2)
}
}
Section(header: Text("Project information")) {
Link("Website", destination: URL(string: "https://meshtastic.org")!)
.font(.title2)

View file

@ -11,33 +11,37 @@ import CoreLocation
@available(iOS 17.0, macOS 14.0, *)
struct GPSStatus: View {
var largeFont: Font = .footnote
var smallFont: Font = .caption2
@ObservedObject var locationsHandler: LocationsHandler = LocationsHandler.shared
var body: some View {
if let newLocation = locationsHandler.locationsArray.last {
let horizontalAccuracy = Measurement(value: newLocation.horizontalAccuracy, unit: UnitLength.meters)
let verticalAccuracy = Measurement(value: newLocation.verticalAccuracy, unit: UnitLength.meters)
let altitiude = Measurement(value: newLocation.altitude, unit: UnitLength.meters)
let speed = Measurement(value: newLocation.speed, unit: UnitSpeed.kilometersPerHour)
let speedAccuracy = Measurement(value: newLocation.speedAccuracy, unit: UnitSpeed.metersPerSecond)
let courseAccuracy = Measurement(value: newLocation.courseAccuracy, unit: UnitAngle.degrees)
let horizontalAccuracy = Measurement(value: newLocation.horizontalAccuracy, unit: UnitLength.meters)
let verticalAccuracy = Measurement(value: newLocation.verticalAccuracy, unit: UnitLength.meters)
let altitiude = Measurement(value: newLocation.altitude, unit: UnitLength.meters)
let speed = Measurement(value: newLocation.speed, unit: UnitSpeed.kilometersPerHour)
let speedAccuracy = Measurement(value: newLocation.speedAccuracy, unit: UnitSpeed.metersPerSecond)
let courseAccuracy = Measurement(value: newLocation.courseAccuracy, unit: UnitAngle.degrees)
Label("Coordinate \(String(format: "%.5f", newLocation.coordinate.latitude)), \(String(format: "%.5f", newLocation.coordinate.longitude))", systemImage: "mappin")
.font(.footnote)
.font(largeFont)
.textSelection(.enabled)
HStack {
Label("Accuracy \(horizontalAccuracy.formatted())", systemImage: "scope")
.font(.footnote)
.font(largeFont)
Label("Sats Estimate \(LocationsHandler.satsInView)", systemImage: "sparkles")
.font(.footnote)
.font(largeFont)
}
HStack {
if newLocation.verticalAccuracy > 0 {
Label("Altitude \(altitiude.formatted())", systemImage: "mountain.2")
.font(.footnote)
.font(largeFont)
}
Label("Accuracy \(verticalAccuracy.formatted())", systemImage: "lines.measurement.vertical")
.font(.caption2)
.font(smallFont)
}
HStack {
let degrees = Angle.degrees(newLocation.course)
@ -49,15 +53,15 @@ struct GPSStatus: View {
.symbolRenderingMode(.hierarchical)
.rotationEffect(degrees)
}
.font(.footnote)
.font(largeFont)
Label("Accuracy \(courseAccuracy.formatted(.measurement(width: .narrow, numberFormatStyle: .number.precision(.fractionLength(0)))))", systemImage: "safari")
.font(.caption2)
.font(smallFont)
}
HStack {
Label("Speed \(speed.formatted(.measurement(width: .abbreviated, numberFormatStyle: .number.precision(.fractionLength(0)))))", systemImage: "speedometer")
.font(.footnote)
.font(largeFont)
Label("Accuracy \(speedAccuracy.formatted(.measurement(width: .abbreviated, numberFormatStyle: .number.precision(.fractionLength(0)))))", systemImage: "gauge.with.dots.needle.bottom.50percent.badge.plus")
.font(.caption2)
.font(smallFont)
}
}
}

View file

@ -30,142 +30,209 @@ struct TimerDisplayObject {
@available(iOS 17.0, macOS 14.0, *)
struct RouteRecorder: View {
@ObservedObject var locationsHandler = LocationsHandler.shared
@ObservedObject var locationsHandler: LocationsHandler = LocationsHandler.shared
@Environment(\.managedObjectContext) var context
@State private var position: MapCameraPosition = .userLocation(followsHeading: true, fallback: .automatic)
@State var isTimerRunning = false
@State var isShowingDetails = false
@State var timer: Timer?
@Namespace var namespace
@Namespace var routerecorderscope
@State var timeElapsed: TimerDisplayObject = TimerDisplayObject()
@State var timerDisplay = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
var body: some View {
VStack {
VStack {
VStack {
Map(position: $position, scope: routerecorderscope) {
UserAnnotation()
Map(position: $position, scope: routerecorderscope) {
UserAnnotation()
// ForEach(locations, id: \.id) { location in
// Marker(location.name, systemImage: location.icon, coordinate: location.location)
// .tint(location.colour)
// }
}
}
.mapScope(routerecorderscope)
.mapControls {
MapUserLocationButton()
MapCompass()
MapScaleView()
MapPitchToggle()
}
.mapStyle(.hybrid(elevation: .realistic, showsTraffic: true))
.transition(.slide)
.mapControlVisibility(.visible)
.safeAreaInset(edge: .bottom) {
ZStack {
VStack {
HStack(spacing: 10) {
Spacer()
if isTimerRunning {
Button {
isShowingDetails = true
isTimerRunning = false
} label: {
Image(systemName: "pause.fill")
.frame(width: 60, height: 60)
}
.buttonStyle(.bordered)
.buttonBorderShape(.circle)
.matchedGeometryEffect(id: "Pause Button", in: namespace)
} else {
Button {
isShowingDetails = true
isTimerRunning = true
timeElapsed.seconds -= 1
} label: {
Image(systemName: "play.fill")
.frame(width: 60, height: 60)
}
.buttonStyle(.bordered)
.buttonBorderShape(.circle)
.matchedGeometryEffect(id: "Play Button", in: namespace)
}
Spacer()
}
}
.onReceive(timerDisplay) { _ in
if isTimerRunning {
timeElapsed.seconds += 1
if timeElapsed.seconds == 60 {
timeElapsed.seconds = 0
timeElapsed.minutes += 1
if timeElapsed.minutes == 60 {
timeElapsed.minutes = 0
timeElapsed.hours += 1
}
}
}
.mapScope(routerecorderscope)
.mapControls {
MapUserLocationButton()
MapCompass()
MapScaleView()
MapPitchToggle()
}
.mapStyle(.hybrid(elevation: .realistic, showsTraffic: true))
.transition(.slide)
.mapControlVisibility(.visible)
.safeAreaInset(edge: .bottom) {
ZStack {
VStack {
HStack(spacing: 10) {
Spacer()
Button {
isShowingDetails = true
} label: {
Image(systemName: locationsHandler.isRecording ? "record.circle.fill" : "record.circle")
.font(.system(size: 72))
.symbolRenderingMode(.multicolor)
.foregroundColor(.red)
}
.buttonStyle(.bordered)
.foregroundColor(.red)
.buttonBorderShape(.circle)
.matchedGeometryEffect(id: "Details Button", in: namespace)
Spacer()
}
}
.padding()
}
.sheet(isPresented: $isShowingDetails) {
NavigationStack {
VStack {
HStack {
Text(timeElapsed.display)
.font(.largeTitle)
Text("Time Elapseed")
.font(.callout)
.padding()
}
.sheet(isPresented: $isShowingDetails) {
NavigationStack {
VStack {
if locationsHandler.isRecording {
HStack (alignment: .center) {
Image(systemName: "record.circle.fill")
.symbolRenderingMode(.multicolor)
.font(.title3)
.foregroundColor(.red)
Text("Recording route - \(locationsHandler.count) locations")
.font(.title3)
}
.padding()
.padding(.top)
} else if locationsHandler.isRecordingPaused {
HStack (alignment: .center) {
Image(systemName: "playpause")
.symbolRenderingMode(.multicolor)
.font(.title3)
.foregroundColor(.red)
Text("Route recording paused")
.font(.title3)
}
.padding(.top)
}
if locationsHandler.isRecording || locationsHandler.isRecordingPaused {
Divider()
VStack(alignment: .leading) {
if let lastLocation = locationsHandler.locationsArray.last {
let horizontalAccuracy = Measurement(value: lastLocation.horizontalAccuracy, unit: UnitLength.meters)
let verticalAccuracy = Measurement(value: lastLocation.verticalAccuracy, unit: UnitLength.meters)
let altitiude = Measurement(value: lastLocation.altitude, unit: UnitLength.meters)
let speed = Measurement(value: lastLocation.speed, unit: UnitSpeed.kilometersPerHour)
List {
Label("Coordinate \(String(format: "%.5f", lastLocation.coordinate.latitude)), \(String(format: "%.5f", lastLocation.coordinate.longitude))", systemImage: "mappin")
.textSelection(.enabled)
Label("Horizontal Accuracy \(horizontalAccuracy.formatted())", systemImage: "scope")
if lastLocation.verticalAccuracy > 0 {
Label("Altitude \(altitiude.formatted())", systemImage: "mountain.2")
}
Label("Vertical Accuracy \(verticalAccuracy.formatted())", systemImage: "lines.measurement.vertical")
Label("Satellites Estimate \(LocationHelper.satsInView)", systemImage: "sparkles")
Label("\(locationsHandler.isStationary ? "Moving" : "Stationary")", systemImage: locationsHandler.isStationary ? "figure.walk.motion" : "figure.stand")
if lastLocation.speedAccuracy > 0 {
Label("Speed \(speed.formatted())", systemImage: "speedometer")
}
if lastLocation.courseAccuracy > 0 {
/// Heading
let degrees = Angle.degrees(Double(lastLocation.course))
Label {
let heading = Measurement(value: degrees.degrees, unit: UnitAngle.degrees)
/// Text("Heading: \(heading.formatted())")
Text("Heading \(String(format: "%.2f", lastLocation.course))°")
.foregroundColor(.primary)
} icon: {
Image(systemName: "location.circle")
.symbolRenderingMode(.hierarchical)
.frame(width: 35)
.rotationEffect(degrees)
}
}
}
.listStyle(.plain)
HStack {
VStack {
Text(locationsHandler.recordingStarted ?? Date(), style: .timer)
.font(.largeTitle)
.fixedSize()
Text("Time")
.font(.callout)
.fixedSize()
}
.padding(.horizontal)
Divider()
VStack {
let distance = Measurement(value: locationsHandler.distanceTraveled, unit: UnitLength.meters)
Text("\(distance.formatted())")
.font(.largeTitle)
.fixedSize()
Text("Distance")
.font(.callout)
.fixedSize()
}
.padding(.horizontal)
Divider()
VStack {
let gain = Measurement(value: locationsHandler.elevationGain, unit: UnitLength.meters)
Text(gain.formatted())
.font(.largeTitle)
Text("Elev. Gain")
.font(.callout)
}
.padding(.horizontal)
}
.frame(maxHeight: 90)
}
Divider()
VStack(alignment: .leading) {
List {
GPSStatus(largeFont: .body, smallFont: .callout)
}
.listStyle(.plain)
HStack {
Spacer()
if !locationsHandler.isRecording && !locationsHandler.isRecordingPaused {
/// We are not recording or paused, show start recording button a new recording
Button {
locationsHandler.isRecording = true
locationsHandler.count = 0
locationsHandler.distanceTraveled = 0.0
locationsHandler.elevationGain = 0.0
locationsHandler.locationsArray.removeAll()
locationsHandler.recordingStarted = Date()
} label: {
Label("start", systemImage: "start")
}
.buttonStyle(.bordered)
.buttonBorderShape(.capsule)
.controlSize(.large)
.padding(.bottom)
} else if locationsHandler.isRecording {
/// We are recording show pause button
Button {
locationsHandler.isRecording = false
locationsHandler.isRecordingPaused = true
} label: {
Label("pause", systemImage: "pause")
}
.buttonStyle(.bordered)
.buttonBorderShape(.capsule)
.controlSize(.large)
.padding(.bottom)
} else if locationsHandler.isRecordingPaused {
/// We are recording show pause button
Button {
locationsHandler.isRecording = true
locationsHandler.isRecordingPaused = false
} label: {
Label("resume", systemImage: "playpause")
}
.buttonStyle(.bordered)
.buttonBorderShape(.capsule)
.controlSize(.large)
.padding(.bottom)
}
if locationsHandler.isRecording || locationsHandler.isRecordingPaused {
/// We are recording show pause button
Button {
locationsHandler.isRecording = false
locationsHandler.isRecordingPaused = false
locationsHandler.distanceTraveled = 0.0
locationsHandler.elevationGain = 0.0
locationsHandler.locationsArray.removeAll()
locationsHandler.recordingStarted = nil
} label: {
Label("finish", systemImage: "flag.checkered")
}
.buttonStyle(.bordered)
.buttonBorderShape(.capsule)
.controlSize(.large)
.padding(.bottom)
}
Button(role: .cancel) {
isShowingDetails = false
} label: {
Label("close", systemImage: "xmark")
}
.buttonStyle(.bordered)
.buttonBorderShape(.capsule)
.controlSize(.large)
.padding(.bottom)
Spacer()
}
}
}
.presentationDetents([.fraction(0.6)])
.presentationDragIndicator(.visible)
}
.presentationDetents([.fraction(0.65)])
.presentationDragIndicator(.hidden)
.interactiveDismissDisabled()
}
}
}

View file

@ -70,14 +70,14 @@ struct Settings: View {
}
.tag(SettingsSidebar.routes)
// NavigationLink {
// RouteRecorder()
// } label: {
// Image(systemName: "record.circle")
// .symbolRenderingMode(.hierarchical)
// Text("route.recorder")
// }
// .tag(SettingsSidebar.routeRecorder)
NavigationLink {
RouteRecorder()
} label: {
Image(systemName: "record.circle")
.symbolRenderingMode(.hierarchical)
Text("route.recorder")
}
.tag(SettingsSidebar.routeRecorder)
}
let node = nodes.first(where: { $0.num == preferredNodeNum })