From 68b34cd8de099ea4a5794b77d52806a124764d3c Mon Sep 17 00:00:00 2001 From: Joshua Pirihi Date: Mon, 10 Jan 2022 06:09:31 +1300 Subject: [PATCH] New map view --- .../Map/Custom/PositionAnnotationView.swift | 52 +- MeshtasticClient/Views/Map/MapView.swift | 18 +- .../Views/Map/MapViewModule.swift | 746 ++++++++++++++++++ MeshtasticClient/Views/Nodes/NodeMap.swift | 89 ++- .../Views/Settings/AppSettings.swift | 10 +- 5 files changed, 879 insertions(+), 36 deletions(-) create mode 100644 MeshtasticClient/Views/Map/MapViewModule.swift diff --git a/MeshtasticClient/Views/Map/Custom/PositionAnnotationView.swift b/MeshtasticClient/Views/Map/Custom/PositionAnnotationView.swift index 8e97bb98..f00858c6 100644 --- a/MeshtasticClient/Views/Map/Custom/PositionAnnotationView.swift +++ b/MeshtasticClient/Views/Map/Custom/PositionAnnotationView.swift @@ -26,38 +26,38 @@ class PositionAnnotation: NSObject, MKAnnotation { class PositionAnnotationView: MKAnnotationView { private let annotationFrame = CGRect(x: 0, y: 0, width: 32, height: 32) - private let label: UILabel + 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) + 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 } + } - required init?(coder aDecoder: NSCoder) { - fatalError("init(coder:) not implemented!") - } + override func draw(_ rect: CGRect) { + guard let context = UIGraphicsGetCurrentContext() else { return } - public var name: String = "" { - didSet { - self.label.text = name - } - } + let circleRect = CGRect(x: 1, y: 1, width: 30, height: 30) - override func draw(_ rect: CGRect) { - guard let context = UIGraphicsGetCurrentContext() else { return } + context.setFillColor(CGColor(red: 0, green: 0.5, blue: 1.0, alpha: 1.0)) - let circleRect = CGRect(x: 1, y: 1, width: 30, height: 30) + context.fillEllipse(in: circleRect) - 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 index ddc5f896..4f2d6431 100644 --- a/MeshtasticClient/Views/Map/MapView.swift +++ b/MeshtasticClient/Views/Map/MapView.swift @@ -10,13 +10,13 @@ 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 - weak var mapViewDelegate = MapViewDelegate() + var mapViewDelegate = MapViewDelegate() // observe changes to the key in UserDefaults @AppStorage("meshMapType") var type: String = "hybrid" @@ -36,6 +36,12 @@ struct MapView: UIViewRepresentable { 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 } @@ -129,6 +135,8 @@ private extension MapView { class MapViewDelegate: NSObject, MKMapViewDelegate { + var renderer: MKTileOverlayRenderer? + func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? { guard !annotation.isKind(of: MKUserLocation.self) else { @@ -144,6 +152,11 @@ class MapViewDelegate: NSObject, MKMapViewDelegate { 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) @@ -157,3 +170,4 @@ class MapViewDelegate: NSObject, MKMapViewDelegate { return annotationView } } +#endif diff --git a/MeshtasticClient/Views/Map/MapViewModule.swift b/MeshtasticClient/Views/Map/MapViewModule.swift new file mode 100644 index 00000000..088ff3e2 --- /dev/null +++ b/MeshtasticClient/Views/Map/MapViewModule.swift @@ -0,0 +1,746 @@ +// +// MapView.swift +// MapViewTest +// +// Created by Cem Yilmaz on 05.07.21. +// +import SwiftUI +import MapKit +import CoreData + +#if canImport(MapKit) && canImport(UIKit) +public struct MapView: UIViewRepresentable { + + @Environment(\.managedObjectContext) var context + + //@Binding private var region: MKCoordinateRegion + + private var customMapOverlay: CustomMapOverlay? + @State private var presentCustomMapOverlayHash: CustomMapOverlay? + + private var mapType: MKMapType + + private var showZoomScale: Bool + private var zoomEnabled: Bool + private var zoomRange: (minHeight: CLLocationDistance?, maxHeight: CLLocationDistance?) + + private var scrollEnabled: Bool + private var scrollBoundaries: MKCoordinateRegion? + + private var rotationEnabled: Bool + private var showCompassWhenRotated: Bool + + private var showUserLocation: Bool + private var userTrackingMode: MKUserTrackingMode + @Binding private var userLocation: CLLocationCoordinate2D? + + //private var annotations: [MKPointAnnotation] + + private var overlays: [Overlay] + + @FetchRequest(sortDescriptors: [NSSortDescriptor(key: "lastHeard", ascending: false)], animation: .default) + private var locationNodes: FetchedResults + + public init( + //region: Binding = .constant(MKCoordinateRegion()), + customMapOverlay: CustomMapOverlay? = nil, + //mapType: MKMapType = MKMapType.standard, + mapType: String = "hybrid", + zoomEnabled: Bool = true, + showZoomScale: Bool = false, + zoomRange: (minHeight: CLLocationDistance?, maxHeight: CLLocationDistance?) = (nil, nil), + scrollEnabled: Bool = true, + scrollBoundaries: MKCoordinateRegion? = nil, + rotationEnabled: Bool = true, + showCompassWhenRotated: Bool = true, + showUserLocation: Bool = true, + userTrackingMode: MKUserTrackingMode = MKUserTrackingMode.none, + userLocation: Binding = .constant(nil), + //annotations: [MKPointAnnotation] = [], + //locationNodes: [NodeInfoEntity] = [], + overlays: [Overlay] = [] + ) { + //self._region = region + + self.customMapOverlay = customMapOverlay + + switch mapType { + case "satellite": + self.mapType = .satellite + break + case "standard": + self.mapType = .standard + break + case "hybrid": + self.mapType = .hybrid + break + default: + self.mapType = .hybrid + } + //self.mapType = mapType + + self.showZoomScale = showZoomScale + self.zoomEnabled = zoomEnabled + self.zoomRange = zoomRange + + self.scrollEnabled = scrollEnabled + self.scrollBoundaries = scrollBoundaries + + self.rotationEnabled = rotationEnabled + self.showCompassWhenRotated = showCompassWhenRotated + + self.showUserLocation = showUserLocation + self.userTrackingMode = userTrackingMode + self._userLocation = userLocation + + //self.annotations = annotations + + //self.locationNodes = locationNodes + + self.overlays = overlays + + } + + public func makeUIView(context: Context) -> MKMapView { + let mapView = MKMapView() + mapView.delegate = context.coordinator + mapView.register(PositionAnnotationView.self, forAnnotationViewWithReuseIdentifier: NSStringFromClass(PositionAnnotationView.self)) + + return mapView + } + + public func updateUIView(_ mapView: MKMapView, context: Context) { + + //if self.userTrackingMode == MKUserTrackingMode.none && (mapView.region.center.latitude != self.region.center.latitude || mapView.region.center.longitude != self.region.center.longitude) { + //mapView.region = self.region + //} + + if self.customMapOverlay != self.presentCustomMapOverlayHash { + mapView.removeOverlays(mapView.overlays) + if let customMapOverlay = self.customMapOverlay { + let overlay = CustomMapOverlaySource( + parent: self, + mapName: customMapOverlay.mapName, + tileType: customMapOverlay.tileType, + defaultTile: customMapOverlay.defaultTile + ) + + if let minZ = customMapOverlay.minimumZoomLevel { + overlay.minimumZ = minZ + } + + if let maxZ = customMapOverlay.maximumZoomLevel { + overlay.maximumZ = maxZ + } + + overlay.canReplaceMapContent = customMapOverlay.canReplaceMapContent + + mapView.addOverlay(overlay) + } + DispatchQueue.main.async { + self.presentCustomMapOverlayHash = self.customMapOverlay + } + } + + if mapView.overlays.count != (self.overlays.count + (self.customMapOverlay == nil ? 0 : 1)) { + context.coordinator.overlays = self.overlays + mapView.overlays.forEach { overlay in + if !(overlay is MKTileOverlay) { + mapView.removeOverlay(overlay) + } + } + mapView.addOverlays(self.overlays.map { overlay in overlay.shape }) + } + + if mapView.mapType != self.mapType { + mapView.mapType = self.mapType + } + + mapView.showsScale = self.zoomEnabled ? self.showZoomScale : false + + if mapView.isZoomEnabled != self.zoomEnabled { + mapView.isZoomEnabled = self.zoomEnabled + } + + if mapView.cameraZoomRange.minCenterCoordinateDistance != self.zoomRange.minHeight ?? 0 || + mapView.cameraZoomRange.maxCenterCoordinateDistance != self.zoomRange.maxHeight ?? .infinity { + mapView.cameraZoomRange = MKMapView.CameraZoomRange( + minCenterCoordinateDistance: self.zoomRange.minHeight ?? 0, + maxCenterCoordinateDistance: self.zoomRange.maxHeight ?? .infinity + ) + } + + mapView.isScrollEnabled = self.userTrackingMode == MKUserTrackingMode.none ? self.scrollEnabled : false + + if let scrollBoundary = self.scrollBoundaries, (mapView.cameraBoundary?.region.center.latitude != scrollBoundary.center.latitude || mapView.cameraBoundary?.region.center.longitude != scrollBoundary.center.longitude || mapView.cameraBoundary?.region.span.latitudeDelta != scrollBoundary.span.latitudeDelta || mapView.cameraBoundary?.region.span.longitudeDelta != scrollBoundary.span.longitudeDelta) { + mapView.cameraBoundary = MKMapView.CameraBoundary(coordinateRegion: scrollBoundary) + } else if self.scrollBoundaries == nil && mapView.cameraBoundary != nil { + mapView.cameraBoundary = nil + } + + mapView.isRotateEnabled = self.userTrackingMode != .followWithHeading ? self.rotationEnabled : false + mapView.showsCompass = self.userTrackingMode != .followWithHeading ? self.showCompassWhenRotated : false + + if mapView.showsUserLocation != self.showUserLocation { + mapView.showsUserLocation = self.showUserLocation + } + + if mapView.userTrackingMode != self.userTrackingMode { + mapView.userTrackingMode = self.userTrackingMode + } + + //if mapView.annotations.filter({ annotation in !(annotation is MKUserLocation) }).count != self.annotations.count { + // mapView.removeAnnotations(mapView.annotations) + // mapView.addAnnotations(self.annotations) + //} + + // clear any existing annotations + var shouldMoveRegion = false + if !mapView.annotations.isEmpty { + mapView.removeAnnotations(mapView.annotations) + } else { + shouldMoveRegion = true + } + + for node in self.locationNodes { + // 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() ?? "???" + + mapView.addAnnotation(annotation) + } + } + + if shouldMoveRegion { + self.moveToMeshRegion(mapView) + } + + + } + + func moveToMeshRegion(_ 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) + + + } + + public func makeCoordinator() -> Coordinator { + Coordinator(parent: self) + } + + public class Coordinator: NSObject, MKMapViewDelegate { + + private var parent: MapView + public var overlays: [Overlay] = [] + + init(parent: MapView) { + self.parent = parent + } + + /*public func mapViewDidChangeVisibleRegion(_ mapView: MKMapView) { + DispatchQueue.main.async { + self.parent.region = mapView.region + } + }*/ + + public 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 { + let identifier = NSStringFromClass(PositionAnnotationView.self) + + //let annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: identifier) as? PositionAnnotationView ?? PositionAnnotationView() + let annotationView = PositionAnnotationView(annotation: annotation, reuseIdentifier: "PositionAnnotation") + + annotationView.name = annotation.shortName ?? "???" + + annotationView.canShowCallout = true + + return annotationView + } + + return annotationView + } + + public func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer { + + if let index = self.overlays.firstIndex(where: { overlay_ in overlay_.shape.hash == overlay.hash }) { + + let unwrappedOverlay = self.overlays[index] + + if let circleOverlay = unwrappedOverlay.shape as? MKCircle { + + let renderer = MKCircleRenderer(circle: circleOverlay) + renderer.fillColor = unwrappedOverlay.fillColor + renderer.strokeColor = unwrappedOverlay.strokeColor + renderer.lineWidth = unwrappedOverlay.lineWidth + return renderer + + } else if let polygonOverlay = unwrappedOverlay.shape as? MKPolygon { + + let renderer = MKPolygonRenderer(polygon: polygonOverlay) + renderer.fillColor = unwrappedOverlay.fillColor + renderer.strokeColor = unwrappedOverlay.strokeColor + renderer.lineWidth = unwrappedOverlay.lineWidth + return renderer + + } else if let multiPolygonOverlay = unwrappedOverlay.shape as? MKMultiPolygon { + + let renderer = MKMultiPolygonRenderer(multiPolygon: multiPolygonOverlay) + renderer.fillColor = unwrappedOverlay.fillColor + renderer.strokeColor = unwrappedOverlay.strokeColor + renderer.lineWidth = unwrappedOverlay.lineWidth + return renderer + + } else if let polyLineOverlay = unwrappedOverlay.shape as? MKPolyline { + + let renderer = MKPolylineRenderer(polyline: polyLineOverlay) + renderer.fillColor = unwrappedOverlay.fillColor + renderer.strokeColor = unwrappedOverlay.strokeColor + renderer.lineWidth = unwrappedOverlay.lineWidth + return renderer + + } else if let multiPolylineOverlay = unwrappedOverlay.shape as? MKMultiPolyline { + + let renderer = MKMultiPolylineRenderer(multiPolyline: multiPolylineOverlay) + renderer.fillColor = unwrappedOverlay.fillColor + renderer.strokeColor = unwrappedOverlay.strokeColor + renderer.lineWidth = unwrappedOverlay.lineWidth + return renderer + + } else { + + return MKOverlayRenderer() + + } + + } else if let tileOverlay = overlay as? MKTileOverlay { + + return MKTileOverlayRenderer(tileOverlay: tileOverlay) + + } else { + + return MKOverlayRenderer() + + } + + } + + + + } + + /// 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 + ) { + if (mapName == nil || mapName! == "") { + return nil + } + self.mapName = mapName! + self.tileType = tileType + self.canReplaceMapContent = canReplaceMapContent + self.minimumZoomLevel = minimumZoomLevel + self.maximumZoomLevel = maximumZoomLevel + self.defaultTile = defaultTile + } + } + + public class CustomMapOverlaySource: MKTileOverlay { + + // requires folder: tiles/{mapName}/z/y/y,{tileType} + private var parent: MapView + private let mapName: String + private let tileType: String + private let defaultTile: DefaultTile? + + public init( + parent: MapView, + mapName: String, + tileType: String, + defaultTile: DefaultTile? + ) { + self.parent = parent + self.mapName = mapName + self.tileType = tileType + self.defaultTile = defaultTile + super.init(urlTemplate: "") + } + + public override func url(forTilePath path: MKTileOverlayPath) -> URL { + if let tileUrl = Bundle.main.url( + forResource: "\(path.y)", + withExtension: self.tileType, + subdirectory: "tiles/\(self.mapName)/\(path.z)/\(path.x)", + localization: nil + ) { + return tileUrl + } else if let defaultTile = self.defaultTile, let defaultTileUrl = Bundle.main.url( + forResource: defaultTile.tileName, + withExtension: defaultTile.tileType, + subdirectory: "tiles/\(self.mapName)", + localization: nil + ) { + return defaultTileUrl + } else { + let urlstring = self.mapName+"\(path.z)/\(path.x)/\(path.y).png" + return URL(string: urlstring)! + // Bundle.main.url(forResource: "surrounding", withExtension: "png", subdirectory: "tiles")! + } + + } + + } + + public struct Overlay { + + public static func == (lhs: MapView.Overlay, rhs: MapView.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 + } + } + +} + +// MARK: End of implementation +// MARK: Demonstration +/* +public struct MapViewDemo: View { + + @State private var locationManager: CLLocationManager + + @State private var mapRegion: MKCoordinateRegion = MKCoordinateRegion( + center: CLLocationCoordinate2D( + latitude: -38.758247, + longitude: 175.360208 + ), + span: MKCoordinateSpan( + latitudeDelta: 0.01, + longitudeDelta: 0.01 + ) + ) + + @State private var customMapOverlay: MapView.CustomMapOverlay? + + @State private var mapType: MKMapType = MKMapType.standard + + @State private var zoomEnabled: Bool = true + @State private var showZoomScale: Bool = true + @State private var useMinZoomBoundary: Bool = false + @State private var minZoom: Double = 0 + @State private var useMaxZoomBoundary: Bool = false + @State private var maxZoom: Double = 3000000 + + @State private var scrollEnabled: Bool = true + @State private var useScrollBoundaries: Bool = false + @State private var scrollBoundaries: MKCoordinateRegion = MKCoordinateRegion() + + @State private var rotationEnabled: Bool = true + @State private var showCompassWhenRotated: Bool = true + + @State private var showUserLocation: Bool = true + @State private var userTrackingMode: MKUserTrackingMode = MKUserTrackingMode.none + @State private var userLocation: CLLocationCoordinate2D? + + @State private var showAnnotations: Bool = true + @State private var annotations: [MKPointAnnotation] = [] + + @State private var showOverlays: Bool = true + @State private var overlays: [MapView.Overlay] = [] + + @State private var showMapCenter: Bool = false + + public init() { + self.locationManager = CLLocationManager() + self.locationManager.requestWhenInUseAuthorization() + } + + public var body: some View { + + NavigationView { + + List { + + Section(header: Text("Scroll")) { + Toggle("Scroll enabled", isOn: self.$scrollEnabled) + Toggle("Use scroll boundaries", isOn: self.$useScrollBoundaries) + .onChange(of: self.useScrollBoundaries) { newValue in + if newValue { + self.scrollBoundaries = MKCoordinateRegion(center: self.mapRegion.center, span: MKCoordinateSpan()) + } + } + if self.useScrollBoundaries { + VStack(alignment: .leading) { + Text(String(format: "Vertical distance to center: %.2f m", self.scrollBoundaries.span.latitudeDelta * 10609)) + Slider(value: self.$scrollBoundaries.span.latitudeDelta, in: 0...(300/10609)) + } + VStack(alignment: .leading) { + Text(String(format: "Horizontal distance to center: %.2f m", self.self.scrollBoundaries.span.longitudeDelta * 10609)) + Slider(value: self.$scrollBoundaries.span.longitudeDelta, in: 0...(300/10609)) + } + } + } + + Section(header: Text("Zoom")) { + Toggle("Zoom enabled", isOn: self.$zoomEnabled) + Toggle("Show zoom scale", isOn: self.$showZoomScale) + Toggle("Use minimum zoom boundary", isOn: self.$useMinZoomBoundary) + if self.useMinZoomBoundary { + VStack(alignment: .leading) { + Text(String(format: "Minimum Height: %.2f m", self.minZoom)) + Slider(value: self.$minZoom, in: 0...(self.useMaxZoomBoundary ? self.maxZoom : 3000000), step: 10) + } + } + Toggle("Use maximum zoom boundary", isOn: self.$useMaxZoomBoundary) + if self.useMaxZoomBoundary { + VStack(alignment: .leading) { + Text(String(format: "Maximum Height: %.2f m", self.maxZoom)) + Slider(value: self.$maxZoom, in: (self.useMinZoomBoundary ? self.minZoom : 0)...3000000, step: 10) + } + } + } + + Section(header: Text("Rotation")) { + Toggle("Rotation enabled", isOn: self.$rotationEnabled) + Toggle("Show compass when rotated", isOn: self.$showCompassWhenRotated) + } + + Section { + Toggle("Show map Center", isOn: self.$showMapCenter) + } + + Section(header: Text("User Location")) { + Toggle("Show User Location", isOn: self.$showUserLocation) + Picker("Follow Mode", selection: self.$userTrackingMode) { + Text("Nicht folgen").tag(MKUserTrackingMode.none) + Text("Folgen").tag(MKUserTrackingMode.follow) + Text("Richtung folgen").tag(MKUserTrackingMode.followWithHeading) + }.pickerStyle(MenuPickerStyle()) + + } + + Section(header: Text("Annotations")) { + Toggle("Show Annotations", isOn: self.$showAnnotations) + Button("Add Annotation") { + let annotation = MKPointAnnotation() + annotation.coordinate = self.mapRegion.center + annotation.title = "Title" + annotation.subtitle = "Subtitle" + self.annotations.append(annotation) + } + + Button("Delete all") { self.annotations = [] }.foregroundColor(.red) + } + + Section(header: Text("Overlays")) { + Toggle("Show Overlays", isOn: self.$showOverlays) + Button("Add circle") { + self.overlays.append(MapView.Overlay( + shape: MKCircle( + center: self.mapRegion.center, + radius: 20 + ), + strokeColor: UIColor.systemBlue, + lineWidth: 10 + )) + } + + Button("Delete all") { self.overlays = [] }.foregroundColor(.red) + } + + Section(header: Text("Custom Map Overlay")) { + Button("Keine") { self.customMapOverlay = nil } + Button("OSM Online") { + self.customMapOverlay = MapView.CustomMapOverlay( + mapName: "https://tile.openstreetmap.org/", + tileType: "png", + canReplaceMapContent: true + ) + } + Button("Farm Map") { + self.customMapOverlay = MapView.CustomMapOverlay( + mapName: "http://10.147.253.250:5050/local/map/", + tileType: "png", + canReplaceMapContent: true + ) + } + } + + }.listStyle(GroupedListStyle()) + .navigationBarTitle("Map Configuration", displayMode: NavigationBarItem.TitleDisplayMode.inline) + + ZStack { + + MapView( + region: self.$mapRegion, + customMapOverlay: self.customMapOverlay, + mapType: self.mapType, + zoomEnabled: self.zoomEnabled, + showZoomScale: self.showZoomScale, + zoomRange: (minHeight: self.useMinZoomBoundary ? self.minZoom : 0, maxHeight: self.useMaxZoomBoundary ? self.maxZoom : .infinity), + scrollEnabled: self.scrollEnabled, + scrollBoundaries: self.useScrollBoundaries ? self.scrollBoundaries : nil, + rotationEnabled: self.rotationEnabled, + showCompassWhenRotated: self.showCompassWhenRotated, + showUserLocation: self.showUserLocation, + userTrackingMode: self.userTrackingMode, + userLocation: self.$userLocation, + annotations: self.showAnnotations ? self.annotations : [], + overlays: self.showOverlays ? self.overlays : [] + ) + + VStack { + + Spacer() + + HStack { + if let userLocation = self.userLocation, self.showUserLocation { + VStack(alignment: .leading) { + Button("Center user location") { + self.mapRegion.center = userLocation + } + Text("User Location").bold() + Text("\(userLocation.latitude)") + Text("\(userLocation.longitude)") + } + } + + Spacer() + + VStack(alignment: .leading) { + Text("Map Center").bold() + Text("\(self.mapRegion.center.latitude)") + Text("\(self.mapRegion.center.longitude)") + } + } + + Picker("", selection: self.$mapType) { + Text("Standard").tag(MKMapType.standard) + Text("Muted Standard").tag(MKMapType.mutedStandard) + Text("Satellite").tag(MKMapType.satellite) + Text("Satellite Flyover").tag(MKMapType.satelliteFlyover) + Text("Hybrid").tag(MKMapType.hybrid) + Text("Hybrid Flyover").tag(MKMapType.hybridFlyover) + }.pickerStyle(SegmentedPickerStyle()) + + if self.showMapCenter { + Circle().frame(width: 8, height: 8).foregroundColor(.red) + } + + }.padding() + + }.navigationBarTitle("SwiftUI MapView", displayMode: NavigationBarItem.TitleDisplayMode.inline) + .ignoresSafeArea(edges: .bottom) + + } + + } + +} + + +public struct MapView_Previews: PreviewProvider { + + public static var previews: some View { + + MapViewDemo() + + } + +}*/ +#endif diff --git a/MeshtasticClient/Views/Nodes/NodeMap.swift b/MeshtasticClient/Views/Nodes/NodeMap.swift index 6fc93b3e..5d1ba62e 100644 --- a/MeshtasticClient/Views/Nodes/NodeMap.swift +++ b/MeshtasticClient/Views/Nodes/NodeMap.swift @@ -16,25 +16,100 @@ struct NodeMap: View { @Environment(\.managedObjectContext) var context @EnvironmentObject var bleManager: BLEManager - // @AppStorage("meshMapType") var meshMapType: String = "hybrid" - + @AppStorage("meshMapType") var type: String = "hybrid" + @AppStorage("meshMapCustomTileServer") var customTileServer: String = "" { + didSet { + if customTileServer == "" { + self.customMapOverlay = nil + } else { + self.customMapOverlay = MapView.CustomMapOverlay( + mapName: customTileServer, + tileType: "png", + canReplaceMapContent: true + ) + } + } + } + @State private var showLabels: Bool = false - @State private var annotationItems: [MapLocation] = [] - @FetchRequest( sortDescriptors: [NSSortDescriptor(keyPath: \NodeInfoEntity.lastHeard, ascending: false)], animation: .default) + //@State private var annotationItems: [MapLocation] = [] + //@FetchRequest( sortDescriptors: [NSSortDescriptor(keyPath: \NodeInfoEntity.lastHeard, ascending: false)], animation: .default) + //private var locationNodes: FetchedResults - private var locationNodes: FetchedResults + /*@State private var mapRegion: MKCoordinateRegion = MKCoordinateRegion( + center: CLLocationCoordinate2D( + latitude: -38.758247, + longitude: 175.360208 + ), + span: MKCoordinateSpan( + latitudeDelta: 0.01, + longitudeDelta: 0.01 + ) + )*/ + + @State private var customMapOverlay: MapView.CustomMapOverlay? = MapView.CustomMapOverlay( + mapName: UserDefaults.standard.string(forKey: "meshMapCustomTileServer"), + tileType: "png", + canReplaceMapContent: true + ) + //@State private var mapType: MKMapType = MKMapType.standard + + @State private var zoomEnabled: Bool = true + @State private var showZoomScale: Bool = true + @State private var useMinZoomBoundary: Bool = false + @State private var minZoom: Double = 0 + @State private var useMaxZoomBoundary: Bool = false + @State private var maxZoom: Double = 3000000 + + @State private var scrollEnabled: Bool = true + @State private var useScrollBoundaries: Bool = false + @State private var scrollBoundaries: MKCoordinateRegion = MKCoordinateRegion() + + @State private var rotationEnabled: Bool = true + @State private var showCompassWhenRotated: Bool = true + + @State private var showUserLocation: Bool = true + @State private var userTrackingMode: MKUserTrackingMode = MKUserTrackingMode.none + @State private var userLocation: CLLocationCoordinate2D? + + @State private var showAnnotations: Bool = true + @State private var annotations: [MKPointAnnotation] = [] + + @State private var showOverlays: Bool = true + @State private var overlays: [MapView.Overlay] = [] + + @State private var showMapCenter: Bool = false + var body: some View { - let location = LocationHelper.currentLocation + //let location = LocationHelper.currentLocation NavigationView { ZStack { - MapView(nodes: self.locationNodes)//.environmentObject(bleManager) + //MapView(nodes: self.locationNodes)//.environmentObject(bleManager) // } + MapView( + //region: self.$mapRegion, + customMapOverlay: self.customMapOverlay, + mapType: self.type, + zoomEnabled: self.zoomEnabled, + showZoomScale: self.showZoomScale, + zoomRange: (minHeight: self.useMinZoomBoundary ? self.minZoom : 0, maxHeight: self.useMaxZoomBoundary ? self.maxZoom : .infinity), + scrollEnabled: self.scrollEnabled, + scrollBoundaries: self.useScrollBoundaries ? self.scrollBoundaries : nil, + rotationEnabled: self.rotationEnabled, + showCompassWhenRotated: self.showCompassWhenRotated, + showUserLocation: self.showUserLocation, + userTrackingMode: self.userTrackingMode, + userLocation: self.$userLocation, + //annotations: self.annotations, + overlays: self.overlays + ) + .frame(maxHeight: .infinity) .ignoresSafeArea(.all, edges: [.leading, .trailing]) } diff --git a/MeshtasticClient/Views/Settings/AppSettings.swift b/MeshtasticClient/Views/Settings/AppSettings.swift index 98690e2a..ad654ee8 100644 --- a/MeshtasticClient/Views/Settings/AppSettings.swift +++ b/MeshtasticClient/Views/Settings/AppSettings.swift @@ -90,6 +90,12 @@ class UserSettings: ObservableObject { UserDefaults.standard.set(meshMapType, forKey: "meshMapType") } } + + @Published var meshMapCustomTileServer: String { + didSet { + UserDefaults.standard.set(meshMapCustomTileServer, forKey: "meshMapCustomTileServer") + } + } init() { @@ -100,6 +106,7 @@ class UserSettings: ObservableObject { 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" + self.meshMapCustomTileServer = UserDefaults.standard.string(forKey: "meshMapCustomTileServer") ?? "" } } @@ -171,12 +178,13 @@ struct AppSettings: View { } } Section(header: Text("MAP OPTIONS")) { - Picker("Map Type", selection: $userSettings.meshMapType) { + Picker("Base Map (Apple)", selection: $userSettings.meshMapType) { ForEach(MeshMapType.allCases) { map in Text(map.description) } } .pickerStyle(DefaultPickerStyle()) + TextField("Custom Tile Server", text: $userSettings.meshMapCustomTileServer) } } }