mirror of
https://github.com/meshtastic/Meshtastic-Apple.git
synced 2026-04-20 22:13:56 +00:00
Cluster for map pins on the node details view
This commit is contained in:
parent
cd0fdec368
commit
8bc645412b
7 changed files with 144 additions and 266 deletions
|
|
@ -7,7 +7,6 @@
|
|||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
C9483F6D2773017500998F6B /* MapView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9483F6C2773017500998F6B /* MapView.swift */; };
|
||||
C9697F9D279336B700250207 /* LocalMBTileOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9697F9C279336B700250207 /* LocalMBTileOverlay.swift */; };
|
||||
C9697FA527933B8C00250207 /* SQLite in Frameworks */ = {isa = PBXBuildFile; productRef = C9697FA427933B8C00250207 /* SQLite */; };
|
||||
C9A7BC1027759A9600760B50 /* PositionAnnotationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9A7BC0F27759A9600760B50 /* PositionAnnotationView.swift */; };
|
||||
|
|
@ -22,6 +21,7 @@
|
|||
DD23A50F26FD1B4400D9B90C /* PeripheralModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD23A50E26FD1B4400D9B90C /* PeripheralModel.swift */; };
|
||||
DD2553572855B02500E55709 /* LoRaConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD2553562855B02500E55709 /* LoRaConfig.swift */; };
|
||||
DD2553592855B52700E55709 /* PositionConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD2553582855B52700E55709 /* PositionConfig.swift */; };
|
||||
DD2AD8A8296D2DF9001FF0E7 /* MapViewSwiftUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD2AD8A7296D2DF9001FF0E7 /* MapViewSwiftUI.swift */; };
|
||||
DD2E65262767A01F00E45FC5 /* NodeDetail.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD2E65252767A01F00E45FC5 /* NodeDetail.swift */; };
|
||||
DD3501892852FC3B000FC853 /* Settings.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD3501882852FC3B000FC853 /* Settings.swift */; };
|
||||
DD35018B2852FC79000FC853 /* UserSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD35018A2852FC79000FC853 /* UserSettings.swift */; };
|
||||
|
|
@ -127,7 +127,6 @@
|
|||
/* End PBXContainerItemProxy section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
C9483F6C2773017500998F6B /* MapView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapView.swift; sourceTree = "<group>"; };
|
||||
C9697F9C279336B700250207 /* LocalMBTileOverlay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalMBTileOverlay.swift; sourceTree = "<group>"; };
|
||||
C9A7BC0F27759A9600760B50 /* PositionAnnotationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PositionAnnotationView.swift; sourceTree = "<group>"; };
|
||||
C9A88B54278B503C00BD810A /* MapViewModule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MapViewModule.swift; sourceTree = "<group>"; };
|
||||
|
|
@ -141,6 +140,7 @@
|
|||
DD23A50E26FD1B4400D9B90C /* PeripheralModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PeripheralModel.swift; sourceTree = "<group>"; };
|
||||
DD2553562855B02500E55709 /* LoRaConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoRaConfig.swift; sourceTree = "<group>"; };
|
||||
DD2553582855B52700E55709 /* PositionConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PositionConfig.swift; sourceTree = "<group>"; };
|
||||
DD2AD8A7296D2DF9001FF0E7 /* MapViewSwiftUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapViewSwiftUI.swift; sourceTree = "<group>"; };
|
||||
DD2E65252767A01F00E45FC5 /* NodeDetail.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeDetail.swift; sourceTree = "<group>"; };
|
||||
DD3501882852FC3B000FC853 /* Settings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Settings.swift; sourceTree = "<group>"; };
|
||||
DD35018A2852FC79000FC853 /* UserSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSettings.swift; sourceTree = "<group>"; };
|
||||
|
|
@ -270,9 +270,9 @@
|
|||
isa = PBXGroup;
|
||||
children = (
|
||||
C9A7BC0E27759A6800760B50 /* Custom */,
|
||||
C9483F6C2773017500998F6B /* MapView.swift */,
|
||||
C9A88B54278B503C00BD810A /* MapViewModule.swift */,
|
||||
C9697F9C279336B700250207 /* LocalMBTileOverlay.swift */,
|
||||
DD2AD8A7296D2DF9001FF0E7 /* MapViewSwiftUI.swift */,
|
||||
);
|
||||
path = Map;
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -744,6 +744,7 @@
|
|||
DDC4D568275499A500A4208E /* Persistence.swift in Sources */,
|
||||
DD90860E26F69BAE00DC5189 /* NodeMap.swift in Sources */,
|
||||
DD8169FF272476C700F4AB02 /* LogDocument.swift in Sources */,
|
||||
DD2AD8A8296D2DF9001FF0E7 /* MapViewSwiftUI.swift in Sources */,
|
||||
DD6193792863875F00E59241 /* SerialConfig.swift in Sources */,
|
||||
DDA0B6B2294CDC55001356EC /* Channels.swift in Sources */,
|
||||
DDAF8C6926ED0D070058C060 /* deviceonly.pb.swift in Sources */,
|
||||
|
|
@ -783,7 +784,6 @@
|
|||
DD86D40C287F401000BAEB7A /* SaveChannelQRCode.swift in Sources */,
|
||||
DDA1C48E28DB49D3009933EC /* ChannelRoles.swift in Sources */,
|
||||
DD8ED9C8289CE4B900B3B0AB /* RoutingError.swift in Sources */,
|
||||
C9483F6D2773017500998F6B /* MapView.swift in Sources */,
|
||||
DDAF8C5826ED07FD0058C060 /* mesh.pb.swift in Sources */,
|
||||
DD8169FB271F1F3A00F4AB02 /* MeshLog.swift in Sources */,
|
||||
DD8ED9C52898D51F00B3B0AB /* NetworkConfig.swift in Sources */,
|
||||
|
|
@ -1006,7 +1006,7 @@
|
|||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 2.0.10;
|
||||
MARKETING_VERSION = 2.0.11;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SUPPORTS_MACCATALYST = YES;
|
||||
|
|
@ -1039,7 +1039,7 @@
|
|||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 2.0.10;
|
||||
MARKETING_VERSION = 2.0.11;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SUPPORTS_MACCATALYST = YES;
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ extension PositionEntity {
|
|||
return d / 1e7
|
||||
}
|
||||
|
||||
var coordinate: CLLocationCoordinate2D? {
|
||||
var nodeCoordinate: CLLocationCoordinate2D? {
|
||||
if latitudeI != 0 && longitudeI != 0 {
|
||||
let coord = CLLocationCoordinate2D(latitude: latitude!, longitude: longitude!)
|
||||
return coord
|
||||
|
|
@ -31,12 +31,17 @@ extension PositionEntity {
|
|||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var annotaton: MKPointAnnotation {
|
||||
let pointAnn = MKPointAnnotation()
|
||||
if coordinate != nil {
|
||||
pointAnn.coordinate = coordinate!
|
||||
if nodeCoordinate != nil {
|
||||
pointAnn.coordinate = nodeCoordinate!
|
||||
}
|
||||
return pointAnn
|
||||
}
|
||||
}
|
||||
|
||||
extension PositionEntity: MKAnnotation {
|
||||
public var coordinate: CLLocationCoordinate2D { nodeCoordinate! }
|
||||
public var subtitle: String? { time?.formatted() }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,173 +0,0 @@
|
|||
//
|
||||
// MapView.swift
|
||||
// MeshtasticApple
|
||||
//
|
||||
// Created by Joshua Pirihi on 22/12/21.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
import MapKit
|
||||
import SwiftUI
|
||||
import CoreData
|
||||
#if false
|
||||
// wrap a MKMapView into something we can use in SwiftUI
|
||||
struct MapView: UIViewRepresentable {
|
||||
|
||||
var nodes: FetchedResults<NodeInfoEntity>
|
||||
|
||||
var 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)
|
||||
self.showNodePositions(to: map)
|
||||
self.moveToMeshRegion(in: map)
|
||||
|
||||
map.register(PositionAnnotationView.self, forAnnotationViewWithReuseIdentifier: NSStringFromClass(PositionAnnotationView.self))
|
||||
|
||||
let overlay = MKTileOverlay(urlTemplate: //"http://tiles-a.data-cdn.linz.govt.nz/services;key=7fa19132d53240708c4ff436df5b9800/tiles/v4/layer=50767/EPSG:3857/{z}/{x}/{y}.png")
|
||||
"http://10.147.253.250:5050/local/map/{z}/{x}/{y}.png")
|
||||
overlay.canReplaceMapContent = true
|
||||
self.mapViewDelegate.renderer = MKTileOverlayRenderer(tileOverlay: overlay)
|
||||
map.addOverlay(overlay)
|
||||
|
||||
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)
|
||||
|
||||
//if (self.needToMoveToMeshRegion) {
|
||||
// self.moveToMeshRegion(in: view)
|
||||
// self.needToMoveToMeshRegion = false
|
||||
//}
|
||||
}
|
||||
|
||||
func moveToMeshRegion(in mapView: MKMapView) {
|
||||
//go through the annotations and create a bounding box that encloses them
|
||||
|
||||
var minLat: CLLocationDegrees = 90.0
|
||||
var maxLat: CLLocationDegrees = -90.0
|
||||
var minLon: CLLocationDegrees = 180.0
|
||||
var maxLon: CLLocationDegrees = -180.0
|
||||
|
||||
for annotation in mapView.annotations {
|
||||
if annotation.isKind(of: PositionAnnotation.self) {
|
||||
minLat = min(minLat, annotation.coordinate.latitude)
|
||||
maxLat = max(maxLat, annotation.coordinate.latitude)
|
||||
minLon = min(minLon, annotation.coordinate.longitude)
|
||||
maxLon = max(maxLon, annotation.coordinate.longitude)
|
||||
}
|
||||
}
|
||||
|
||||
//check if the mesh region looks sensible before we move to it. Otherwise we won't move the map (leave it at the current location)
|
||||
if maxLat < minLat || (maxLat-minLat) > 5 || maxLon < minLon || (maxLon-minLon) > 5 {
|
||||
return
|
||||
}
|
||||
|
||||
let centerCoord = CLLocationCoordinate2D(latitude: (minLat+maxLat)/2, longitude: (minLon+maxLon)/2)
|
||||
|
||||
let span = MKCoordinateSpan(latitudeDelta: (maxLat-minLat)*1.5, longitudeDelta: (maxLon-minLon)*1.5)
|
||||
|
||||
let region = mapView.regionThatFits(MKCoordinateRegion(center: centerCoord, span: span))
|
||||
|
||||
mapView.setRegion(region, animated: true)
|
||||
|
||||
|
||||
}
|
||||
|
||||
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 ?? NSLocalizedString("unknown", comment: "Unknown")
|
||||
annotation.shortName = node.user?.shortName?.uppercased() ?? "???"
|
||||
|
||||
view.addAnnotation(annotation)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class MapViewDelegate: NSObject, MKMapViewDelegate {
|
||||
|
||||
var renderer: MKTileOverlayRenderer?
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
|
||||
return self.renderer!
|
||||
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
|
@ -223,7 +223,7 @@ public struct MapView: UIViewRepresentable {
|
|||
}
|
||||
|
||||
let annotation = PositionAnnotation()
|
||||
annotation.coordinate = position.coordinate!
|
||||
annotation.coordinate = position.nodeCoordinate!
|
||||
annotation.title = position.nodePosition!.user?.longName ?? NSLocalizedString("unknown", comment: "Unknown")
|
||||
annotation.shortName = position.nodePosition!.user?.shortName?.uppercased() ?? "???"
|
||||
|
||||
|
|
|
|||
57
Meshtastic/Views/Map/MapViewSwiftUI.swift
Normal file
57
Meshtastic/Views/Map/MapViewSwiftUI.swift
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
//
|
||||
// MapViewSwitUI.swift
|
||||
// Meshtastic
|
||||
//
|
||||
// Copyright(c) Garth Vander Houwen 1/9/23.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import MapKit
|
||||
|
||||
struct MapViewSwiftUI: UIViewRepresentable {
|
||||
|
||||
let positions: [PositionEntity]
|
||||
let region: MKCoordinateRegion
|
||||
let mapViewType: MKMapType
|
||||
|
||||
func makeUIView(context: Context) -> MKMapView {
|
||||
let mapView = MKMapView()
|
||||
mapView.mapType = mapViewType
|
||||
mapView.setRegion(region, animated: true)
|
||||
mapView.isRotateEnabled = true
|
||||
mapView.addAnnotations(positions)
|
||||
mapView.delegate = context.coordinator
|
||||
return mapView
|
||||
}
|
||||
|
||||
func updateUIView(_ mapView: MKMapView, context: Context) {
|
||||
mapView.mapType = mapViewType
|
||||
}
|
||||
|
||||
func makeCoordinator() -> MapCoordinator {
|
||||
.init()
|
||||
}
|
||||
|
||||
final class MapCoordinator: NSObject, MKMapViewDelegate {
|
||||
|
||||
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
|
||||
|
||||
switch annotation {
|
||||
|
||||
case _ as MKClusterAnnotation:
|
||||
let annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: "nodeGroup") as? MKMarkerAnnotationView ?? MKMarkerAnnotationView(annotation: annotation, reuseIdentifier: "nodeGroup")
|
||||
annotationView.markerTintColor = .darkGray
|
||||
return annotationView
|
||||
case _ as PositionEntity:
|
||||
let annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: "node") as? MKMarkerAnnotationView ?? MKMarkerAnnotationView(annotation: annotation, reuseIdentifier: "Node")
|
||||
annotationView.canShowCallout = true
|
||||
annotationView.glyphText = "📟"
|
||||
annotationView.clusteringIdentifier = "nodeGroup"
|
||||
annotationView.markerTintColor = UIColor(.accentColor)
|
||||
annotationView.titleVisibility = .visible
|
||||
return annotationView
|
||||
default: return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,73 +1,62 @@
|
|||
/*
|
||||
Abstract:
|
||||
A view showing the details for a node.
|
||||
*/
|
||||
Abstract:
|
||||
A view showing the details for a node.
|
||||
*/
|
||||
|
||||
import SwiftUI
|
||||
import MapKit
|
||||
import CoreLocation
|
||||
|
||||
struct NodeDetail: View {
|
||||
|
||||
|
||||
@Environment(\.managedObjectContext) var context
|
||||
@EnvironmentObject var bleManager: BLEManager
|
||||
|
||||
@State private var mapType: MKMapType = .standard
|
||||
|
||||
@State private var showingDetailsPopover = false
|
||||
|
||||
@State var satsInView = 0
|
||||
@State private var showingShutdownConfirm: Bool = false
|
||||
@State private var showingRebootConfirm: Bool = false
|
||||
|
||||
|
||||
var node: NodeInfoEntity
|
||||
|
||||
|
||||
var body: some View {
|
||||
|
||||
let hwModelString = node.user?.hwModel ?? "UNSET"
|
||||
|
||||
|
||||
NavigationStack {
|
||||
GeometryReader { bounds in
|
||||
VStack {
|
||||
if node.positions?.count ?? 0 > 0 {
|
||||
let mostRecent = node.positions?.lastObject as! PositionEntity
|
||||
if mostRecent.coordinate != nil {
|
||||
let nodeCoordinatePosition = CLLocationCoordinate2D(latitude: mostRecent.latitude!, longitude: mostRecent.longitude!)
|
||||
|
||||
let regionBinding = Binding<MKCoordinateRegion>(
|
||||
get: {
|
||||
MKCoordinateRegion(center: nodeCoordinatePosition, span: MKCoordinateSpan(latitudeDelta: 0.005, longitudeDelta: 0.005))
|
||||
},
|
||||
set: { _ in }
|
||||
)
|
||||
let nodeCoordinatePosition = CLLocationCoordinate2D(latitude: mostRecent.latitude!, longitude: mostRecent.longitude!)
|
||||
ZStack {
|
||||
let annotations = node.positions?.array as! [PositionEntity]
|
||||
ZStack {
|
||||
let annotations = node.positions?.array as! [PositionEntity]
|
||||
|
||||
Map(coordinateRegion: regionBinding,
|
||||
interactionModes: [.all],
|
||||
showsUserLocation: true,
|
||||
userTrackingMode: .constant(.follow),
|
||||
annotationItems: annotations) { location in
|
||||
MapViewSwiftUI(positions: annotations, region: MKCoordinateRegion(center: nodeCoordinatePosition, span: MKCoordinateSpan(latitudeDelta: 0.005, longitudeDelta: 0.005)), mapViewType: mapType)
|
||||
VStack {
|
||||
Spacer()
|
||||
Text(mostRecent.satsInView > 0 ? "Sats: \(mostRecent.satsInView)" : " ")
|
||||
.font(.caption2)
|
||||
|
||||
return MapAnnotation(
|
||||
coordinate: location.coordinate ?? CLLocationCoordinate2D(latitude: 0, longitude: 0),
|
||||
content: {
|
||||
|
||||
NodeAnnotation(time: location.time!)
|
||||
}
|
||||
)
|
||||
Picker("", selection: $mapType) {
|
||||
Text("Standard").tag(MKMapType.standard)
|
||||
Text("Hybrid").tag(MKMapType.hybrid)
|
||||
Text("Satellite").tag(MKMapType.satellite)
|
||||
}
|
||||
.pickerStyle(SegmentedPickerStyle())
|
||||
}
|
||||
.ignoresSafeArea(.all, edges: [.leading, .trailing])
|
||||
.frame(idealWidth: bounds.size.width, minHeight: bounds.size.height / 2)
|
||||
|
||||
}
|
||||
Text(mostRecent.satsInView > 0 ? "Sats: \(mostRecent.satsInView)" : " ")
|
||||
.offset( y:-40)
|
||||
.ignoresSafeArea(.all, edges: [.top, .leading, .trailing])
|
||||
.frame(idealWidth: bounds.size.width, minHeight: bounds.size.height / 1.65)
|
||||
}
|
||||
|
||||
} else {
|
||||
HStack {
|
||||
|
||||
|
||||
}
|
||||
.padding([.top], 60)
|
||||
.padding([.top], 20)
|
||||
}
|
||||
|
||||
ScrollView {
|
||||
|
|
@ -80,13 +69,13 @@ struct NodeDetail: View {
|
|||
Divider()
|
||||
VStack {
|
||||
if node.user != nil {
|
||||
|
||||
|
||||
Image(hwModelString)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fill)
|
||||
.frame(width: 100, height: 100)
|
||||
.cornerRadius(5)
|
||||
|
||||
|
||||
Text(String(hwModelString))
|
||||
.foregroundColor(.gray)
|
||||
.font(.largeTitle).fixedSize()
|
||||
|
|
@ -96,7 +85,7 @@ struct NodeDetail: View {
|
|||
if node.snr > 0 {
|
||||
Divider()
|
||||
VStack(alignment: .center) {
|
||||
|
||||
|
||||
Image(systemName: "waveform.path")
|
||||
.font(.title)
|
||||
.foregroundColor(.accentColor)
|
||||
|
|
@ -109,15 +98,15 @@ struct NodeDetail: View {
|
|||
.fixedSize()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if node.telemetries?.count ?? 0 >= 1 {
|
||||
|
||||
|
||||
let mostRecent = node.telemetries?.lastObject as! TelemetryEntity
|
||||
Divider()
|
||||
VStack(alignment: .center) {
|
||||
BatteryGauge(batteryLevel: Double(mostRecent.batteryLevel))
|
||||
if mostRecent.voltage > 0 {
|
||||
|
||||
|
||||
Text(String(format: "%.2f", mostRecent.voltage) + " V")
|
||||
.font(.title)
|
||||
.foregroundColor(.gray)
|
||||
|
|
@ -147,7 +136,7 @@ struct NodeDetail: View {
|
|||
Divider()
|
||||
VStack {
|
||||
HStack {
|
||||
Image(systemName: "number")
|
||||
Image(systemName: "number")
|
||||
.font(.title2)
|
||||
.foregroundColor(.accentColor)
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
|
|
@ -160,8 +149,8 @@ struct NodeDetail: View {
|
|||
HStack {
|
||||
Image(systemName: "globe")
|
||||
.font(.title)
|
||||
.foregroundColor(.accentColor)
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
.foregroundColor(.accentColor)
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
Text("MAC Address: ").font(.title)
|
||||
|
||||
}
|
||||
|
|
@ -174,8 +163,8 @@ struct NodeDetail: View {
|
|||
HStack {
|
||||
Image(systemName: "clock.badge.checkmark.fill")
|
||||
.font(.title)
|
||||
.foregroundColor(.accentColor)
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
.foregroundColor(.accentColor)
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
Text("heard.last").font(.title)+Text(":").font(.title)
|
||||
|
||||
}
|
||||
|
|
@ -190,7 +179,7 @@ struct NodeDetail: View {
|
|||
} else {
|
||||
|
||||
HStack {
|
||||
|
||||
|
||||
VStack(alignment: .center) {
|
||||
CircleText(text: node.user?.shortName ?? "???", color: .accentColor)
|
||||
}
|
||||
|
|
@ -210,7 +199,7 @@ struct NodeDetail: View {
|
|||
if node.snr > 0 {
|
||||
Divider()
|
||||
VStack(alignment: .center) {
|
||||
|
||||
|
||||
Image(systemName: "waveform.path")
|
||||
.font(.title)
|
||||
.foregroundColor(.accentColor)
|
||||
|
|
@ -223,15 +212,15 @@ struct NodeDetail: View {
|
|||
}
|
||||
.padding(5)
|
||||
}
|
||||
|
||||
|
||||
if node.telemetries?.count ?? 0 >= 1 {
|
||||
|
||||
|
||||
let mostRecent = node.telemetries?.lastObject as! TelemetryEntity
|
||||
|
||||
|
||||
Divider()
|
||||
|
||||
|
||||
VStack(alignment: .center) {
|
||||
|
||||
|
||||
BatteryGauge(batteryLevel: Double(mostRecent.batteryLevel))
|
||||
|
||||
if mostRecent.voltage > 0 {
|
||||
|
|
@ -262,7 +251,7 @@ struct NodeDetail: View {
|
|||
Divider()
|
||||
VStack {
|
||||
HStack {
|
||||
Image(systemName: "number")
|
||||
Image(systemName: "number")
|
||||
.font(.title2)
|
||||
.foregroundColor(.accentColor)
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
|
|
@ -275,9 +264,9 @@ struct NodeDetail: View {
|
|||
Divider()
|
||||
HStack {
|
||||
Image(systemName: "globe")
|
||||
.font(.headline)
|
||||
.foregroundColor(.accentColor)
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
.font(.headline)
|
||||
.foregroundColor(.accentColor)
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
Text("MAC Address: ")
|
||||
Text(String(node.user?.macaddr?.macAddressString ?? "not a valid mac address")).foregroundColor(.gray)
|
||||
}
|
||||
|
|
@ -334,16 +323,16 @@ struct NodeDetail: View {
|
|||
}
|
||||
|
||||
if self.bleManager.connectedPeripheral != nil && self.bleManager.connectedPeripheral.num == node.num && self.bleManager.connectedPeripheral.num == node.num {
|
||||
|
||||
|
||||
HStack {
|
||||
|
||||
if hwModelString == "TBEAM" || hwModelString == "TECHO" || hwModelString.contains("4631") {
|
||||
|
||||
|
||||
Button(action: {
|
||||
|
||||
showingShutdownConfirm = true
|
||||
}) {
|
||||
|
||||
|
||||
Label("Power Off", systemImage: "power")
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
|
|
@ -361,13 +350,13 @@ struct NodeDetail: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Button(action: {
|
||||
|
||||
showingRebootConfirm = true
|
||||
|
||||
}) {
|
||||
|
||||
|
||||
Label("reboot", systemImage: "arrow.triangle.2.circlepath")
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
|
|
@ -376,31 +365,31 @@ struct NodeDetail: View {
|
|||
.padding()
|
||||
.confirmationDialog("are.you.sure",
|
||||
|
||||
isPresented: $showingRebootConfirm
|
||||
) {
|
||||
Button("reboot.node", role: .destructive) {
|
||||
|
||||
if !bleManager.sendReboot(fromUser: node.user!, toUser: node.user!) {
|
||||
print("Reboot Failed")
|
||||
}
|
||||
}
|
||||
isPresented: $showingRebootConfirm
|
||||
) {
|
||||
Button("reboot.node", role: .destructive) {
|
||||
|
||||
if !bleManager.sendReboot(fromUser: node.user!, toUser: node.user!) {
|
||||
print("Reboot Failed")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(5)
|
||||
}
|
||||
}
|
||||
.offset( y:-40)
|
||||
//.offset( y:-40)
|
||||
}
|
||||
.edgesIgnoringSafeArea([.leading, .trailing])
|
||||
.navigationBarTitle(String(node.user?.longName ?? NSLocalizedString("unknown", comment: "")), displayMode: .inline)
|
||||
.padding(.bottom, 10)
|
||||
.navigationBarItems(trailing:
|
||||
ZStack {
|
||||
ConnectedDevice(
|
||||
bluetoothOn: bleManager.isSwitchedOn,
|
||||
deviceConnected: bleManager.connectedPeripheral != nil,
|
||||
name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "????")
|
||||
}
|
||||
ZStack {
|
||||
ConnectedDevice(
|
||||
bluetoothOn: bleManager.isSwitchedOn,
|
||||
deviceConnected: bleManager.connectedPeripheral != nil,
|
||||
name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "????")
|
||||
}
|
||||
)
|
||||
.onAppear {
|
||||
self.bleManager.context = context
|
||||
|
|
|
|||
|
|
@ -53,8 +53,8 @@ struct NodeList: View {
|
|||
HStack(alignment: .bottom) {
|
||||
let lastPostion = node.positions!.reversed()[0] as! PositionEntity
|
||||
let myCoord = CLLocation(latitude: LocationHelper.currentLocation.latitude, longitude: LocationHelper.currentLocation.longitude)
|
||||
if lastPostion.coordinate != nil && myCoord.coordinate.longitude != LocationHelper.DefaultLocation.longitude && myCoord.coordinate.latitude != LocationHelper.DefaultLocation.latitude {
|
||||
let nodeCoord = CLLocation(latitude: lastPostion.coordinate!.latitude, longitude: lastPostion.coordinate!.longitude)
|
||||
if lastPostion.nodeCoordinate != nil && myCoord.coordinate.longitude != LocationHelper.DefaultLocation.longitude && myCoord.coordinate.latitude != LocationHelper.DefaultLocation.latitude {
|
||||
let nodeCoord = CLLocation(latitude: lastPostion.nodeCoordinate!.latitude, longitude: lastPostion.nodeCoordinate!.longitude)
|
||||
let metersAway = nodeCoord.distance(from: myCoord)
|
||||
Image(systemName: "lines.measurement.horizontal")
|
||||
.font(.title3)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue