diff --git a/Meshtastic Client.xcodeproj/project.pbxproj b/Meshtastic Client.xcodeproj/project.pbxproj index ca4bcddd..980e4498 100644 --- a/Meshtastic Client.xcodeproj/project.pbxproj +++ b/Meshtastic Client.xcodeproj/project.pbxproj @@ -8,8 +8,12 @@ /* 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 */; }; + C9697FA72793F9FB00250207 /* MTAppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9697FA62793F9FB00250207 /* MTAppDelegate.swift */; }; C9A7BC1027759A9600760B50 /* PositionAnnotationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9A7BC0F27759A9600760B50 /* PositionAnnotationView.swift */; }; - DD17E5DD277D49D400010EC2 /* apponly.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD17E5DB277D49D400010EC2 /* apponly.pb.swift */; }; + C9A88B55278B503C00BD810A /* MapViewModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9A88B54278B503C00BD810A /* MapViewModule.swift */; }; + C9A88B57278B559900BD810A /* apponly.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9A88B56278B559900BD810A /* apponly.pb.swift */; }; DD17E5DE277D49D400010EC2 /* storeforward.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD17E5DC277D49D400010EC2 /* storeforward.pb.swift */; }; DD1BF2F92776FE2E008C8D2F /* UserMessageList.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1BF2F82776FE2E008C8D2F /* UserMessageList.swift */; }; DD23A50F26FD1B4400D9B90C /* PeripheralModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD23A50E26FD1B4400D9B90C /* PeripheralModel.swift */; }; @@ -73,8 +77,11 @@ /* Begin PBXFileReference section */ C9483F6C2773017500998F6B /* MapView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapView.swift; sourceTree = ""; }; + C9697F9C279336B700250207 /* LocalMBTileOverlay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalMBTileOverlay.swift; sourceTree = ""; }; + C9697FA62793F9FB00250207 /* MTAppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MTAppDelegate.swift; sourceTree = ""; }; C9A7BC0F27759A9600760B50 /* PositionAnnotationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PositionAnnotationView.swift; sourceTree = ""; }; - DD17E5DB277D49D400010EC2 /* apponly.pb.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = apponly.pb.swift; sourceTree = ""; }; + C9A88B54278B503C00BD810A /* MapViewModule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MapViewModule.swift; sourceTree = ""; }; + C9A88B56278B559900BD810A /* apponly.pb.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = apponly.pb.swift; sourceTree = ""; }; DD17E5DC277D49D400010EC2 /* storeforward.pb.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = storeforward.pb.swift; sourceTree = ""; }; DD1BF2F82776FE2E008C8D2F /* UserMessageList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserMessageList.swift; sourceTree = ""; }; DD23A50E26FD1B4400D9B90C /* PeripheralModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PeripheralModel.swift; sourceTree = ""; }; @@ -130,6 +137,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + C9697FA527933B8C00250207 /* SQLite in Frameworks */, DD5394FC276993AD00AD86B1 /* SwiftProtobuf in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -156,6 +164,8 @@ children = ( C9A7BC0E27759A6800760B50 /* Custom */, C9483F6C2773017500998F6B /* MapView.swift */, + C9A88B54278B503C00BD810A /* MapViewModule.swift */, + C9697F9C279336B700250207 /* LocalMBTileOverlay.swift */, ); path = Map; sourceTree = ""; @@ -206,7 +216,7 @@ DDAF8C5626ED07740058C060 /* Protobufs */ = { isa = PBXGroup; children = ( - DD17E5DB277D49D400010EC2 /* apponly.pb.swift */, + C9A88B56278B559900BD810A /* apponly.pb.swift */, DD17E5DC277D49D400010EC2 /* storeforward.pb.swift */, DDAF8C5E26ED09B50058C060 /* radioconfig.pb.swift */, DDAF8C6426ED0A490058C060 /* channel.pb.swift */, @@ -255,6 +265,7 @@ DDC2E18826CE24EE0042C5E4 /* Model */, DDC2E18926CE24F70042C5E4 /* Resources */, DDC2E15726CE248E0042C5E4 /* MeshtasticClientApp.swift */, + C9697FA62793F9FB00250207 /* MTAppDelegate.swift */, DDC2E16526CE248F0042C5E4 /* Info.plist */, DDC2E15D26CE248F0042C5E4 /* Preview Content */, ); @@ -380,6 +391,7 @@ name = MeshtasticClient; packageProductDependencies = ( DD5394FB276993AD00AD86B1 /* SwiftProtobuf */, + C9697FA427933B8C00250207 /* SQLite */, ); productName = MeshtasticClient; productReference = DDC2E15426CE248E0042C5E4 /* MeshtasticClient.app */; @@ -432,6 +444,7 @@ TargetAttributes = { DDC2E15326CE248E0042C5E4 = { CreatedOnToolsVersion = 12.5.1; + LastSwiftMigration = 1320; }; DDC2E16926CE248F0042C5E4 = { CreatedOnToolsVersion = 12.5.1; @@ -454,6 +467,7 @@ mainGroup = DDC2E14B26CE248E0042C5E4; packageReferences = ( DD5394FA276993AD00AD86B1 /* XCRemoteSwiftPackageReference "swift-protobuf" */, + C9697FA327933B8C00250207 /* XCRemoteSwiftPackageReference "SQLite.swift" */, ); productRefGroup = DDC2E15526CE248E0042C5E4 /* Products */; projectDirPath = ""; @@ -544,16 +558,19 @@ DD47E3D626F17ED900029299 /* CircleText.swift in Sources */, DDC2E18F26CE25FE0042C5E4 /* ContentView.swift in Sources */, DD17E5DE277D49D400010EC2 /* storeforward.pb.swift in Sources */, + C9A88B55278B503C00BD810A /* MapViewModule.swift in Sources */, DDAF8C6326ED0A230058C060 /* admin.pb.swift in Sources */, C9483F6D2773017500998F6B /* MapView.swift in Sources */, DDAF8C5826ED07FD0058C060 /* mesh.pb.swift in Sources */, + C9697FA72793F9FB00250207 /* MTAppDelegate.swift in Sources */, DD8169FB271F1F3A00F4AB02 /* MeshLog.swift in Sources */, DD2E65262767A01F00E45FC5 /* NodeDetail.swift in Sources */, + C9A88B57278B559900BD810A /* apponly.pb.swift in Sources */, DD47E3D926F3093800029299 /* MessageBubble.swift in Sources */, + C9697F9D279336B700250207 /* LocalMBTileOverlay.swift in Sources */, DD8169F9271F1A6100F4AB02 /* MeshLogger.swift in Sources */, DD539502276DAA6A00AD86B1 /* MapLocation.swift in Sources */, DDAF8C6726ED0C8C0058C060 /* remote_hardware.pb.swift in Sources */, - DD17E5DD277D49D400010EC2 /* apponly.pb.swift in Sources */, DDAF8C6526ED0A490058C060 /* channel.pb.swift in Sources */, DD47E3DD26F390A000029299 /* Messages.swift in Sources */, DDC2E15826CE248E0042C5E4 /* MeshtasticClientApp.swift in Sources */, @@ -716,9 +733,10 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES; + CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = MeshtasticClient/MeshtasticClient.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 17; + CURRENT_PROJECT_VERSION = 18; DEVELOPMENT_ASSET_PATHS = "\"MeshtasticClient/Preview Content\""; DEVELOPMENT_TEAM = GCH7VS5Y9R; ENABLE_PREVIEWS = YES; @@ -733,6 +751,7 @@ PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2,6"; }; @@ -744,9 +763,10 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES; + CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = MeshtasticClient/MeshtasticClient.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 17; + CURRENT_PROJECT_VERSION = 18; DEVELOPMENT_ASSET_PATHS = "\"MeshtasticClient/Preview Content\""; DEVELOPMENT_TEAM = GCH7VS5Y9R; ENABLE_PREVIEWS = YES; @@ -892,6 +912,14 @@ /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ + C9697FA327933B8C00250207 /* XCRemoteSwiftPackageReference "SQLite.swift" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/stephencelis/SQLite.swift.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 0.9.2; + }; + }; DD5394FA276993AD00AD86B1 /* XCRemoteSwiftPackageReference "swift-protobuf" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/apple/swift-protobuf.git"; @@ -903,6 +931,11 @@ /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ + C9697FA427933B8C00250207 /* SQLite */ = { + isa = XCSwiftPackageProductDependency; + package = C9697FA327933B8C00250207 /* XCRemoteSwiftPackageReference "SQLite.swift" */; + productName = SQLite; + }; DD5394FB276993AD00AD86B1 /* SwiftProtobuf */ = { isa = XCSwiftPackageProductDependency; package = DD5394FA276993AD00AD86B1 /* XCRemoteSwiftPackageReference "swift-protobuf" */; diff --git a/Meshtastic Client.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Meshtastic Client.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 1cb05e42..c0572681 100644 --- a/Meshtastic Client.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Meshtastic Client.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,6 +1,15 @@ { "object": { "pins": [ + { + "package": "SQLite.swift", + "repositoryURL": "https://github.com/stephencelis/SQLite.swift.git", + "state": { + "branch": null, + "revision": "60a65015f6402b7c34b9a924f755ca0a73afeeaa", + "version": "0.13.1" + } + }, { "package": "SwiftProtobuf", "repositoryURL": "https://github.com/apple/swift-protobuf.git", diff --git a/Meshtastic Client.xcodeproj/project.xcworkspace/xcuserdata/joshuapirihi.xcuserdatad/UserInterfaceState.xcuserstate b/Meshtastic Client.xcodeproj/project.xcworkspace/xcuserdata/joshuapirihi.xcuserdatad/UserInterfaceState.xcuserstate index c864b6bc..c932fa44 100644 Binary files a/Meshtastic Client.xcodeproj/project.xcworkspace/xcuserdata/joshuapirihi.xcuserdatad/UserInterfaceState.xcuserstate and b/Meshtastic Client.xcodeproj/project.xcworkspace/xcuserdata/joshuapirihi.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/MeshtasticClient/Helpers/BLEManager.swift b/MeshtasticClient/Helpers/BLEManager.swift index 87547c94..2545f807 100644 --- a/MeshtasticClient/Helpers/BLEManager.swift +++ b/MeshtasticClient/Helpers/BLEManager.swift @@ -811,6 +811,31 @@ class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeriph } fetchedNode[0].snr = decodedInfo.packet.rxSnr + + if let positionMessage = try? Position(serializedData: decodedInfo.packet.decoded.payload) { + let position = PositionEntity(context: context!) + position.latitudeI = positionMessage.latitudeI + position.longitudeI = positionMessage.longitudeI + position.altitude = positionMessage.altitude + position.batteryLevel = positionMessage.batteryLevel + position.time = Date(timeIntervalSince1970: TimeInterval(Int64(positionMessage.time))) + + let mutablePositions = fetchedNode[0].positions!.mutableCopy() as! NSMutableOrderedSet + mutablePositions.add(position) + + print("💾 Recieved a Position Packet") + + if position.coordinate == nil { + var newPostions = [PositionEntity]() + newPostions.append(position) + fetchedNode[0].positions? = NSOrderedSet(array: newPostions) + + } else { + + fetchedNode[0].positions = mutablePositions.copy() as? NSOrderedSet + } + } + } else { return diff --git a/MeshtasticClient/Info.plist b/MeshtasticClient/Info.plist index b5b09295..5d7d0870 100644 --- a/MeshtasticClient/Info.plist +++ b/MeshtasticClient/Info.plist @@ -6,6 +6,19 @@ $(DEVELOPMENT_LANGUAGE) CFBundleDisplayName Meshtastic + CFBundleDocumentTypes + + + CFBundleTypeName + MBTiles Map + LSHandlerRank + Default + LSItemContentTypes + + gvh.MeshtasticClient.mbtiles + + + CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier @@ -26,6 +39,8 @@ public.app-category.utilities LSRequiresIPhoneOS + LSSupportsOpeningDocumentsInPlace + NSBluetoothAlwaysUsageDescription We use bluetooth to connect to nearby Meshtastic Devices NSBluetoothPeripheralUsageDescription @@ -65,5 +80,29 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight + UISupportsDocumentBrowser + + UTImportedTypeDeclarations + + + UTTypeConformsTo + + public.data + + UTTypeDescription + MBTiles Map + UTTypeIconFiles + + UTTypeIdentifier + gvh.MeshtasticClient.mbtiles + UTTypeTagSpecification + + public.filename-extension + + mbtiles + + + + diff --git a/MeshtasticClient/MTAppDelegate.swift b/MeshtasticClient/MTAppDelegate.swift new file mode 100644 index 00000000..a59f9b27 --- /dev/null +++ b/MeshtasticClient/MTAppDelegate.swift @@ -0,0 +1,28 @@ +// +// AppDelegate.swift +// MeshtasticClient +// +// Created by Joshua Pirihi on 16/01/22. +// + +import Foundation +import UIKit + +class MTAppDelegate: NSObject, UIApplicationDelegate { + + func applicationDidFinishLaunching(_ application: UIApplication) { + } + + func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil + ) -> Bool { + // ... + return true + } + + func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool { + print("We received a file") + return true + } +} diff --git a/MeshtasticClient/MeshtasticClientApp.swift b/MeshtasticClient/MeshtasticClientApp.swift index f7222a60..5f99066d 100644 --- a/MeshtasticClient/MeshtasticClientApp.swift +++ b/MeshtasticClient/MeshtasticClientApp.swift @@ -4,6 +4,8 @@ import CoreData @main struct MeshtasticClientApp: App { + @UIApplicationDelegateAdaptor var delegate: MTAppDelegate + let persistenceController = PersistenceController.shared @ObservedObject private var bleManager: BLEManager = BLEManager.shared 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/LocalMBTileOverlay.swift b/MeshtasticClient/Views/Map/LocalMBTileOverlay.swift new file mode 100644 index 00000000..088f6365 --- /dev/null +++ b/MeshtasticClient/Views/Map/LocalMBTileOverlay.swift @@ -0,0 +1,117 @@ +// +// LocalMBTileOverlay.swift +// MeshtasticClient +// +// Created by Joshua Pirihi on 16/01/22. +// + +import UIKit +import MapKit +import SQLite + +extension MKMapRect { + init(coordinates: [CLLocationCoordinate2D]) { + self = MKMapRect() + var coordinates = coordinates + if !coordinates.isEmpty { + let first = coordinates.removeFirst() + var top = first.latitude + var bottom = first.latitude + var left = first.longitude + var right = first.longitude + coordinates.forEach { coordinate in + top = max(top, coordinate.latitude) + bottom = min(bottom, coordinate.latitude) + left = min(left, coordinate.longitude) + right = max(right, coordinate.longitude) + } + let topLeft = MKMapPoint(CLLocationCoordinate2D(latitude:top, longitude:left)) + let bottomRight = MKMapPoint(CLLocationCoordinate2D(latitude:bottom, longitude:right)) + self = MKMapRect(x:topLeft.x, y:topLeft.y, + width:bottomRight.x - topLeft.x, height:bottomRight.y - topLeft.y) + } + } +} + +class LocalMBTileOverlay: MKTileOverlay { + + var path: String! + + var mb: Connection! + + private var _boundingMapRect: MKMapRect! + override var boundingMapRect: MKMapRect { + get { + return _boundingMapRect + } + } + + init(mbTilePath path: String) { + + super.init(urlTemplate: nil) + self.path = path + + do { + self.mb = try Connection(self.path, readonly: true) + let metadata = Table("metadata") + + let name = Expression("name") + let value = Expression("value") + + let minZQuery = try mb.pluck(metadata.select(value).filter(name == "minzoom")) + self.minimumZ = Int(minZQuery![value])! + + let maxZQuery = try mb.pluck(metadata.select(value).filter(name == "maxzoom")) + self.maximumZ = Int(maxZQuery![value])! + + self.isGeometryFlipped = true + + let boundingBoxString = try mb.pluck(metadata.select(value).filter(name == "bounds")) + let boundCoords = boundingBoxString![value].split(separator: ",") + let coords = [ + CLLocationCoordinate2D(latitude: Double(boundCoords[1]) ?? 0, + longitude: Double(boundCoords[0]) ?? 0), + CLLocationCoordinate2D(latitude: Double(boundCoords[3]) ?? 0, + longitude: Double(boundCoords[2]) ?? 0) + ] + self._boundingMapRect = MKMapRect(coordinates: coords) + + + } catch { + //print("MAP ERROR \(error)") + } + + + } + + override func loadTile(at path: MKTileOverlayPath, result: @escaping (Data?, Error?) -> Void) { + + //} + + //override func loadTile(at path: MKTileOverlayPath) async throws -> Data { + + let tileX = Int64(path.x) + let tileY = Int64(path.y) + let tileZ = Int64(path.z) + + let tileData = Expression("tile_data") + let zoomLevel = Expression("zoom_level") + let tileColumn = Expression("tile_column") + let tileRow = Expression("tile_row") + + if let dataQuery = try? self.mb.pluck(Table("tiles").select(tileData).filter(zoomLevel == tileZ).filter(tileColumn == tileX).filter(tileRow == tileY)) { + + let data = Data(bytes: dataQuery[tileData].bytes, count: dataQuery[tileData].bytes.count)//dataQuery![tileData].bytes + + //return data + result(data, nil) + + } else { + print("💥 No tile here: x:\(tileX) y:\(tileY) z:\(tileZ)") + //return Data() + let error = NSError(domain: "LocalMBTileOverlay", code: 1, userInfo: ["reason": "no_tile"]) + result(nil, error) + } + } + +} 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..f7468edd --- /dev/null +++ b/MeshtasticClient/Views/Map/MapViewModule.swift @@ -0,0 +1,775 @@ +// +// 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 + + //var context: NSManagedObjectContext? + + //@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 + + @FetchRequest(sortDescriptors: [NSSortDescriptor(key: "time", ascending: false)], animation: .default) + private var positions: FetchedResults + + //@State private var locationNodes: [NodeInfoEntity] + + 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] = [] + //context: NSManagedObjectContext? = nil + ) { + //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)) + + /*Timer.scheduledTimer(withTimeInterval: 10, repeats: true) { timer in + 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) + } + } + }*/ + + 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 { + + if let tilePath = Bundle.main.path(forResource: "offline_map", ofType: "mbtiles") { + let overlay = LocalMBTileOverlay(mbTilePath: tilePath) + + overlay.canReplaceMapContent = false//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) + } + }*/ + + var displayedNodes: [Int64] = [] + for position in self.positions { + if position.nodePosition == nil || displayedNodes.contains(position.nodePosition!.num) || position.coordinate == nil { + continue + } + + let annotation = PositionAnnotation() + annotation.coordinate = position.coordinate! + annotation.title = position.nodePosition!.user?.longName ?? "Unknown" + annotation.shortName = position.nodePosition!.user?.shortName?.uppercased() ?? "???" + + mapView.addAnnotation(annotation) + + displayedNodes.append(position.nodePosition!.num) + } + + 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..c9e99e80 100644 --- a/MeshtasticClient/Views/Nodes/NodeMap.swift +++ b/MeshtasticClient/Views/Nodes/NodeMap.swift @@ -16,30 +16,108 @@ 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: "offlinemap", + 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? = LocationHelper.currentLocation + + @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 + //self.$userLocation = 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, + //locationNodes: self.locationNodes.map({ nodeinfo in return nodeinfo }), + overlays: self.overlays + //context: self.context + ) + .frame(maxHeight: .infinity) .ignoresSafeArea(.all, edges: [.leading, .trailing]) } .navigationTitle("Mesh Map") .navigationBarTitleDisplayMode(.inline) + .navigationBarItems(trailing: ZStack { diff --git a/MeshtasticClient/Views/Settings/AppSettings.swift b/MeshtasticClient/Views/Settings/AppSettings.swift index c3ec7c64..686a333d 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") ?? "" } } @@ -164,6 +171,7 @@ struct AppSettings: View { } } .pickerStyle(DefaultPickerStyle()) + // TextField("Custom Tile Server", text: $userSettings.meshMapCustomTileServer) } Section(header: Text("DEBUG")) { // Toggle(isOn: $userSettings.meshActivityLog) { @@ -178,7 +186,6 @@ struct AppSettings: View { .listRowSeparator(.visible) } } - } } .navigationTitle("App Settings")