diff --git a/Meshtastic Client.xcodeproj/project.pbxproj b/Meshtastic Client.xcodeproj/project.pbxproj index ae67097f..39370ae8 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 */; }; DD23A50F26FD1B4400D9B90C /* PeripheralModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD23A50E26FD1B4400D9B90C /* PeripheralModel.swift */; }; DD2E65262767A01F00E45FC5 /* NodeDetail.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD2E65252767A01F00E45FC5 /* NodeDetail.swift */; }; DD47E3CE26F103C600029299 /* NodeList.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD47E3CD26F103C600029299 /* NodeList.swift */; }; @@ -67,6 +69,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 = ""; }; DD23A50E26FD1B4400D9B90C /* PeripheralModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PeripheralModel.swift; sourceTree = ""; }; DD2E65252767A01F00E45FC5 /* NodeDetail.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeDetail.swift; sourceTree = ""; }; DD47E3CD26F103C600029299 /* NodeList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeList.swift; sourceTree = ""; }; @@ -141,6 +145,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 = ( @@ -261,6 +282,7 @@ DDC2E18726CE24E40042C5E4 /* Views */ = { isa = PBXGroup; children = ( + C9483F6B2773016700998F6B /* Map */, DDC2E18D26CE25CB0042C5E4 /* Helpers */, DD47E3D726F2F21A00029299 /* Bluetooth */, DD47E3CA26F0E50300029299 /* Nodes */, @@ -506,11 +528,13 @@ DDAF8C5D26ED09490058C060 /* portnums.pb.swift in Sources */, DD9D8F2F2764403B00080993 /* Meshtastic.xcdatamodeld in Sources */, DD23A50F26FD1B4400D9B90C /* PeripheralModel.swift in Sources */, + C9A7BC1027759A9600760B50 /* PositionAnnotationView.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 */, 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 0cf93559..5d4768a3 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 new file mode 100644 index 00000000..8b76d074 --- /dev/null +++ b/MeshtasticClient/Views/Map/Custom/PositionAnnotationView.swift @@ -0,0 +1,63 @@ +// +// PositionAnnotationView.swift +// MeshtasticClient +// +// Created by Joshua Pirihi on 24/12/21. +// + +import UIKit +import MapKit + +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" + + var shortName: String? + + // This property defined by `MKAnnotation` is not required. + //var subtitle: String? = NSLocalizedString("SAN_FRANCISCO_SUBTITLE", comment: "SF annotation") +} + +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..34fed2e6 --- /dev/null +++ b/MeshtasticClient/Views/Map/MapView.swift @@ -0,0 +1,96 @@ +// +// MapView.swift +// MeshtasticClient +// +// Created by Joshua Pirihi on 22/12/21. +// + +import Foundation +import UIKit +import MapKit +import SwiftUI +import CoreData + +struct MapView: UIViewRepresentable { + //@Binding var route: MKPolyline? + var nodes: FetchedResults + + let mapViewDelegate = MapViewDelegate() + + func makeUIView(context: Context) -> MKMapView { + let map = MKMapView(frame: .zero) + map.userTrackingMode = .follow + map.mapType = .satellite + 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) + } +} + +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) { + 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 { + let annotation = PositionAnnotation() + annotation.coordinate = (node.positions!.lastObject as! PositionEntity).coordinate ?? CLLocationCoordinate2D(latitude: 0, longitude: 0) + 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 9a76385d..0074f354 100644 --- a/MeshtasticClient/Views/Nodes/NodeMap.swift +++ b/MeshtasticClient/Views/Nodes/NodeMap.swift @@ -51,12 +51,12 @@ struct NodeMap: View { - Map(coordinateRegion: regionBinding, + /*Map(coordinateRegion: regionBinding, interactionModes: [.all], showsUserLocation: true, userTrackingMode: .constant(.follow), annotationItems: self.locationNodes.filter({ nodeinfo in - return nodeinfo.positions != nil && nodeinfo.positions!.count > 0 + return nodeinfo.positions != nil && nodeinfo.positions!.count > 0// && (nodeinfo.positions?.lastObject as? AnyObject)?.coordinate != nil }) ) { locationNode in @@ -67,7 +67,10 @@ struct NodeMap: View { } ) - } + }*/ + + MapView(nodes: self.locationNodes) + //} .frame(maxHeight: .infinity) .ignoresSafeArea(.all, edges: [.leading, .trailing])