diff --git a/.gitignore b/.gitignore index e43b0f98..6bec327e 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,5 @@ .DS_Store +Build +*xcuserdata* +*xcscmblueprint* +nRF Toolbox.xcworkspace/xcshareddata diff --git a/Meshtastic Client.xcodeproj/project.pbxproj b/Meshtastic Client.xcodeproj/project.pbxproj index e91bf92c..fa84c0f5 100644 --- a/Meshtastic Client.xcodeproj/project.pbxproj +++ b/Meshtastic Client.xcodeproj/project.pbxproj @@ -7,6 +7,8 @@ objects = { /* Begin PBXBuildFile section */ + C9483F6D2773017500998F6B /* MapView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9483F6C2773017500998F6B /* MapView.swift */; }; + C9A7BC1027759A9600760B50 /* PositionAnnotationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9A7BC0F27759A9600760B50 /* PositionAnnotationView.swift */; }; DD1BF2F92776FE2E008C8D2F /* UserMessageList.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1BF2F82776FE2E008C8D2F /* UserMessageList.swift */; }; DD23A50F26FD1B4400D9B90C /* PeripheralModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD23A50E26FD1B4400D9B90C /* PeripheralModel.swift */; }; DD2E65262767A01F00E45FC5 /* NodeDetail.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD2E65252767A01F00E45FC5 /* NodeDetail.swift */; }; @@ -69,6 +71,8 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + C9483F6C2773017500998F6B /* MapView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapView.swift; sourceTree = ""; }; + C9A7BC0F27759A9600760B50 /* PositionAnnotationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PositionAnnotationView.swift; sourceTree = ""; }; DD1BF2F82776FE2E008C8D2F /* UserMessageList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserMessageList.swift; sourceTree = ""; }; DD23A50E26FD1B4400D9B90C /* PeripheralModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PeripheralModel.swift; sourceTree = ""; }; DD2E65252767A01F00E45FC5 /* NodeDetail.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeDetail.swift; sourceTree = ""; }; @@ -145,6 +149,23 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + C9483F6B2773016700998F6B /* Map */ = { + isa = PBXGroup; + children = ( + C9A7BC0E27759A6800760B50 /* Custom */, + C9483F6C2773017500998F6B /* MapView.swift */, + ); + path = Map; + sourceTree = ""; + }; + C9A7BC0E27759A6800760B50 /* Custom */ = { + isa = PBXGroup; + children = ( + C9A7BC0F27759A9600760B50 /* PositionAnnotationView.swift */, + ); + path = Custom; + sourceTree = ""; + }; DD47E3CA26F0E50300029299 /* Nodes */ = { isa = PBXGroup; children = ( @@ -265,6 +286,7 @@ DDC2E18726CE24E40042C5E4 /* Views */ = { isa = PBXGroup; children = ( + C9483F6B2773016700998F6B /* Map */, DDC2E18D26CE25CB0042C5E4 /* Helpers */, DD47E3D726F2F21A00029299 /* Bluetooth */, DD47E3CA26F0E50300029299 /* Nodes */, @@ -513,12 +535,14 @@ DD9D8F2F2764403B00080993 /* Meshtastic.xcdatamodeld in Sources */, DD1BF2F92776FE2E008C8D2F /* UserMessageList.swift in Sources */, DD23A50F26FD1B4400D9B90C /* PeripheralModel.swift in Sources */, + C9A7BC1027759A9600760B50 /* PositionAnnotationView.swift in Sources */, DD882F5D2772E4640005BF05 /* Contacts.swift in Sources */, DD47E3CE26F103C600029299 /* NodeList.swift in Sources */, DD47E3D626F17ED900029299 /* CircleText.swift in Sources */, DDC2E18F26CE25FE0042C5E4 /* ContentView.swift in Sources */, DDAF8C6326ED0A230058C060 /* admin.pb.swift in Sources */, DD539500276C452400AD86B1 /* Preferences.swift in Sources */, + C9483F6D2773017500998F6B /* MapView.swift in Sources */, DDAF8C5826ED07FD0058C060 /* mesh.pb.swift in Sources */, DD8169FB271F1F3A00F4AB02 /* MeshLog.swift in Sources */, DD2E65262767A01F00E45FC5 /* NodeDetail.swift in Sources */, @@ -691,7 +715,7 @@ CODE_SIGN_ENTITLEMENTS = MeshtasticClient/MeshtasticClient.entitlements; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_ASSET_PATHS = "\"MeshtasticClient/Preview Content\""; - DEVELOPMENT_TEAM = GCH7VS5Y9R; + DEVELOPMENT_TEAM = 37C534H572; ENABLE_PREVIEWS = YES; INFOPLIST_FILE = MeshtasticClient/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 15.0; @@ -718,7 +742,7 @@ CODE_SIGN_ENTITLEMENTS = MeshtasticClient/MeshtasticClient.entitlements; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_ASSET_PATHS = "\"MeshtasticClient/Preview Content\""; - DEVELOPMENT_TEAM = GCH7VS5Y9R; + DEVELOPMENT_TEAM = 37C534H572; ENABLE_PREVIEWS = YES; INFOPLIST_FILE = MeshtasticClient/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 15.0; diff --git a/Meshtastic Client.xcodeproj/project.xcworkspace/xcuserdata/joshuapirihi.xcuserdatad/UserInterfaceState.xcuserstate b/Meshtastic Client.xcodeproj/project.xcworkspace/xcuserdata/joshuapirihi.xcuserdatad/UserInterfaceState.xcuserstate new file mode 100644 index 00000000..c864b6bc Binary files /dev/null and b/Meshtastic Client.xcodeproj/project.xcworkspace/xcuserdata/joshuapirihi.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/MeshtasticClient/Views/Map/Custom/PositionAnnotationView.swift b/MeshtasticClient/Views/Map/Custom/PositionAnnotationView.swift new file mode 100644 index 00000000..ea2005dd --- /dev/null +++ b/MeshtasticClient/Views/Map/Custom/PositionAnnotationView.swift @@ -0,0 +1,64 @@ +// +// PositionAnnotationView.swift +// MeshtasticClient +// +// Created by Joshua Pirihi on 24/12/21. +// + +import UIKit +import MapKit + +//a simple circle annotation, with a string in it +class PositionAnnotation: NSObject, MKAnnotation { + + // This property must be key-value observable, which the `@objc dynamic` attributes provide. + @objc dynamic var coordinate = CLLocationCoordinate2D(latitude: 0, longitude: 0) + + // Required if you set the annotation view's `canShowCallout` property to `true` + //this string fills the callout label when you tap an annotation + var title: String? + + //the text to appear inside the little circle + var shortName: String? + +} + +class PositionAnnotationView: MKAnnotationView { + + private let annotationFrame = CGRect(x: 0, y: 0, width: 32, height: 32) + private let label: UILabel + + override init(annotation: MKAnnotation?, reuseIdentifier: String?) { + self.label = UILabel(frame: annotationFrame.offsetBy(dx: 0, dy: 0)) + super.init(annotation: annotation, reuseIdentifier: reuseIdentifier) + self.frame = annotationFrame + self.label.font = UIFont.preferredFont(forTextStyle: .caption2) + self.label.textColor = .white + self.label.textAlignment = .center + self.backgroundColor = .clear + self.addSubview(label) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) not implemented!") + } + + public var name: String = "" { + didSet { + self.label.text = name + } + } + + override func draw(_ rect: CGRect) { + guard let context = UIGraphicsGetCurrentContext() else { return } + + let circleRect = CGRect(x: 1, y: 1, width: 30, height: 30) + + context.setFillColor(CGColor(red: 0, green: 0.5, blue: 1.0, alpha: 1.0)) + + context.fillEllipse(in: circleRect) + + } + + +} diff --git a/MeshtasticClient/Views/Map/MapView.swift b/MeshtasticClient/Views/Map/MapView.swift new file mode 100644 index 00000000..ebdd6742 --- /dev/null +++ b/MeshtasticClient/Views/Map/MapView.swift @@ -0,0 +1,120 @@ +// +// MapView.swift +// MeshtasticClient +// +// Created by Joshua Pirihi on 22/12/21. +// + +import Foundation +import UIKit +import MapKit +import SwiftUI +import CoreData + +//wrap a MKMapView into something we can use in SwiftUI +struct MapView: UIViewRepresentable { + + var nodes: FetchedResults + + let mapViewDelegate = MapViewDelegate() + + //observe changes to the key in UserDefaults + @AppStorage("meshMapType") var type: String = "hybrid" + + func makeUIView(context: Context) -> MKMapView { + + let map = MKMapView(frame: .zero) + + map.userTrackingMode = .follow + + let region = MKCoordinateRegion( center: map.centerCoordinate, latitudinalMeters: CLLocationDistance(exactly: 500)!, longitudinalMeters: CLLocationDistance(exactly: 500)!) + map.setRegion(map.regionThatFits(region), animated: false) + + //self.updateMapType(map) + + map.register(PositionAnnotationView.self, forAnnotationViewWithReuseIdentifier: NSStringFromClass(PositionAnnotationView.self)) + + return map + } + + func updateUIView(_ view: MKMapView, context: Context) { + view.delegate = mapViewDelegate // (1) This should be set in makeUIView, but it is getting reset to `nil` + view.translatesAutoresizingMaskIntoConstraints = false // (2) In the absence of this, we get constraints error on rotation; and again, it seems one should do this in makeUIView, but has to be here + + self.updateMapType(view) + + self.showNodePositions(to: view) + } + + func updateMapType(_ map: MKMapView) { + + switch self.type { + case "satellite": + map.mapType = .satellite + break + case "standard": + map.mapType = .standard + break + case "hybrid": + map.mapType = .hybrid + break + default: + map.mapType = .hybrid + } + } +} + +private extension MapView { + + func showNodePositions(to view: MKMapView) { + + //clear any existing annotations + if !view.annotations.isEmpty { + view.removeAnnotations(view.annotations) + } + + for node in self.nodes { + //try and get the last position + if (node.positions?.count ?? 0) > 0 && (node.positions!.lastObject as! PositionEntity).coordinate != nil { + let annotation = PositionAnnotation() + annotation.coordinate = (node.positions!.lastObject as! PositionEntity).coordinate! + annotation.title = node.user?.longName ?? "Unknown" + annotation.shortName = node.user?.shortName?.uppercased() ?? "???" + + view.addAnnotation(annotation) + } + } + } +} + +class MapViewDelegate: NSObject, MKMapViewDelegate { + + func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? { + + guard !annotation.isKind(of: MKUserLocation.self) else { + // Make a fast exit if the annotation is the `MKUserLocation`, as it's not an annotation view we wish to customize. + return nil + } + + var annotationView: MKAnnotationView? + + if let annotation = annotation as? PositionAnnotation { + annotationView = self.setupPositionAnnotationView(for: annotation, on: mapView) + } + + return annotationView + } + + private func setupPositionAnnotationView(for annotation: PositionAnnotation, on mapView: MKMapView) -> PositionAnnotationView { + let identifier = NSStringFromClass(PositionAnnotationView.self) + + let annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: identifier) as? PositionAnnotationView ?? PositionAnnotationView() + + annotationView.name = annotation.shortName ?? "???" + + annotationView.canShowCallout = true + + + return annotationView + } +} diff --git a/MeshtasticClient/Views/Nodes/NodeMap.swift b/MeshtasticClient/Views/Nodes/NodeMap.swift index 81dde092..c2cca6e3 100644 --- a/MeshtasticClient/Views/Nodes/NodeMap.swift +++ b/MeshtasticClient/Views/Nodes/NodeMap.swift @@ -16,11 +16,15 @@ struct NodeMap: View { @Environment(\.managedObjectContext) var context @EnvironmentObject var bleManager: BLEManager + //@AppStorage("meshMapType") var meshMapType: String = "hybrid" + @State private var showLabels: Bool = false @State private var annotationItems: [MapLocation] = [] @FetchRequest( sortDescriptors: [NSSortDescriptor(keyPath: \NodeInfoEntity.lastHeard, ascending: false)], animation: .default) + + private var locationNodes: FetchedResults var annotations: [MapLocation] = [MapLocation]() @@ -36,16 +40,29 @@ struct NodeMap: View { set: { _ in } ) + /*ForEach ( locationNodes ) { node in + let mostRecent = node.positions?.lastObject as! PositionEntity + if mostRecent.coordinate != nil { + + annotations.append(MapLocation(name: node.user?.shortName! ?? "???", coordinate: mostRecent.coordinate!)) + + } + }*/ + NavigationView { - ZStack { - - Map(coordinateRegion: regionBinding, - interactionModes: [.all], - showsUserLocation: true, + + ZStack { + + + + /*Map(coordinateRegion: regionBinding, + interactionModes: [.all], + showsUserLocation: true, userTrackingMode: .constant(.follow), annotationItems: self.locationNodes.filter({ nodeinfo in - return nodeinfo.positions != nil && nodeinfo.positions!.count > 1 + return nodeinfo.positions != nil && nodeinfo.positions!.count > 0// && (nodeinfo.positions?.lastObject as? AnyObject)?.coordinate != nil + }) ) { locationNode in @@ -56,10 +73,14 @@ struct NodeMap: View { } ) - } - .frame(maxHeight: .infinity) - .ignoresSafeArea(.all, edges: [.leading, .trailing]) - } + + }*/ + + MapView(nodes: self.locationNodes)//.environmentObject(userSettings) + //} + .frame(maxHeight: .infinity) + .ignoresSafeArea(.all, edges: [.leading, .trailing]) + } .navigationTitle("Mesh Map") .navigationBarTitleDisplayMode(.inline) .navigationBarItems(trailing: diff --git a/MeshtasticClient/Views/Settings/AppSettings.swift b/MeshtasticClient/Views/Settings/AppSettings.swift index d541e3d8..3710d2eb 100644 --- a/MeshtasticClient/Views/Settings/AppSettings.swift +++ b/MeshtasticClient/Views/Settings/AppSettings.swift @@ -2,6 +2,7 @@ import Foundation import Combine import SwiftUI import SwiftProtobuf +import MapKit enum KeyboardType: Int, CaseIterable, Identifiable { @@ -30,6 +31,28 @@ enum KeyboardType: Int, CaseIterable, Identifiable { } } +enum MeshMapType: String, CaseIterable, Identifiable { + + case satellite = "satellite" + case hybrid = "hybrid" + case standard = "standard" + + var id: String { self.rawValue } + + var description: String { + get { + switch self { + case .satellite: + return "Satellite" + case .standard: + return "Standard" + case .hybrid: + return "Hybrid" + } + } + } +} + class UserSettings: ObservableObject { // @Published var meshtasticUsername: String { // didSet { @@ -61,6 +84,14 @@ class UserSettings: ObservableObject { UserDefaults.standard.set(meshActivityLog, forKey: "meshActivityLog") } } + + @Published var meshMapType: String { + didSet { + UserDefaults.standard.set(meshMapType, forKey: "meshMapType") + } + } + + init() { @@ -70,6 +101,7 @@ class UserSettings: ObservableObject { //self.provideLocation = UserDefaults.standard.object(forKey: "provideLocation") as? Bool ?? false self.keyboardType = UserDefaults.standard.object(forKey: "keyboardType") as? Int ?? 0 self.meshActivityLog = UserDefaults.standard.object(forKey: "meshActivityLog") as? Bool ?? false + self.meshMapType = UserDefaults.standard.string(forKey: "meshMapType") ?? "hybrid" } } @@ -140,6 +172,14 @@ struct AppSettings: View { .listRowSeparator(.visible) } } + Section(header: Text("MAP OPTIONS")) { + Picker("Map Type", selection: $userSettings.meshMapType) { + ForEach(MeshMapType.allCases) { map in + Text(map.description) + } + } + .pickerStyle(DefaultPickerStyle()) + } } } .navigationTitle("App Settings")