mirror of
https://github.com/meshtastic/Meshtastic-Apple.git
synced 2026-04-20 22:13:56 +00:00
Merge pull request #18 from garthvh/jpirihi-dev
Improved annotations for node map multiple map types
This commit is contained in:
commit
d807c11c5d
7 changed files with 285 additions and 12 deletions
4
.gitignore
vendored
4
.gitignore
vendored
|
|
@ -1 +1,5 @@
|
|||
.DS_Store
|
||||
Build
|
||||
*xcuserdata*
|
||||
*xcscmblueprint*
|
||||
nRF Toolbox.xcworkspace/xcshareddata
|
||||
|
|
|
|||
|
|
@ -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 = "<group>"; };
|
||||
C9A7BC0F27759A9600760B50 /* PositionAnnotationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PositionAnnotationView.swift; sourceTree = "<group>"; };
|
||||
DD1BF2F82776FE2E008C8D2F /* UserMessageList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserMessageList.swift; sourceTree = "<group>"; };
|
||||
DD23A50E26FD1B4400D9B90C /* PeripheralModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PeripheralModel.swift; sourceTree = "<group>"; };
|
||||
DD2E65252767A01F00E45FC5 /* NodeDetail.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeDetail.swift; sourceTree = "<group>"; };
|
||||
|
|
@ -145,6 +149,23 @@
|
|||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
C9483F6B2773016700998F6B /* Map */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
C9A7BC0E27759A6800760B50 /* Custom */,
|
||||
C9483F6C2773017500998F6B /* MapView.swift */,
|
||||
);
|
||||
path = Map;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
C9A7BC0E27759A6800760B50 /* Custom */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
C9A7BC0F27759A9600760B50 /* PositionAnnotationView.swift */,
|
||||
);
|
||||
path = Custom;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
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;
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -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)
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
120
MeshtasticClient/Views/Map/MapView.swift
Normal file
120
MeshtasticClient/Views/Map/MapView.swift
Normal file
|
|
@ -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<NodeInfoEntity>
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
@ -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<NodeInfoEntity>
|
||||
|
||||
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:
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue