From f6cb21d6d82fdaed555f358947b74e5e11aa3723 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Tue, 21 Nov 2023 22:39:42 -0800 Subject: [PATCH] Routes --- Meshtastic.xcodeproj/project.pbxproj | 12 +- .../CoreData/LocationEntityExtension.swift | 42 ++ .../Meshtastic.xcdatamodeld/.xccurrentversion | 2 +- .../contents | 4 +- .../contents | 376 ++++++++++++++++++ .../Nodes/Helpers/Map/MapSettingsForm.swift | 2 + .../Nodes/Helpers/Map/NodeMapSwiftUI.swift | 22 +- .../Nodes/Helpers/Map/WaypointForm.swift | 10 +- Meshtastic/Views/Settings/Routes.swift | 126 ++++++ Meshtastic/Views/Settings/Settings.swift | 12 + 10 files changed, 587 insertions(+), 21 deletions(-) create mode 100644 Meshtastic/Extensions/CoreData/LocationEntityExtension.swift create mode 100644 Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV20.xcdatamodel/contents create mode 100644 Meshtastic/Views/Settings/Routes.swift diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index 742bf295..3da89de8 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -100,6 +100,8 @@ DDA0B6B2294CDC55001356EC /* Channels.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDA0B6B1294CDC55001356EC /* Channels.swift */; }; DDA1C48E28DB49D3009933EC /* ChannelRoles.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDA1C48D28DB49D3009933EC /* ChannelRoles.swift */; }; DDA6B2E928419CF2003E8C16 /* MeshPackets.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDA6B2E828419CF2003E8C16 /* MeshPackets.swift */; }; + DDAB580D2B0DAA9E00147258 /* Routes.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDAB580C2B0DAA9E00147258 /* Routes.swift */; }; + DDAB580F2B0DAFBC00147258 /* LocationEntityExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDAB580E2B0DAFBC00147258 /* LocationEntityExtension.swift */; }; DDAD49ED2AFB39DC00B4425D /* MeshMap.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDAD49EC2AFB39DC00B4425D /* MeshMap.swift */; }; DDAF8C5326EB1DF10058C060 /* BLEManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDAF8C5226EB1DF10058C060 /* BLEManager.swift */; }; DDB6ABD628AE742000384BA1 /* BluetoothConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDB6ABD528AE742000384BA1 /* BluetoothConfig.swift */; }; @@ -314,6 +316,9 @@ DDA0B6B1294CDC55001356EC /* Channels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Channels.swift; sourceTree = ""; }; DDA1C48D28DB49D3009933EC /* ChannelRoles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChannelRoles.swift; sourceTree = ""; }; DDA6B2E828419CF2003E8C16 /* MeshPackets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeshPackets.swift; sourceTree = ""; }; + DDAB580B2B0D913500147258 /* MeshtasticDataModelV20.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV20.xcdatamodel; sourceTree = ""; }; + DDAB580C2B0DAA9E00147258 /* Routes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Routes.swift; sourceTree = ""; }; + DDAB580E2B0DAFBC00147258 /* LocationEntityExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationEntityExtension.swift; sourceTree = ""; }; DDAD49EC2AFB39DC00B4425D /* MeshMap.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeshMap.swift; sourceTree = ""; }; DDAF8C5226EB1DF10058C060 /* BLEManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BLEManager.swift; sourceTree = ""; }; DDB6ABD528AE742000384BA1 /* BluetoothConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BluetoothConfig.swift; sourceTree = ""; }; @@ -472,6 +477,7 @@ DD5394FD276BA0EF00AD86B1 /* PositionEntityExtension.swift */, DDD9E4E3284B208E003777C5 /* UserEntityExtension.swift */, DD964FC1297272AE007C176F /* WaypointEntityExtension.swift */, + DDAB580E2B0DAFBC00147258 /* LocationEntityExtension.swift */, ); path = CoreData; sourceTree = ""; @@ -506,6 +512,7 @@ DD97E96728EFE9A00056DDA4 /* About.swift */, DD0F791A28713C8A00A6FDAD /* AdminMessageList.swift */, DD4A911D2708C65400501B7E /* AppSettings.swift */, + DDAB580C2B0DAA9E00147258 /* Routes.swift */, DDA0B6B1294CDC55001356EC /* Channels.swift */, DDD6EEAE29BC024700383354 /* Firmware.swift */, DD8169FA271F1F3A00F4AB02 /* MeshLog.swift */, @@ -1208,6 +1215,7 @@ DD3CC6C028E7A60700FA9159 /* MessagingEnums.swift in Sources */, DD97E96628EFD9820056DDA4 /* MeshtasticLogo.swift in Sources */, DD5E523A298EFA5300D21B61 /* TelemetryWeather.swift in Sources */, + DDAB580D2B0DAA9E00147258 /* Routes.swift in Sources */, C9697F9D279336B700250207 /* LocalMBTileOverlay.swift in Sources */, DD58C5F22919AD3C00D5BEFB /* ChannelEntityExtension.swift in Sources */, DDDB444E29F8AB0E00EE2349 /* Int.swift in Sources */, @@ -1223,6 +1231,7 @@ DD41582A28585C32009B0E59 /* RangeTestConfig.swift in Sources */, DD1925B728CDA5A400720036 /* CannedMessagesConfigEnums.swift in Sources */, DDDB444429F8A8DD00EE2349 /* Float.swift in Sources */, + DDAB580F2B0DAFBC00147258 /* LocationEntityExtension.swift in Sources */, DD5E5211298EE33B00D21B61 /* remote_hardware.pb.swift in Sources */, DD5E5204298EE33B00D21B61 /* xmodem.pb.swift in Sources */, DDC2E15826CE248E0042C5E4 /* MeshtasticApp.swift in Sources */, @@ -1735,6 +1744,7 @@ DD3CC6BA28E366DF00FA9159 /* Meshtastic.xcdatamodeld */ = { isa = XCVersionGroup; children = ( + DDAB580B2B0D913500147258 /* MeshtasticDataModelV20.xcdatamodel */, DD2CC2E52ABFE04E00EDFDA7 /* MeshtasticDataModelV19.xcdatamodel */, DDDB26492AAD743E003AFCB7 /* MeshtasticDataModelV18.xcdatamodel */, DDF6B2462A9AEB9E00BA6931 /* MeshtasticDataModelV17.xcdatamodel */, @@ -1755,7 +1765,7 @@ DD5D0A9A2931AD6B00F7EA61 /* MeshtasticDataModelV2.xcdatamodel */, DD3CC6BB28E366DF00FA9159 /* MeshtasticDataModel.xcdatamodel */, ); - currentVersion = DD2CC2E52ABFE04E00EDFDA7 /* MeshtasticDataModelV19.xcdatamodel */; + currentVersion = DDAB580B2B0D913500147258 /* MeshtasticDataModelV20.xcdatamodel */; name = Meshtastic.xcdatamodeld; path = Meshtastic/Meshtastic.xcdatamodeld; sourceTree = ""; diff --git a/Meshtastic/Extensions/CoreData/LocationEntityExtension.swift b/Meshtastic/Extensions/CoreData/LocationEntityExtension.swift new file mode 100644 index 00000000..310fe626 --- /dev/null +++ b/Meshtastic/Extensions/CoreData/LocationEntityExtension.swift @@ -0,0 +1,42 @@ +// +// LocationEntityExtension.swift +// Meshtastic +// +// Copyright (c) Garth Vander Houwen 11/21/23. +// + +import CoreData +import CoreLocation +import MapKit +import SwiftUI + +extension LocationEntity { + + var latitude: Double? { + + let d = Double(latitudeI) + if d == 0 { + return 0 + } + return d / 1e7 + } + + var longitude: Double? { + + let d = Double(longitudeI) + if d == 0 { + return 0 + } + return d / 1e7 + } + + var locationCoordinate: CLLocationCoordinate2D? { + if latitudeI != 0 && longitudeI != 0 { + let coord = CLLocationCoordinate2D(latitude: latitude!, longitude: longitude!) + return coord + } else { + return nil + } + } +} + diff --git a/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion b/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion index 074f016d..fbcba258 100644 --- a/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion +++ b/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion @@ -3,6 +3,6 @@ _XCCurrentVersionName - MeshtasticDataModelV19.xcdatamodel + MeshtasticDataModelV20.xcdatamodel diff --git a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV19.xcdatamodel/contents b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV19.xcdatamodel/contents index 0dc699d5..08699813 100644 --- a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV19.xcdatamodel/contents +++ b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV19.xcdatamodel/contents @@ -1,5 +1,5 @@ - + @@ -290,7 +290,7 @@ - + diff --git a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV20.xcdatamodel/contents b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV20.xcdatamodel/contents new file mode 100644 index 00000000..ec6e67db --- /dev/null +++ b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV20.xcdatamodel/contents @@ -0,0 +1,376 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Meshtastic/Views/Nodes/Helpers/Map/MapSettingsForm.swift b/Meshtastic/Views/Nodes/Helpers/Map/MapSettingsForm.swift index fdf4cc4a..c9c351ba 100644 --- a/Meshtastic/Views/Nodes/Helpers/Map/MapSettingsForm.swift +++ b/Meshtastic/Views/Nodes/Helpers/Map/MapSettingsForm.swift @@ -80,6 +80,7 @@ struct MapSettingsForm: View { } } } + #if targetEnvironment(macCatalyst) Spacer() Button { @@ -95,5 +96,6 @@ Spacer() } .presentationDetents([.fraction(0.45), .fraction(0.65)]) .presentationDragIndicator(.visible) + } } diff --git a/Meshtastic/Views/Nodes/Helpers/Map/NodeMapSwiftUI.swift b/Meshtastic/Views/Nodes/Helpers/Map/NodeMapSwiftUI.swift index 95d32fe7..703ef890 100644 --- a/Meshtastic/Views/Nodes/Helpers/Map/NodeMapSwiftUI.swift +++ b/Meshtastic/Views/Nodes/Helpers/Map/NodeMapSwiftUI.swift @@ -61,18 +61,16 @@ struct NodeMapSwiftUI: View { let nodeColor = UIColor(hex: UInt32(node.num)) /// Route Lines 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: 3, - lineCap: .round, lineJoin: .round, dash: [10, 10] - ) - MapPolyline(coordinates: lineCoords) - .stroke(gradient, style: dashed) - } + let gradient = LinearGradient( + colors: [Color(nodeColor.lighter().lighter().lighter()), Color(nodeColor.lighter()), Color(nodeColor)], + startPoint: .leading, endPoint: .trailing + ) + let dashed = StrokeStyle( + lineWidth: 3, + lineCap: .round, lineJoin: .round, dash: [10, 10] + ) + MapPolyline(coordinates: lineCoords) + .stroke(gradient, style: dashed) } /// Convex Hull if showConvexHull { diff --git a/Meshtastic/Views/Nodes/Helpers/Map/WaypointForm.swift b/Meshtastic/Views/Nodes/Helpers/Map/WaypointForm.swift index f68ed469..36bf1555 100644 --- a/Meshtastic/Views/Nodes/Helpers/Map/WaypointForm.swift +++ b/Meshtastic/Views/Nodes/Helpers/Map/WaypointForm.swift @@ -11,7 +11,7 @@ import MapKit import CoreLocation struct WaypointForm: View { - + @EnvironmentObject var bleManager: BLEManager @Environment(\.dismiss) private var dismiss @State var waypoint: WaypointEntity @@ -27,7 +27,7 @@ struct WaypointForm: View { @State private var expire: Date = Date.now.addingTimeInterval(60 * 480) // 1 minute * 480 = 8 Hours @State private var locked: Bool = false @State private var lockedTo: Int64 = 0 - + var body: some View { VStack { if editMode { @@ -341,7 +341,7 @@ struct WaypointForm: View { } } .padding(.top) - #if targetEnvironment(macCatalyst) +#if targetEnvironment(macCatalyst) Spacer() Button { dismiss() @@ -352,7 +352,7 @@ struct WaypointForm: View { .buttonBorderShape(.capsule) .controlSize(.large) .padding() - #endif +#endif } } } @@ -381,7 +381,7 @@ struct WaypointForm: View { expires = false expire = Date.now.addingTimeInterval(60 * 480) icon = "📍" - latitude = waypoint.coordinate.latitude + latitude = waypoint.coordinate.latitude longitude = waypoint.coordinate.longitude } } diff --git a/Meshtastic/Views/Settings/Routes.swift b/Meshtastic/Views/Settings/Routes.swift new file mode 100644 index 00000000..43dfa543 --- /dev/null +++ b/Meshtastic/Views/Settings/Routes.swift @@ -0,0 +1,126 @@ +// +// Routes.swift +// Meshtastic +// +// Created by Garth Vander Houwen on 11/21/23. +// + +import SwiftUI +import CoreData +import MapKit + +@available(iOS 17.0, macOS 14.0, *) +struct Routes: View { + + @State private var columnVisibility = NavigationSplitViewVisibility.doubleColumn + @Environment(\.managedObjectContext) var context + @EnvironmentObject var bleManager: BLEManager + @State private var selectedRoute: RouteEntity? + @State private var importing = false + + @FetchRequest(sortDescriptors: [], animation: .default) + + var routes: FetchedResults + var body: some View { + NavigationSplitView(columnVisibility: $columnVisibility) { + Button("Import Route") { + importing = true + } + .fileImporter( + isPresented: $importing, + allowedContentTypes: [.commaSeparatedText], + allowsMultipleSelection: false + ) { result in + do { + guard let selectedFile: URL = try result.get().first else { return } + + guard selectedFile.startAccessingSecurityScopedResource() else { // Notice this line right here + return + } + + do { + guard let fileContent = String(data: try Data(contentsOf: selectedFile), encoding: .utf8) else { return } + let routeName = selectedFile.lastPathComponent.dropLast(4) + let lines = fileContent.components(separatedBy: "\n") + let headers = lines.first?.components(separatedBy: ",") + var latIndex = -1 + var longIndex = -1 + for index in headers!.indices { + print("\(index): \( headers![index])") + if headers![index].trimmingCharacters(in: .whitespaces) == "Latitude" { + latIndex = index + } else if headers![index].trimmingCharacters(in: .whitespaces) == "Longitude" { + longIndex = index + } + } + if latIndex >= 0 && longIndex >= 0 { + let newRoute = RouteEntity(context: context) + newRoute.name = ("\(String(routeName)) - \(Date().formatted())") + newRoute.id = Int32.random(in: Int32(Int8.max) ... Int32.max) + newRoute.color = 12 + newRoute.date = Date() + var newLocations = [LocationEntity]() + lines.dropFirst().forEach { line in + let data = line.components(separatedBy: ",") + if data.count > 1 { + let latitude = latIndex >= 0 ? data[latIndex].trimmingCharacters(in: .whitespaces) : "0" + let longitude = longIndex >= 0 ? data[longIndex].trimmingCharacters(in: .whitespaces) : "0" + let loc = LocationEntity(context: context) + loc.latitudeI = Int32((Double(latitude) ?? 0) * 1e7) + loc.longitudeI = Int32((Double(longitude) ?? 0) * 1e7) + newLocations.append(loc) + print("Longitude: \(longitude) Latitude: \(latitude)") + } + } + newRoute.locations? = NSOrderedSet(array: newLocations) + do { + try context.save() + } catch let error as NSError { + print("Error: \(error.localizedDescription)") + } + } + + } catch { + print("error: \(error)") // to do deal with errors + } + + } catch { + print("CSV Import Error") + } + } + + VStack { + List(routes, id: \.self, selection: $selectedRoute) { route in + Text(route.name ?? "No Name Route") + .font(.title) + } + .listStyle(.plain) + } + .navigationTitle("Route List") + } detail: { + VStack { + if selectedRoute != nil { + let locationArray = selectedRoute?.locations?.array as? [LocationEntity] ?? [] + let lineCoords = locationArray.compactMap({(location) -> CLLocationCoordinate2D in + return location.locationCoordinate ?? LocationHelper.DefaultLocation + }) + + Map () { + + let gradient = LinearGradient( + colors: [.cyan, .blue, .secondary],//[Color(nodeColor.lighter().lighter()), Color(nodeColor.lighter()), Color(nodeColor)], + startPoint: .leading, endPoint: .trailing + ) + let dashed = StrokeStyle( + lineWidth: 3, + lineCap: .round, lineJoin: .round, dash: [10, 10] + ) + MapPolyline(coordinates: lineCoords) + .stroke(gradient, style: dashed) + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + } + }.navigationTitle(" \(selectedRoute?.name ?? "Unknown Route") Map") + } + } +} diff --git a/Meshtastic/Views/Settings/Settings.swift b/Meshtastic/Views/Settings/Settings.swift index 9bc1792c..1694105e 100644 --- a/Meshtastic/Views/Settings/Settings.swift +++ b/Meshtastic/Views/Settings/Settings.swift @@ -17,6 +17,7 @@ struct Settings: View { @State private var selection: SettingsSidebar = .about enum SettingsSidebar { case appSettings + case routes case shareChannels case userConfig case loraConfig @@ -57,6 +58,17 @@ struct Settings: View { Text("app.settings") } .tag(SettingsSidebar.appSettings) + if #available(iOS 17.0, macOS 14.0, *) { + NavigationLink { + Routes() + } label: { + Image(systemName: "gearshape") + .symbolRenderingMode(.hierarchical) + Text("routes") + } + .tag(SettingsSidebar.routes) + } + let node = nodes.first(where: { $0.num == preferredNodeNum }) let hasAdmin = node?.myInfo?.adminIndex ?? 0 > 0 ? true : false if !(node?.deviceConfig?.isManaged ?? false) {