diff --git a/Meshtastic/Views/Map/Custom/MapViewSwiftUI.swift b/Meshtastic/Views/Map/Custom/MapViewSwiftUI.swift index 2fb8b422..91f841b1 100644 --- a/Meshtastic/Views/Map/Custom/MapViewSwiftUI.swift +++ b/Meshtastic/Views/Map/Custom/MapViewSwiftUI.swift @@ -12,38 +12,38 @@ func degreesToRadians(_ number: Double) -> Double { } struct MapViewSwiftUI: UIViewRepresentable { - + var onLongPress: (_ waypointCoordinate: CLLocationCoordinate2D) -> Void var onWaypointEdit: (_ waypointId: Int ) -> Void let mapView = MKMapView() + let colors: [UIColor] = [UIColor.systemIndigo, UIColor.orange, UIColor.green, UIColor.brown, UIColor.purple, UIColor.systemMint, UIColor.cyan, UIColor.magenta, UIColor.systemPink, UIColor.blue] + // Parameters let positions: [PositionEntity] let waypoints: [WaypointEntity] let mapViewType: MKMapType let userTrackingMode: MKUserTrackingMode let showNodeHistory: Bool let showRouteLines: Bool - let colors: [UIColor] = [UIColor.systemIndigo, UIColor.orange, UIColor.green, UIColor.brown, UIColor.purple, UIColor.systemMint, UIColor.cyan, UIColor.magenta, UIColor.systemPink, UIColor.blue] - @AppStorage("meshMapRecentering") private var recenter: Bool = false - // Offline Maps - // make this view dependent on the UserDefault that is updated when importing a new map file + // Offline Map Tiles @AppStorage("lastUpdatedLocalMapFile") private var lastUpdatedLocalMapFile = 0 @State private var loadedLastUpdatedLocalMapFile = 0 var customMapOverlay: CustomMapOverlay? @State private var presentCustomMapOverlayHash: CustomMapOverlay? - func makeUIView(context: Context) -> MKMapView { // Map View Parameters mapView.mapType = mapViewType mapView.addAnnotations(waypoints) // Do the initial map centering + let latest = positions + .filter { $0.latest == true } + .sorted { $0.nodePosition?.num ?? 0 > $1.nodePosition?.num ?? -1 } let span = MKCoordinateSpan(latitudeDelta: 0.003, longitudeDelta: 0.003) - let center = LocationHelper.currentLocation + let center = (latest.count > 0 && userTrackingMode == MKUserTrackingMode.none) ? latest[0].coordinate : LocationHelper.currentLocation let region = MKCoordinateRegion(center: center, span: span) mapView.setRegion(region, animated: true) // Set user (phone gps) tracking options - let latest = positions.filter { $0.latest == true } mapView.setUserTrackingMode(userTrackingMode, animated: true) if userTrackingMode == MKUserTrackingMode.none { mapView.showsUserLocation = false @@ -51,6 +51,15 @@ struct MapViewSwiftUI: UIViewRepresentable { mapView.showsUserLocation = true } mapView.addAnnotations(showNodeHistory ? positions : latest) + // Set user (phone gps) tracking options + mapView.setUserTrackingMode(userTrackingMode, animated: true) + if userTrackingMode == MKUserTrackingMode.none { + mapView.fitAllAnnotations() + mapView.showsUserLocation = false + } else { + mapView.showsUserLocation = true + } + mapView.addAnnotations(showNodeHistory ? positions : latest) // Other MKMapView Settings mapView.preferredConfiguration.elevationStyle = .realistic// .flat mapView.isPitchEnabled = true @@ -60,14 +69,14 @@ struct MapViewSwiftUI: UIViewRepresentable { mapView.showsBuildings = true mapView.showsScale = true mapView.showsTraffic = true - + #if targetEnvironment(macCatalyst) // Show the default always visible compass and the mac only controls mapView.showsCompass = true mapView.showsZoomControls = true mapView.showsPitchControl = true #else - + #if os(iOS) // Hide the default compass that only appears when you are not going north and instead always show the compass in the bottom right corner of the map mapView.showsCompass = false @@ -78,19 +87,19 @@ struct MapViewSwiftUI: UIViewRepresentable { compassButton.trailingAnchor.constraint(equalTo: mapView.trailingAnchor, constant: -5).isActive = true compassButton.bottomAnchor.constraint(equalTo: mapView.bottomAnchor, constant: -25).isActive = true #endif - + #endif mapView.delegate = context.coordinator return mapView } - + func updateUIView(_ mapView: MKMapView, context: Context) { mapView.mapType = mapViewType - + if self.customMapOverlay != self.presentCustomMapOverlayHash || self.loadedLastUpdatedLocalMapFile != self.lastUpdatedLocalMapFile { mapView.removeOverlays(mapView.overlays) if self.customMapOverlay != nil { - + let fileManager = FileManager.default let documentsDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first! let tilePath = documentsDirectory.appendingPathComponent("offline_map.mbtiles", isDirectory: false).path @@ -109,40 +118,38 @@ struct MapViewSwiftUI: UIViewRepresentable { self.loadedLastUpdatedLocalMapFile = self.lastUpdatedLocalMapFile } } - + DispatchQueue.main.async { let latest = positions .filter { $0.latest == true } .sorted { $0.nodePosition?.num ?? 0 > $1.nodePosition?.num ?? -1 } - - if showRouteLines { - // Remove all existing PolyLine Overlays - for overlay in mapView.overlays { - if overlay is MKPolyline { - mapView.removeOverlay(overlay) + let annotationCount = waypoints.count + (showNodeHistory || showRouteLines ? positions.count : latest.count) + if annotationCount > mapView.annotations.count { + if showRouteLines { + // Remove all existing PolyLine Overlays + for overlay in mapView.overlays { + if overlay is MKPolyline { + mapView.removeOverlay(overlay) + } + } + var lineIndex = 0 + for position in latest { + + let nodePositions = positions.filter { $0.time! >= Calendar.current.startOfDay(for: Date()) && $0.nodePosition?.num ?? 0 == position.nodePosition?.num ?? -1 } + let lineCoords = nodePositions.map ({ + (position) -> CLLocationCoordinate2D in + return position.nodeCoordinate! + }) + let polyline = MKPolyline(coordinates: lineCoords, count: nodePositions.count) + polyline.title = "\(String(position.nodePosition?.num ?? 0))-\(String(lineIndex))" + mapView.addOverlay(polyline) + lineIndex += 1 + // There are 10 colors for lines, start over if we are at index 10 + if lineIndex > 9 { + lineIndex = 0 + } } } - var lineIndex = 0 - for position in latest { - - let nodePositions = positions.filter { $0.time! >= Calendar.current.startOfDay(for: Date()) && $0.nodePosition?.num ?? 0 == position.nodePosition?.num ?? -1 } - let lineCoords = nodePositions.map ({ - (position) -> CLLocationCoordinate2D in - return position.nodeCoordinate! - }) - let polyline = MKPolyline(coordinates: lineCoords, count: nodePositions.count) - polyline.title = "\(String(position.nodePosition?.num ?? 0))-\(String(lineIndex))" - mapView.addOverlay(polyline) - lineIndex += 1 - // There are 10 colors for lines, start over if we are at index 10 - if lineIndex > 9 { - lineIndex = 0 - } - } - } - - let annotationCount = waypoints.count + positions.count - if annotationCount != mapView.annotations.count { mapView.removeAnnotations(mapView.annotations) mapView.addAnnotations(waypoints) mapView.setUserTrackingMode(userTrackingMode, animated: true) @@ -152,7 +159,7 @@ struct MapViewSwiftUI: UIViewRepresentable { mapView.addAnnotations(showNodeHistory ? positions : latest) if recenter { if showRouteLines || showNodeHistory { - mapView.fit(annotations: showNodeHistory ? positions : positions, andShow: false) + mapView.fit(annotations: showNodeHistory ? positions : latest, andShow: false) } else { mapView.fitAllAnnotations() } @@ -165,16 +172,16 @@ struct MapViewSwiftUI: UIViewRepresentable { } } } - + func makeCoordinator() -> MapCoordinator { return Coordinator(self) } - + final class MapCoordinator: NSObject, MKMapViewDelegate, UIGestureRecognizerDelegate { - + var parent: MapViewSwiftUI var longPressRecognizer = UILongPressGestureRecognizer() - + init(_ parent: MapViewSwiftUI) { self.parent = parent super.init() @@ -184,16 +191,16 @@ struct MapViewSwiftUI: UIViewRepresentable { self.longPressRecognizer.delegate = self self.parent.mapView.addGestureRecognizer(longPressRecognizer) } - + func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? { - + switch annotation { case let positionAnnotation as PositionEntity: let reuseID = String(positionAnnotation.nodePosition?.num ?? 0) + "-" + String(positionAnnotation.time?.timeIntervalSince1970 ?? 0) let annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: "node") as? MKMarkerAnnotationView ?? MKMarkerAnnotationView(annotation: annotation, reuseIdentifier: reuseID ) annotationView.tag = -1 annotationView.canShowCallout = true - + if positionAnnotation.latest { annotationView.markerTintColor = .systemRed annotationView.displayPriority = .required @@ -216,7 +223,7 @@ struct MapViewSwiftUI: UIViewRepresentable { let distanceFormatter = MKDistanceFormatter() subtitle.text! += "Altitude: \(distanceFormatter.string(fromDistance: Double(positionAnnotation.altitude))) \n" if positionAnnotation.nodePosition?.metadata != nil { - + if DeviceRoles(rawValue: Int(positionAnnotation.nodePosition!.metadata?.role ?? 0)) == DeviceRoles.client || DeviceRoles(rawValue: Int(positionAnnotation.nodePosition!.metadata?.role ?? 0)) == DeviceRoles.clientMute || DeviceRoles(rawValue: Int(positionAnnotation.nodePosition!.metadata?.role ?? 0)) == DeviceRoles.routerClient { @@ -230,7 +237,7 @@ struct MapViewSwiftUI: UIViewRepresentable { } else if DeviceRoles(rawValue: Int(positionAnnotation.nodePosition!.metadata?.role ?? 0)) == DeviceRoles.sensor { annotationView.glyphImage = UIImage(systemName: "sensor") } - + let pf = PositionFlags(rawValue: Int(positionAnnotation.nodePosition?.metadata?.positionFlags ?? 3)) if pf.contains(.Satsinview) { subtitle.text! += "Sats in view: \(String(positionAnnotation.satsInView)) \n" @@ -239,7 +246,7 @@ struct MapViewSwiftUI: UIViewRepresentable { subtitle.text! += "Sequence: \(String(positionAnnotation.seqNo)) \n" } if pf.contains(.Heading) { - + if parent.userTrackingMode != MKUserTrackingMode.followWithHeading { annotationView.glyphImage = UIImage(systemName: "location.north.fill")?.rotate(radians: Float(degreesToRadians(Double(positionAnnotation.heading)))) subtitle.text! += "Heading: \(String(positionAnnotation.heading)) \n" @@ -255,7 +262,7 @@ struct MapViewSwiftUI: UIViewRepresentable { } subtitle.text! += "Speed: \(formatter.string(from: Measurement(value: Double(positionAnnotation.speed), unit: UnitSpeed.kilometersPerHour))) \n" } - + } else { // node metadata is nil annotationView.glyphImage = UIImage(systemName: "flipphone") @@ -316,23 +323,23 @@ struct MapViewSwiftUI: UIViewRepresentable { default: return nil } } - + func mapView(_ mapView: MKMapView, annotationView view: MKAnnotationView, calloutAccessoryControlTapped control: UIControl) { // Only Allow Edit for waypoint annotations with a id if view.tag > 0 { parent.onWaypointEdit(view.tag) } } - + @objc func longPressHandler(_ gesture: UILongPressGestureRecognizer) { - + if gesture.state != UIGestureRecognizer.State.ended { return } else if gesture.state != UIGestureRecognizer.State.began { - + // Screen Position - CGPoint let location = longPressRecognizer.location(in: self.parent.mapView) - + // Map Coordinate - CLLocationCoordinate2D let coordinate = self.parent.mapView.convert(location, toCoordinateFrom: self.parent.mapView) let annotation = MKPointAnnotation() @@ -343,9 +350,9 @@ struct MapViewSwiftUI: UIViewRepresentable { parent.onLongPress(coordinate) } } - + public func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer { - + if let tileOverlay = overlay as? MKTileOverlay { return MKTileOverlayRenderer(tileOverlay: tileOverlay) } else { @@ -362,18 +369,18 @@ struct MapViewSwiftUI: UIViewRepresentable { } } } - + /// is supposed to be located in the folder with the map name public struct DefaultTile: Hashable { let tileName: String let tileType: String - + public init(tileName: String, tileType: String) { self.tileName = tileName self.tileType = tileType } } - + public struct CustomMapOverlay: Equatable, Hashable { let mapName: String let tileType: String @@ -381,7 +388,7 @@ struct MapViewSwiftUI: UIViewRepresentable { var minimumZoomLevel: Int? var maximumZoomLevel: Int? let defaultTile: DefaultTile? - + public init( mapName: String, tileType: String, @@ -397,7 +404,7 @@ struct MapViewSwiftUI: UIViewRepresentable { self.maximumZoomLevel = maximumZoomLevel self.defaultTile = defaultTile } - + public init?( mapName: String?, tileType: String, @@ -417,15 +424,15 @@ struct MapViewSwiftUI: UIViewRepresentable { self.defaultTile = defaultTile } } - + public class CustomMapOverlaySource: MKTileOverlay { - + // requires folder: tiles/{mapName}/z/y/y,{tileType} private var parent: MapViewSwiftUI private let mapName: String private let tileType: String private let defaultTile: DefaultTile? - + public init( parent: MapViewSwiftUI, mapName: String, @@ -438,7 +445,7 @@ struct MapViewSwiftUI: UIViewRepresentable { self.defaultTile = defaultTile super.init(urlTemplate: "") } - + public override func url(forTilePath path: MKTileOverlayPath) -> URL { if let tileUrl = Bundle.main.url( forResource: "\(path.y)", @@ -460,31 +467,4 @@ struct MapViewSwiftUI: UIViewRepresentable { } } } - -// public struct Overlay { -// -// public static func == (lhs: MapViewSwiftUI.Overlay, rhs: MapViewSwiftUI.Overlay) -> Bool { -// // maybe to use in the future for comparison of full array -// lhs.shape.coordinate.latitude == rhs.shape.coordinate.latitude && -// lhs.shape.coordinate.longitude == rhs.shape.coordinate.longitude && -// lhs.fillColor == rhs.fillColor -// } -// -// var shape: MKOverlay -// var fillColor: UIColor? -// var strokeColor: UIColor? -// var lineWidth: CGFloat -// -// public init( -// shape: MKOverlay, -// fillColor: UIColor? = nil, -// strokeColor: UIColor? = nil, -// lineWidth: CGFloat = 0 -// ) { -// self.shape = shape -// self.fillColor = fillColor -// self.strokeColor = strokeColor -// self.lineWidth = lineWidth -// } -// } } diff --git a/Meshtastic/Views/Nodes/DeviceMetricsLog.swift b/Meshtastic/Views/Nodes/DeviceMetricsLog.swift index 98ba306b..3ed3ad81 100644 --- a/Meshtastic/Views/Nodes/DeviceMetricsLog.swift +++ b/Meshtastic/Views/Nodes/DeviceMetricsLog.swift @@ -105,7 +105,7 @@ struct DeviceMetricsLog: View { ForEach(node.telemetries?.reversed() as? [TelemetryEntity] ?? [], id: \.self) { (dm: TelemetryEntity) in if dm.metricsType == 0 { GridRow { - if dm.batteryLevel == 0 { + if dm.batteryLevel == 111 { Text("USB") .font(.caption) } else { diff --git a/Meshtastic/Views/Nodes/NodeDetail.swift b/Meshtastic/Views/Nodes/NodeDetail.swift index 38544cde..f7c8ab6f 100644 --- a/Meshtastic/Views/Nodes/NodeDetail.swift +++ b/Meshtastic/Views/Nodes/NodeDetail.swift @@ -60,7 +60,8 @@ struct NodeDetail: View { if node.positions?.count ?? 0 > 0 { ZStack { let positionArray = node.positions?.array as? [PositionEntity] ?? [] - let todaysPositions = positionArray.filter { $0.time! >= Calendar.current.startOfDay(for: Date()) } + let lastThousand = Array(positionArray.prefix(1000)) + // let todaysPositions = positionArray.filter { $0.time! >= Calendar.current.startOfDay(for: Date()) } ZStack { MapViewSwiftUI(onLongPress: { coord in waypointCoordinate = coord @@ -71,7 +72,7 @@ struct NodeDetail: View { editingWaypoint = wpId presentingWaypointForm = true } - }, positions: todaysPositions, waypoints: Array(waypoints), + }, positions: lastThousand, waypoints: Array(waypoints), mapViewType: mapType, userTrackingMode: MKUserTrackingMode.none, showNodeHistory: meshMapShowNodeHistory, diff --git a/Widgets/WidgetsLiveActivity.swift b/Widgets/WidgetsLiveActivity.swift index b6f1adfd..a9ffa68e 100644 --- a/Widgets/WidgetsLiveActivity.swift +++ b/Widgets/WidgetsLiveActivity.swift @@ -232,7 +232,7 @@ struct NodeInfoView: View { } else { Text("Not Connected") .multilineTextAlignment(.leading) - .font(.callout) + .font(.caption) .fontWeight(.semibold) .foregroundStyle(.tint) }