diff --git a/Meshtastic Client.xcodeproj/project.xcworkspace/xcuserdata/joshuapirihi.xcuserdatad/UserInterfaceState.xcuserstate b/Meshtastic Client.xcodeproj/project.xcworkspace/xcuserdata/joshuapirihi.xcuserdatad/UserInterfaceState.xcuserstate index 5d4768a3..c864b6bc 100644 Binary files a/Meshtastic Client.xcodeproj/project.xcworkspace/xcuserdata/joshuapirihi.xcuserdatad/UserInterfaceState.xcuserstate 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 index 8b76d074..ea2005dd 100644 --- a/MeshtasticClient/Views/Map/Custom/PositionAnnotationView.swift +++ b/MeshtasticClient/Views/Map/Custom/PositionAnnotationView.swift @@ -8,18 +8,19 @@ 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` - var title: String? = "Title" + //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? - // This property defined by `MKAnnotation` is not required. - //var subtitle: String? = NSLocalizedString("SAN_FRANCISCO_SUBTITLE", comment: "SF annotation") } class PositionAnnotationView: MKAnnotationView { diff --git a/MeshtasticClient/Views/Map/MapView.swift b/MeshtasticClient/Views/Map/MapView.swift index 34fed2e6..ebdd6742 100644 --- a/MeshtasticClient/Views/Map/MapView.swift +++ b/MeshtasticClient/Views/Map/MapView.swift @@ -11,49 +11,73 @@ import MapKit import SwiftUI import CoreData +//wrap a MKMapView into something we can use in SwiftUI struct MapView: UIViewRepresentable { - //@Binding var route: MKPolyline? + 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 - map.mapType = .satellite + + 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 - //addRoute(to: view) - showNodePositions(to: view) + + 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 addRoute(to view: MKMapView) { - // if !view.overlays.isEmpty { - // view.removeOverlays(view.overlays) - // } - //guard let route = route else { return } - //let mapRect = route.boundingMapRect - //view.setVisibleMapRect(mapRect, edgePadding: UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10), animated: true) - //view.addOverlay(route) - //} 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 { + if (node.positions?.count ?? 0) > 0 && (node.positions!.lastObject as! PositionEntity).coordinate != nil { let annotation = PositionAnnotation() - annotation.coordinate = (node.positions!.lastObject as! PositionEntity).coordinate ?? CLLocationCoordinate2D(latitude: 0, longitude: 0) + annotation.coordinate = (node.positions!.lastObject as! PositionEntity).coordinate! annotation.title = node.user?.longName ?? "Unknown" annotation.shortName = node.user?.shortName?.uppercased() ?? "???" diff --git a/MeshtasticClient/Views/Nodes/NodeMap.swift b/MeshtasticClient/Views/Nodes/NodeMap.swift index 88211a53..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]() @@ -72,8 +76,7 @@ struct NodeMap: View { }*/ - MapView(nodes: self.locationNodes) - + MapView(nodes: self.locationNodes)//.environmentObject(userSettings) //} .frame(maxHeight: .infinity) .ignoresSafeArea(.all, edges: [.leading, .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")