// // Routes.swift // Meshtastic // // Created by Garth Vander Houwen on 11/21/23. // import SwiftUI import CoreData import MapKit import CoreLocation import CoreMotion @available(iOS 17.0, macOS 14.0, *) struct RouteRecorder: View { @ObservedObject var locationsHandler: LocationsHandler = LocationsHandler.shared @Environment(\.managedObjectContext) var context @State private var position: MapCameraPosition = .userLocation(followsHeading: true, fallback: .automatic) //@State var mapStyle: MapStyle = MapStyle.hybrid(elevation: .realistic, pointsOfInterest: .all, showsTraffic: true) @State var mapStyle: MapStyle = MapStyle.standard(elevation: .realistic) @State var isShowingDetails = false @Namespace var namespace @Namespace var routerecorderscope @State var recording: RouteEntity? @State var color: Color = .blue var body: some View { VStack { ZStack { Map(position: $position, scope: routerecorderscope) { UserAnnotation() /// Route Lines let lineCoords = locationsHandler.locationsArray.compactMap({(position) -> CLLocationCoordinate2D in return position.coordinate }) let gradient = LinearGradient( colors: [color], startPoint: .leading, endPoint: .trailing ) let dashed = StrokeStyle( lineWidth: 3, lineCap: .round, lineJoin: .round, dash: [10, 10] ) MapPolyline(coordinates: lineCoords) .stroke(gradient, style: dashed) } .mapStyle(mapStyle) } .mapScope(routerecorderscope) .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 { if locationsHandler.isRecording { HStack (alignment: .center) { Image(systemName: "record.circle.fill") .symbolRenderingMode(.multicolor) .font(.title) .foregroundColor(.red) Text("Recording route") .font(.title) Spacer() Text("\(locationsHandler.count)") .foregroundColor(.red) .font(.title2) } .padding() } else if locationsHandler.isRecordingPaused { HStack (alignment: .center) { Image(systemName: "playpause") .symbolRenderingMode(.multicolor) .font(.title3) .foregroundColor(.red) Text("Route recording paused") .font(.title) } .padding(.top) } if locationsHandler.isRecording || locationsHandler.isRecordingPaused { Divider() HStack { VStack { Text(locationsHandler.recordingStarted ?? Date(), style: .timer) .font(.title) .fixedSize() Text("Time") .font(.callout) .fixedSize() } .padding(.horizontal) Divider() VStack { let distance = Measurement(value: locationsHandler.distanceTraveled, unit: UnitLength.meters) Text("\(distance.formatted())") .font(.title) .fixedSize() Text("Distance") .font(.callout) .fixedSize() } .padding(.horizontal) Divider() VStack { let gain = Measurement(value: locationsHandler.elevationGain, unit: UnitLength.meters) Text(gain.formatted()) .font(.title) 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 Button { locationsHandler.isRecording = true locationsHandler.count = 0 locationsHandler.distanceTraveled = 0.0 locationsHandler.elevationGain = 0.0 locationsHandler.locationsArray.removeAll() locationsHandler.recordingStarted = Date() let newRoute = RouteEntity(context: context) newRoute.name = String("Route Recording") newRoute.id = Int32.random(in: Int32(Int8.max) ... Int32.max) newRoute.color = Int64(UIColor.random.hex) newRoute.date = Date() newRoute.enabled = false color = Color(UIColor(hex: UInt32(newRoute.color))) self.recording = newRoute do { try context.save() print("💾 Saved a new route") } catch { context.rollback() let nsError = error as NSError print("💥 Error Saving RouteEntity from the Route Recorder \(nsError)") } } label: { Label("start", systemImage: "play") } .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 paused show resume 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 or paused, show finish button Button { locationsHandler.isRecording = false locationsHandler.isRecordingPaused = false locationsHandler.distanceTraveled = 0.0 locationsHandler.elevationGain = 0.0 locationsHandler.locationsArray.removeAll() locationsHandler.recordingStarted = nil if let rec = recording { rec.enabled = true context.refresh(rec, mergeChanges:true) } do { try context.save() print("💾 Saved a route finish") } catch { context.rollback() let nsError = error as NSError print("💥 Error Saving RouteEntity from the Route Recorder \(nsError)") } } label: { Label("finish", systemImage: "flag.checkered") } .buttonStyle(.bordered) .buttonBorderShape(.capsule) .controlSize(.large) .padding(.bottom) } #if targetEnvironment(macCatalyst) Button(role: .cancel) { isShowingDetails = false } label: { Label("close", systemImage: "xmark") } .buttonStyle(.bordered) .buttonBorderShape(.capsule) .controlSize(.large) .padding(.bottom) #endif Spacer() } } } } .presentationDetents([.fraction(0.30), .fraction(0.65)]) .presentationDragIndicator(.hidden) .interactiveDismissDisabled(false) .onAppear { UIApplication.shared.isIdleTimerDisabled = true } .onDisappear(perform: { UIApplication.shared.isIdleTimerDisabled = false }) .onChange(of: locationsHandler.locationsArray.last) { newLoc in if locationsHandler.isRecording { if let loc = newLoc { if recording != nil { let locationEntity = LocationEntity(context: context) locationEntity.routeLocation = recording locationEntity.id = Int32(locationsHandler.count) locationEntity.altitude = Int32(loc.altitude) locationEntity.heading = Int32(loc.course) locationEntity.speed = Int32(loc.speed) locationEntity.latitudeI = Int32(loc.coordinate.latitude * 1e7) locationEntity.longitudeI = Int32(loc.coordinate.longitude * 1e7) do { try context.save() print("💾 Saved a new route location") //print("💾 Updated Canned Messages Messages For: \(fetchedNode[0].num)") } catch { context.rollback() let nsError = error as NSError print("💥 Error Saving LocationEntity from the Route Recorder \(nsError)") } } } } } } } .ignoresSafeArea(.all, edges: [.top, .leading, .trailing]) } }