2023-01-10 06:49:19 -08:00
|
|
|
//
|
|
|
|
|
// MapViewSwitUI.swift
|
|
|
|
|
// Meshtastic
|
|
|
|
|
//
|
2023-01-13 22:30:10 -08:00
|
|
|
// Copyright(c) Josh Pirihi & Garth Vander Houwen 1/16/22.
|
2023-01-10 06:49:19 -08:00
|
|
|
|
2023-05-16 05:54:12 -07:00
|
|
|
import Foundation
|
2023-01-10 06:49:19 -08:00
|
|
|
import SwiftUI
|
|
|
|
|
import MapKit
|
|
|
|
|
|
2023-05-16 05:54:12 -07:00
|
|
|
struct PolygonInfo: Codable {
|
|
|
|
|
let stroke: String?
|
|
|
|
|
let strokeWidth, strokeOpacity: Int?
|
|
|
|
|
let fill: String?
|
|
|
|
|
let fillOpacity: Double?
|
|
|
|
|
let title, subtitle: String?
|
|
|
|
|
}
|
|
|
|
|
|
2023-02-19 09:57:43 -08:00
|
|
|
func degreesToRadians(_ number: Double) -> Double {
|
|
|
|
|
return number * .pi / 180
|
|
|
|
|
}
|
2023-05-06 16:15:12 -07:00
|
|
|
var currentMapLayer: MapLayer?
|
2023-02-19 09:57:43 -08:00
|
|
|
|
2023-01-10 06:49:19 -08:00
|
|
|
struct MapViewSwiftUI: UIViewRepresentable {
|
2023-04-18 00:09:13 -07:00
|
|
|
var onLongPress: (_ waypointCoordinate: CLLocationCoordinate2D) -> Void
|
|
|
|
|
var onWaypointEdit: (_ waypointId: Int ) -> Void
|
2023-01-11 21:39:14 -08:00
|
|
|
let mapView = MKMapView()
|
2023-03-24 22:57:36 -07:00
|
|
|
// Parameters
|
2023-05-14 00:16:55 -07:00
|
|
|
let selectedMapLayer: MapLayer
|
|
|
|
|
let selectedWeatherLayer: MapOverlayServer = UserDefaults.mapOverlayServer
|
2023-02-21 21:57:22 -08:00
|
|
|
let positions: [PositionEntity]
|
|
|
|
|
let waypoints: [WaypointEntity]
|
2023-03-03 19:41:02 -08:00
|
|
|
let userTrackingMode: MKUserTrackingMode
|
2023-04-18 00:09:13 -07:00
|
|
|
let showNodeHistory: Bool
|
|
|
|
|
let showRouteLines: Bool
|
2023-05-06 16:15:12 -07:00
|
|
|
let mapViewType: MKMapType = MKMapType.standard
|
2023-03-24 22:57:36 -07:00
|
|
|
// Offline Map Tiles
|
2023-01-12 01:27:17 -08:00
|
|
|
@AppStorage("lastUpdatedLocalMapFile") private var lastUpdatedLocalMapFile = 0
|
|
|
|
|
@State private var loadedLastUpdatedLocalMapFile = 0
|
|
|
|
|
var customMapOverlay: CustomMapOverlay?
|
|
|
|
|
@State private var presentCustomMapOverlayHash: CustomMapOverlay?
|
2023-04-25 17:56:57 -07:00
|
|
|
// MARK: Private methods
|
|
|
|
|
private func configureMap(mapView: MKMapView) {
|
2023-02-28 12:25:44 -08:00
|
|
|
// Map View Parameters
|
2023-02-21 21:57:22 -08:00
|
|
|
mapView.mapType = mapViewType
|
2023-01-14 11:26:32 -08:00
|
|
|
mapView.addAnnotations(waypoints)
|
2023-03-04 17:09:49 -08:00
|
|
|
// Do the initial map centering
|
2023-03-24 22:57:36 -07:00
|
|
|
let latest = positions
|
|
|
|
|
.filter { $0.latest == true }
|
|
|
|
|
.sorted { $0.nodePosition?.num ?? 0 > $1.nodePosition?.num ?? -1 }
|
2023-03-03 19:41:02 -08:00
|
|
|
let span = MKCoordinateSpan(latitudeDelta: 0.003, longitudeDelta: 0.003)
|
2023-04-18 00:09:13 -07:00
|
|
|
let center = (latest.count > 0 && userTrackingMode == MKUserTrackingMode.none) ? latest[0].coordinate : LocationHelper.currentLocation
|
2023-03-03 19:41:02 -08:00
|
|
|
let region = MKCoordinateRegion(center: center, span: span)
|
2023-03-19 22:11:46 -07:00
|
|
|
mapView.addAnnotations(showNodeHistory ? positions : latest)
|
2023-03-26 09:08:08 -07:00
|
|
|
mapView.setRegion(region, animated: true)
|
2023-03-24 22:57:36 -07:00
|
|
|
// Set user (phone gps) tracking options
|
|
|
|
|
mapView.setUserTrackingMode(userTrackingMode, animated: true)
|
|
|
|
|
if userTrackingMode == MKUserTrackingMode.none {
|
2023-03-24 23:56:31 -07:00
|
|
|
if latest.count == 1 {
|
2023-08-26 23:17:30 -07:00
|
|
|
mapView.fit(annotations: showNodeHistory ? positions: latest, andShow: false)
|
2023-03-24 23:56:31 -07:00
|
|
|
} else {
|
|
|
|
|
mapView.fitAllAnnotations()
|
|
|
|
|
}
|
2023-03-24 22:57:36 -07:00
|
|
|
mapView.showsUserLocation = false
|
|
|
|
|
} else {
|
|
|
|
|
mapView.showsUserLocation = true
|
|
|
|
|
}
|
2023-01-13 22:30:10 -08:00
|
|
|
// Other MKMapView Settings
|
2023-02-28 12:25:44 -08:00
|
|
|
mapView.preferredConfiguration.elevationStyle = .realistic// .flat
|
2023-05-16 05:54:12 -07:00
|
|
|
mapView.pointOfInterestFilter = MKPointOfInterestFilter.excludingAll
|
2023-01-13 22:30:10 -08:00
|
|
|
mapView.isPitchEnabled = true
|
|
|
|
|
mapView.isRotateEnabled = true
|
|
|
|
|
mapView.isScrollEnabled = true
|
|
|
|
|
mapView.isZoomEnabled = true
|
|
|
|
|
mapView.showsBuildings = true
|
2023-01-11 13:53:50 -08:00
|
|
|
mapView.showsScale = true
|
2023-01-13 22:30:10 -08:00
|
|
|
mapView.showsTraffic = true
|
2024-05-29 16:40:07 -05:00
|
|
|
|
2023-09-03 20:23:40 -07:00
|
|
|
mapView.showsCompass = false
|
|
|
|
|
let compass = MKCompassButton(mapView: mapView)
|
|
|
|
|
compass.translatesAutoresizingMaskIntoConstraints = false
|
2023-03-03 19:41:02 -08:00
|
|
|
#if targetEnvironment(macCatalyst)
|
2023-02-22 13:16:33 -08:00
|
|
|
// Show the default always visible compass and the mac only controls
|
2023-09-03 20:23:40 -07:00
|
|
|
compass.compassVisibility = .visible
|
|
|
|
|
mapView.addSubview(compass)
|
2023-01-13 22:30:10 -08:00
|
|
|
mapView.showsZoomControls = true
|
2023-02-21 19:03:11 -08:00
|
|
|
mapView.showsPitchControl = true
|
2023-09-03 20:23:40 -07:00
|
|
|
compass.trailingAnchor.constraint(equalTo: mapView.trailingAnchor, constant: -115).isActive = true
|
|
|
|
|
compass.bottomAnchor.constraint(equalTo: mapView.bottomAnchor, constant: -5).isActive = true
|
2023-03-03 19:41:02 -08:00
|
|
|
#else
|
2023-04-25 23:10:08 -07:00
|
|
|
compass.compassVisibility = .adaptive
|
|
|
|
|
mapView.addSubview(compass)
|
|
|
|
|
compass.trailingAnchor.constraint(equalTo: mapView.trailingAnchor, constant: -5).isActive = true
|
|
|
|
|
compass.topAnchor.constraint(equalTo: mapView.topAnchor, constant: 145).isActive = true
|
2023-04-25 17:56:57 -07:00
|
|
|
#endif
|
|
|
|
|
}
|
2023-05-14 12:27:26 -07:00
|
|
|
private func setMapBaseLayer(mapView: MKMapView) {
|
2023-05-06 16:15:12 -07:00
|
|
|
// Avoid refreshing UI if selectedLayer has not changed
|
|
|
|
|
guard currentMapLayer != selectedMapLayer else { return }
|
|
|
|
|
currentMapLayer = selectedMapLayer
|
|
|
|
|
for overlay in mapView.overlays {
|
2023-05-06 18:40:11 -07:00
|
|
|
if overlay is MKTileOverlay {
|
2023-05-06 16:15:12 -07:00
|
|
|
mapView.removeOverlay(overlay)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
switch selectedMapLayer {
|
|
|
|
|
case .offline:
|
|
|
|
|
mapView.mapType = .standard
|
2023-05-06 18:40:11 -07:00
|
|
|
if !UserDefaults.enableOfflineMapsMBTiles {
|
|
|
|
|
let overlay = TileOverlay()
|
|
|
|
|
overlay.canReplaceMapContent = false
|
2023-05-10 13:44:27 -07:00
|
|
|
overlay.minimumZ = UserDefaults.mapTileServer.zoomRange.startIndex
|
|
|
|
|
overlay.maximumZ = UserDefaults.mapTileServer.zoomRange.endIndex
|
2023-05-07 08:01:16 -07:00
|
|
|
mapView.addOverlay(overlay, level: UserDefaults.mapTilesAboveLabels ? .aboveLabels : .aboveRoads)
|
2023-05-06 18:40:11 -07:00
|
|
|
}
|
2023-05-06 16:15:12 -07:00
|
|
|
case .satellite:
|
|
|
|
|
mapView.mapType = .satellite
|
|
|
|
|
case .hybrid:
|
|
|
|
|
mapView.mapType = .hybrid
|
|
|
|
|
default:
|
|
|
|
|
mapView.mapType = .standard
|
|
|
|
|
}
|
2023-05-14 12:27:26 -07:00
|
|
|
}
|
|
|
|
|
private func setMapOverlays(mapView: MKMapView) {
|
2023-05-14 00:16:55 -07:00
|
|
|
// Weather radar
|
|
|
|
|
if UserDefaults.enableOverlayServer {
|
|
|
|
|
let locale = Locale.current
|
|
|
|
|
if locale.region?.identifier ?? "no locale" == "US" {
|
|
|
|
|
let overlay = MKTileOverlay(urlTemplate: selectedWeatherLayer.tileUrl)
|
|
|
|
|
overlay.canReplaceMapContent = false
|
|
|
|
|
overlay.minimumZ = selectedWeatherLayer.zoomRange.startIndex
|
|
|
|
|
overlay.maximumZ = selectedWeatherLayer.zoomRange.endIndex
|
|
|
|
|
mapView.addOverlay(overlay, level: .aboveLabels)
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-05-06 16:15:12 -07:00
|
|
|
}
|
2023-05-16 05:54:12 -07:00
|
|
|
private func setMbtilesOverlay(mapView: MKMapView) {
|
2023-05-06 18:40:11 -07:00
|
|
|
// MBTiles Offline
|
|
|
|
|
if UserDefaults.enableOfflineMaps && UserDefaults.enableOfflineMapsMBTiles {
|
|
|
|
|
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
|
|
|
|
|
if fileManager.fileExists(atPath: tilePath) {
|
2024-05-31 16:42:34 -05:00
|
|
|
logger.info("Loading local map file")
|
2023-05-06 18:40:11 -07:00
|
|
|
if let overlay = LocalMBTileOverlay(mbTilePath: tilePath) {
|
|
|
|
|
overlay.canReplaceMapContent = false// customMapOverlay.canReplaceMapContent
|
|
|
|
|
mapView.addOverlay(overlay)
|
|
|
|
|
}
|
|
|
|
|
} else {
|
2024-05-31 16:42:34 -05:00
|
|
|
logger.info("Couldn't find a local map file to load")
|
2023-05-06 18:40:11 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
DispatchQueue.main.async {
|
|
|
|
|
self.presentCustomMapOverlayHash = self.customMapOverlay
|
|
|
|
|
self.loadedLastUpdatedLocalMapFile = self.lastUpdatedLocalMapFile
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-05-16 05:54:12 -07:00
|
|
|
}
|
|
|
|
|
func makeUIView(context: Context) -> MKMapView {
|
|
|
|
|
currentMapLayer = nil
|
|
|
|
|
mapView.delegate = context.coordinator
|
|
|
|
|
self.configureMap(mapView: mapView)
|
|
|
|
|
return mapView
|
|
|
|
|
}
|
|
|
|
|
func updateUIView(_ mapView: MKMapView, context: Context) {
|
|
|
|
|
// Set MBTiles overlay layer
|
|
|
|
|
setMbtilesOverlay(mapView: mapView)
|
2023-05-14 12:27:26 -07:00
|
|
|
// Set selected map base layer
|
|
|
|
|
setMapBaseLayer(mapView: mapView)
|
2023-05-16 05:54:12 -07:00
|
|
|
// Set map tile server and weather overlay layers
|
2023-05-14 12:27:26 -07:00
|
|
|
setMapOverlays(mapView: mapView)
|
2023-04-25 21:44:34 -07:00
|
|
|
let latest = positions
|
|
|
|
|
.filter { $0.latest == true }
|
|
|
|
|
.sorted { $0.nodePosition?.num ?? 0 > $1.nodePosition?.num ?? -1 }
|
|
|
|
|
// Node Route Lines
|
2023-05-06 18:40:11 -07:00
|
|
|
if showRouteLines {
|
2023-04-25 21:44:34 -07:00
|
|
|
// Remove all existing PolyLine Overlays
|
|
|
|
|
for overlay in mapView.overlays {
|
|
|
|
|
if overlay is MKPolyline {
|
|
|
|
|
mapView.removeOverlay(overlay)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
var lineIndex = 0
|
|
|
|
|
for position in latest {
|
2023-04-27 18:01:23 -07:00
|
|
|
let nodePositions = positions.filter { $0.nodeCoordinate != nil && $0.nodePosition?.num ?? 0 == position.nodePosition?.num ?? -1 }
|
2023-08-26 23:17:30 -07:00
|
|
|
let lineCoords = nodePositions.compactMap({(position) -> CLLocationCoordinate2D in
|
2023-04-27 18:01:23 -07:00
|
|
|
return position.nodeCoordinate ?? LocationHelper.DefaultLocation
|
2023-04-25 21:44:34 -07:00
|
|
|
})
|
|
|
|
|
let polyline = MKPolyline(coordinates: lineCoords, count: nodePositions.count)
|
|
|
|
|
polyline.title = "\(String(position.nodePosition?.num ?? 0))"
|
|
|
|
|
mapView.addOverlay(polyline, level: .aboveLabels)
|
|
|
|
|
lineIndex += 1
|
|
|
|
|
// There are 18 colors for lines, start over if we are at index 17
|
|
|
|
|
if lineIndex > 17 {
|
|
|
|
|
lineIndex = 0
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-04-25 21:51:57 -07:00
|
|
|
} else {
|
|
|
|
|
// Remove all existing PolyLine Overlays
|
|
|
|
|
for overlay in mapView.overlays {
|
|
|
|
|
if overlay is MKPolyline {
|
|
|
|
|
mapView.removeOverlay(overlay)
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-04-25 21:44:34 -07:00
|
|
|
}
|
2023-04-25 22:02:44 -07:00
|
|
|
let annotationCount = waypoints.count + (showNodeHistory ? positions.count : latest.count)
|
|
|
|
|
if annotationCount != mapView.annotations.count {
|
2024-05-31 16:42:34 -05:00
|
|
|
logger.info("Annotation Count: \(annotationCount) Map Annotations: \(mapView.annotations.count)")
|
2023-04-25 22:02:44 -07:00
|
|
|
mapView.removeAnnotations(mapView.annotations)
|
|
|
|
|
mapView.addAnnotations(waypoints)
|
|
|
|
|
}
|
2023-09-01 07:11:49 -05:00
|
|
|
mapView.addAnnotations(showNodeHistory ? positions : latest)
|
2023-04-25 22:02:44 -07:00
|
|
|
if userTrackingMode == MKUserTrackingMode.none {
|
|
|
|
|
mapView.showsUserLocation = false
|
2023-05-09 19:31:25 -07:00
|
|
|
if UserDefaults.enableMapRecentering {
|
|
|
|
|
if latest.count == 1 {
|
|
|
|
|
mapView.fit(annotations: showNodeHistory ? positions : latest, andShow: true)
|
|
|
|
|
} else {
|
|
|
|
|
mapView.fitAllAnnotations()
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-04-25 22:02:44 -07:00
|
|
|
} else {
|
|
|
|
|
mapView.showsUserLocation = true
|
|
|
|
|
}
|
|
|
|
|
mapView.setUserTrackingMode(userTrackingMode, animated: true)
|
2023-01-10 06:49:19 -08:00
|
|
|
}
|
|
|
|
|
func makeCoordinator() -> MapCoordinator {
|
2023-01-11 21:39:14 -08:00
|
|
|
return Coordinator(self)
|
2023-01-10 06:49:19 -08:00
|
|
|
}
|
2023-01-11 21:39:14 -08:00
|
|
|
final class MapCoordinator: NSObject, MKMapViewDelegate, UIGestureRecognizerDelegate {
|
|
|
|
|
var parent: MapViewSwiftUI
|
2023-01-12 01:27:17 -08:00
|
|
|
var longPressRecognizer = UILongPressGestureRecognizer()
|
2023-01-11 21:39:14 -08:00
|
|
|
init(_ parent: MapViewSwiftUI) {
|
|
|
|
|
self.parent = parent
|
|
|
|
|
super.init()
|
2023-01-12 01:27:17 -08:00
|
|
|
self.longPressRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(longPressHandler))
|
2023-04-18 00:09:13 -07:00
|
|
|
self.longPressRecognizer.minimumPressDuration = 0.5
|
|
|
|
|
self.longPressRecognizer.cancelsTouchesInView = true
|
|
|
|
|
self.longPressRecognizer.delegate = self
|
2023-01-12 01:27:17 -08:00
|
|
|
self.parent.mapView.addGestureRecognizer(longPressRecognizer)
|
2023-01-11 21:39:14 -08:00
|
|
|
}
|
2023-01-10 06:49:19 -08:00
|
|
|
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
|
|
|
|
|
switch annotation {
|
2023-02-05 10:43:56 -08:00
|
|
|
case let positionAnnotation as PositionEntity:
|
2023-02-15 14:52:49 -08:00
|
|
|
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 )
|
2023-01-16 17:40:28 -08:00
|
|
|
annotationView.tag = -1
|
2023-01-10 06:49:19 -08:00
|
|
|
annotationView.canShowCallout = true
|
2023-02-22 09:37:31 -08:00
|
|
|
if positionAnnotation.latest {
|
2023-08-31 22:48:52 -07:00
|
|
|
annotationView.markerTintColor = UIColor(hex: UInt32(positionAnnotation.nodePosition?.num ?? 0)).darker()
|
2023-02-10 20:11:35 -08:00
|
|
|
annotationView.displayPriority = .required
|
|
|
|
|
annotationView.titleVisibility = .visible
|
2023-03-06 10:33:18 -08:00
|
|
|
} else {
|
2023-09-02 08:50:23 -07:00
|
|
|
annotationView.markerTintColor = UIColor(hex: UInt32(positionAnnotation.nodePosition?.num ?? 0)).lighter()
|
2023-02-10 20:11:35 -08:00
|
|
|
annotationView.displayPriority = .defaultHigh
|
|
|
|
|
annotationView.titleVisibility = .adaptive
|
|
|
|
|
}
|
2023-02-21 19:07:23 -08:00
|
|
|
annotationView.tag = -1
|
2023-02-21 19:03:11 -08:00
|
|
|
annotationView.canShowCallout = true
|
2023-01-21 07:28:50 -08:00
|
|
|
annotationView.titleVisibility = .adaptive
|
2023-02-05 10:43:56 -08:00
|
|
|
let leftIcon = UIImageView(image: annotationView.glyphText?.image())
|
|
|
|
|
leftIcon.backgroundColor = UIColor(.indigo)
|
|
|
|
|
annotationView.leftCalloutAccessoryView = leftIcon
|
|
|
|
|
let subtitle = UILabel()
|
2023-02-20 19:52:46 -08:00
|
|
|
subtitle.text = "Long Name: \(positionAnnotation.nodePosition?.user?.longName ?? "Unknown") \n"
|
2023-03-10 08:46:53 -08:00
|
|
|
subtitle.text? += "Latitude: \(String(format: "%.5f", positionAnnotation.coordinate.latitude)) \n"
|
2023-02-05 10:43:56 -08:00
|
|
|
subtitle.text! += "Longitude: \(String(format: "%.5f", positionAnnotation.coordinate.longitude)) \n"
|
2023-02-05 17:30:43 -08:00
|
|
|
let distanceFormatter = MKDistanceFormatter()
|
|
|
|
|
subtitle.text! += "Altitude: \(distanceFormatter.string(fromDistance: Double(positionAnnotation.altitude))) \n"
|
|
|
|
|
if positionAnnotation.nodePosition?.metadata != nil {
|
2023-02-21 19:03:11 -08:00
|
|
|
if DeviceRoles(rawValue: Int(positionAnnotation.nodePosition!.metadata?.role ?? 0)) == DeviceRoles.client ||
|
|
|
|
|
DeviceRoles(rawValue: Int(positionAnnotation.nodePosition!.metadata?.role ?? 0)) == DeviceRoles.clientMute ||
|
2023-03-06 10:33:18 -08:00
|
|
|
DeviceRoles(rawValue: Int(positionAnnotation.nodePosition!.metadata?.role ?? 0)) == DeviceRoles.routerClient {
|
2023-02-21 19:03:11 -08:00
|
|
|
annotationView.glyphImage = UIImage(systemName: "flipphone")
|
|
|
|
|
} else if DeviceRoles(rawValue: Int(positionAnnotation.nodePosition!.metadata?.role ?? 0)) == DeviceRoles.repeater {
|
|
|
|
|
annotationView.glyphImage = UIImage(systemName: "repeat")
|
|
|
|
|
} else if DeviceRoles(rawValue: Int(positionAnnotation.nodePosition!.metadata?.role ?? 0)) == DeviceRoles.router {
|
|
|
|
|
annotationView.glyphImage = UIImage(systemName: "wifi.router.fill")
|
|
|
|
|
} else if DeviceRoles(rawValue: Int(positionAnnotation.nodePosition!.metadata?.role ?? 0)) == DeviceRoles.tracker {
|
|
|
|
|
annotationView.glyphImage = UIImage(systemName: "location.viewfinder")
|
2023-02-24 21:14:08 -08:00
|
|
|
} else if DeviceRoles(rawValue: Int(positionAnnotation.nodePosition!.metadata?.role ?? 0)) == DeviceRoles.sensor {
|
|
|
|
|
annotationView.glyphImage = UIImage(systemName: "sensor")
|
2023-02-21 19:03:11 -08:00
|
|
|
}
|
2023-02-05 17:30:43 -08:00
|
|
|
let pf = PositionFlags(rawValue: Int(positionAnnotation.nodePosition?.metadata?.positionFlags ?? 3))
|
|
|
|
|
if pf.contains(.Satsinview) {
|
|
|
|
|
subtitle.text! += "Sats in view: \(String(positionAnnotation.satsInView)) \n"
|
2023-02-05 18:36:35 -08:00
|
|
|
}
|
|
|
|
|
if pf.contains(.SeqNo) {
|
|
|
|
|
subtitle.text! += "Sequence: \(String(positionAnnotation.seqNo)) \n"
|
|
|
|
|
}
|
2023-03-06 10:33:18 -08:00
|
|
|
if pf.contains(.Heading) {
|
2023-03-03 19:41:02 -08:00
|
|
|
if parent.userTrackingMode != MKUserTrackingMode.followWithHeading {
|
2023-02-27 11:16:46 -08:00
|
|
|
annotationView.glyphImage = UIImage(systemName: "location.north.fill")?.rotate(radians: Float(degreesToRadians(Double(positionAnnotation.heading))))
|
|
|
|
|
subtitle.text! += "Heading: \(String(positionAnnotation.heading)) \n"
|
|
|
|
|
} else {
|
|
|
|
|
annotationView.glyphImage = UIImage(systemName: "flipphone")
|
|
|
|
|
}
|
2023-02-05 18:36:35 -08:00
|
|
|
}
|
2023-03-04 18:03:14 -08:00
|
|
|
if pf.contains(.Speed) {
|
|
|
|
|
let formatter = MeasurementFormatter()
|
|
|
|
|
formatter.locale = Locale.current
|
|
|
|
|
if positionAnnotation.speed <= 1 {
|
|
|
|
|
annotationView.glyphImage = UIImage(systemName: "hexagon")
|
|
|
|
|
}
|
|
|
|
|
subtitle.text! += "Speed: \(formatter.string(from: Measurement(value: Double(positionAnnotation.speed), unit: UnitSpeed.kilometersPerHour))) \n"
|
|
|
|
|
}
|
2023-02-19 09:57:43 -08:00
|
|
|
} else {
|
2023-03-04 18:03:14 -08:00
|
|
|
// node metadata is nil
|
2023-02-21 19:03:11 -08:00
|
|
|
annotationView.glyphImage = UIImage(systemName: "flipphone")
|
2023-02-05 17:30:43 -08:00
|
|
|
}
|
2023-03-04 17:09:49 -08:00
|
|
|
if LocationHelper.currentLocation.distance(from: LocationHelper.DefaultLocation) > 0.0 {
|
2023-04-18 00:09:13 -07:00
|
|
|
let metersAway = positionAnnotation.coordinate.distance(from: LocationHelper.currentLocation)
|
2023-05-05 09:27:24 -07:00
|
|
|
subtitle.text! += "distance".localized + ": \(distanceFormatter.string(fromDistance: Double(metersAway))) \n"
|
2023-03-04 17:09:49 -08:00
|
|
|
}
|
2023-02-05 10:43:56 -08:00
|
|
|
subtitle.text! += positionAnnotation.time?.formatted() ?? "Unknown \n"
|
|
|
|
|
subtitle.numberOfLines = 0
|
|
|
|
|
annotationView.detailCalloutAccessoryView = subtitle
|
2023-02-05 18:36:35 -08:00
|
|
|
let detailsIcon = UIButton(type: .detailDisclosure)
|
2023-05-01 10:39:49 -07:00
|
|
|
detailsIcon.setImage(UIImage(systemName: "trash"), for: .normal)
|
2023-02-05 18:36:35 -08:00
|
|
|
annotationView.rightCalloutAccessoryView = detailsIcon
|
2023-01-10 06:49:19 -08:00
|
|
|
return annotationView
|
2023-01-13 22:30:10 -08:00
|
|
|
case let waypointAnnotation as WaypointEntity:
|
2023-02-15 14:52:49 -08:00
|
|
|
let annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: "waypoint") as? MKMarkerAnnotationView ?? MKMarkerAnnotationView(annotation: annotation, reuseIdentifier: String(waypointAnnotation.id))
|
2023-01-16 17:40:28 -08:00
|
|
|
annotationView.tag = Int(waypointAnnotation.id)
|
2023-01-16 23:16:57 -08:00
|
|
|
annotationView.isEnabled = true
|
2023-01-13 22:30:10 -08:00
|
|
|
annotationView.canShowCallout = true
|
2023-01-14 11:26:32 -08:00
|
|
|
if waypointAnnotation.icon == 0 {
|
2023-01-15 08:49:17 -08:00
|
|
|
annotationView.glyphText = "📍"
|
2023-01-14 11:26:32 -08:00
|
|
|
} else {
|
2023-01-15 08:49:17 -08:00
|
|
|
annotationView.glyphText = String(UnicodeScalar(Int(waypointAnnotation.icon)) ?? "📍")
|
2023-01-14 11:26:32 -08:00
|
|
|
}
|
2023-01-19 00:16:48 -08:00
|
|
|
annotationView.markerTintColor = UIColor(.accentColor)
|
2023-02-28 12:25:44 -08:00
|
|
|
annotationView.displayPriority = .required
|
2023-01-21 07:28:50 -08:00
|
|
|
annotationView.titleVisibility = .adaptive
|
2023-01-19 00:16:48 -08:00
|
|
|
let leftIcon = UIImageView(image: annotationView.glyphText?.image())
|
|
|
|
|
leftIcon.backgroundColor = UIColor(.accentColor)
|
|
|
|
|
annotationView.leftCalloutAccessoryView = leftIcon
|
2023-01-18 16:57:44 -08:00
|
|
|
let subtitle = UILabel()
|
2023-02-06 10:26:04 -08:00
|
|
|
if waypointAnnotation.longDescription?.count ?? 0 > 0 {
|
2023-02-05 20:23:31 -08:00
|
|
|
subtitle.text = (waypointAnnotation.longDescription ?? "") + "\n"
|
2023-03-06 10:33:18 -08:00
|
|
|
} else {
|
2023-02-05 20:23:31 -08:00
|
|
|
subtitle.text = ""
|
|
|
|
|
}
|
2023-03-04 17:09:49 -08:00
|
|
|
if LocationHelper.currentLocation.distance(from: LocationHelper.DefaultLocation) > 0.0 {
|
2023-04-18 00:09:13 -07:00
|
|
|
let metersAway = waypointAnnotation.coordinate.distance(from: LocationHelper.currentLocation)
|
2023-03-04 17:09:49 -08:00
|
|
|
let distanceFormatter = MKDistanceFormatter()
|
2023-05-05 09:27:24 -07:00
|
|
|
subtitle.text! += "distance".localized + ": \(distanceFormatter.string(fromDistance: Double(metersAway))) \n"
|
2023-03-04 17:09:49 -08:00
|
|
|
}
|
2023-02-05 20:23:31 -08:00
|
|
|
if waypointAnnotation.created != nil {
|
|
|
|
|
subtitle.text! += "Created: \(waypointAnnotation.created?.formatted() ?? "Unknown") \n"
|
|
|
|
|
}
|
|
|
|
|
if waypointAnnotation.lastUpdated != nil {
|
|
|
|
|
subtitle.text! += "Updated: \(waypointAnnotation.lastUpdated?.formatted() ?? "Unknown") \n"
|
|
|
|
|
}
|
|
|
|
|
if waypointAnnotation.expire != nil {
|
|
|
|
|
subtitle.text! += "Expires: \(waypointAnnotation.expire?.formatted() ?? "Unknown") \n"
|
|
|
|
|
}
|
2023-01-18 16:57:44 -08:00
|
|
|
subtitle.numberOfLines = 0
|
2023-01-19 00:16:48 -08:00
|
|
|
annotationView.detailCalloutAccessoryView = subtitle
|
2023-01-18 16:57:44 -08:00
|
|
|
let editIcon = UIButton(type: .detailDisclosure)
|
|
|
|
|
editIcon.setImage(UIImage(systemName: "square.and.pencil"), for: .normal)
|
|
|
|
|
annotationView.rightCalloutAccessoryView = editIcon
|
2023-01-13 22:30:10 -08:00
|
|
|
return annotationView
|
2023-01-10 06:49:19 -08:00
|
|
|
default: return nil
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-01-16 23:16:57 -08:00
|
|
|
func mapView(_ mapView: MKMapView, annotationView view: MKAnnotationView, calloutAccessoryControlTapped control: UIControl) {
|
2023-05-01 10:39:49 -07:00
|
|
|
switch view.annotation {
|
2023-09-04 20:17:09 -07:00
|
|
|
case _ as WaypointEntity:
|
2023-05-01 10:39:49 -07:00
|
|
|
// Only Allow Edit for waypoint annotations with a id
|
|
|
|
|
if view.tag > 0 {
|
|
|
|
|
parent.onWaypointEdit(view.tag)
|
|
|
|
|
}
|
|
|
|
|
default: break
|
2023-01-18 16:57:44 -08:00
|
|
|
}
|
2023-01-16 17:40:28 -08:00
|
|
|
}
|
2023-04-18 00:09:13 -07:00
|
|
|
@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()
|
|
|
|
|
annotation.title = "Dropped Pin"
|
|
|
|
|
annotation.coordinate = coordinate
|
|
|
|
|
parent.mapView.addAnnotation(annotation)
|
|
|
|
|
UINotificationFeedbackGenerator().notificationOccurred(.success)
|
|
|
|
|
parent.onLongPress(coordinate)
|
2023-01-19 22:04:18 -08:00
|
|
|
}
|
2023-01-13 09:40:52 -08:00
|
|
|
}
|
|
|
|
|
public func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
|
2023-05-06 18:40:11 -07:00
|
|
|
if let tileOverlay = overlay as? MKTileOverlay {
|
|
|
|
|
return MKTileOverlayRenderer(tileOverlay: tileOverlay)
|
|
|
|
|
} else {
|
|
|
|
|
if let routePolyline = overlay as? MKPolyline {
|
|
|
|
|
let titleString = routePolyline.title ?? "0"
|
|
|
|
|
let renderer = MKPolylineRenderer(polyline: routePolyline)
|
2023-09-02 08:50:23 -07:00
|
|
|
renderer.strokeColor = UIColor(hex: UInt32(titleString) ?? 0).lighter()
|
2023-05-06 18:40:11 -07:00
|
|
|
renderer.lineWidth = 8
|
|
|
|
|
return renderer
|
|
|
|
|
}
|
2023-05-16 05:54:12 -07:00
|
|
|
if let polygon = overlay as? MKPolygon {
|
|
|
|
|
let renderer = MKPolygonRenderer(polygon: polygon)
|
|
|
|
|
renderer.fillColor = UIColor.purple.withAlphaComponent(0.2)
|
|
|
|
|
renderer.strokeColor = .purple.withAlphaComponent(0.7)
|
|
|
|
|
return renderer
|
|
|
|
|
}
|
|
|
|
|
return MKOverlayRenderer(overlay: overlay)
|
2023-01-13 09:40:52 -08:00
|
|
|
}
|
2023-01-12 01:27:17 -08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
/// 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
|
|
|
|
|
var canReplaceMapContent: Bool
|
|
|
|
|
var minimumZoomLevel: Int?
|
|
|
|
|
var maximumZoomLevel: Int?
|
|
|
|
|
let defaultTile: DefaultTile?
|
|
|
|
|
public init(
|
|
|
|
|
mapName: String,
|
|
|
|
|
tileType: String,
|
|
|
|
|
canReplaceMapContent: Bool = true, // false for transparent tiles
|
|
|
|
|
minimumZoomLevel: Int? = nil,
|
|
|
|
|
maximumZoomLevel: Int? = nil,
|
|
|
|
|
defaultTile: DefaultTile? = nil
|
|
|
|
|
) {
|
|
|
|
|
self.mapName = mapName
|
|
|
|
|
self.tileType = tileType
|
|
|
|
|
self.canReplaceMapContent = canReplaceMapContent
|
|
|
|
|
self.minimumZoomLevel = minimumZoomLevel
|
|
|
|
|
self.maximumZoomLevel = maximumZoomLevel
|
|
|
|
|
self.defaultTile = defaultTile
|
|
|
|
|
}
|
|
|
|
|
public init?(
|
|
|
|
|
mapName: String?,
|
|
|
|
|
tileType: String,
|
|
|
|
|
canReplaceMapContent: Bool = true, // false for transparent tiles
|
|
|
|
|
minimumZoomLevel: Int? = nil,
|
|
|
|
|
maximumZoomLevel: Int? = nil,
|
|
|
|
|
defaultTile: DefaultTile? = nil
|
|
|
|
|
) {
|
2023-03-06 10:33:18 -08:00
|
|
|
if mapName == nil || mapName! == "" {
|
2023-01-12 01:27:17 -08:00
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
self.mapName = mapName!
|
|
|
|
|
self.tileType = tileType
|
|
|
|
|
self.canReplaceMapContent = canReplaceMapContent
|
|
|
|
|
self.minimumZoomLevel = minimumZoomLevel
|
|
|
|
|
self.maximumZoomLevel = maximumZoomLevel
|
|
|
|
|
self.defaultTile = defaultTile
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-01-10 06:49:19 -08:00
|
|
|
}
|