Merge pull request #26 from garthvh/map_custom_tiles_and_caching

Mapping updates
This commit is contained in:
Garth Vander Houwen 2022-01-20 18:24:10 -08:00 committed by GitHub
commit 4766873ad1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 1169 additions and 42 deletions

View file

@ -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 = "<group>"; };
C9697F9C279336B700250207 /* LocalMBTileOverlay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalMBTileOverlay.swift; sourceTree = "<group>"; };
C9697FA62793F9FB00250207 /* MTAppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MTAppDelegate.swift; sourceTree = "<group>"; };
C9A7BC0F27759A9600760B50 /* PositionAnnotationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PositionAnnotationView.swift; sourceTree = "<group>"; };
DD17E5DB277D49D400010EC2 /* apponly.pb.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = apponly.pb.swift; sourceTree = "<group>"; };
C9A88B54278B503C00BD810A /* MapViewModule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MapViewModule.swift; sourceTree = "<group>"; };
C9A88B56278B559900BD810A /* apponly.pb.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = apponly.pb.swift; sourceTree = "<group>"; };
DD17E5DC277D49D400010EC2 /* storeforward.pb.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = storeforward.pb.swift; sourceTree = "<group>"; };
DD1BF2F82776FE2E008C8D2F /* UserMessageList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserMessageList.swift; sourceTree = "<group>"; };
DD23A50E26FD1B4400D9B90C /* PeripheralModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PeripheralModel.swift; sourceTree = "<group>"; };
@ -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 = "<group>";
@ -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" */;

View file

@ -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",

View file

@ -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

View file

@ -6,6 +6,19 @@
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>Meshtastic</string>
<key>CFBundleDocumentTypes</key>
<array>
<dict>
<key>CFBundleTypeName</key>
<string>MBTiles Map</string>
<key>LSHandlerRank</key>
<string>Default</string>
<key>LSItemContentTypes</key>
<array>
<string>gvh.MeshtasticClient.mbtiles</string>
</array>
</dict>
</array>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
@ -26,6 +39,8 @@
<string>public.app-category.utilities</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>LSSupportsOpeningDocumentsInPlace</key>
<false/>
<key>NSBluetoothAlwaysUsageDescription</key>
<string>We use bluetooth to connect to nearby Meshtastic Devices</string>
<key>NSBluetoothPeripheralUsageDescription</key>
@ -65,5 +80,29 @@
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportsDocumentBrowser</key>
<false/>
<key>UTImportedTypeDeclarations</key>
<array>
<dict>
<key>UTTypeConformsTo</key>
<array>
<string>public.data</string>
</array>
<key>UTTypeDescription</key>
<string>MBTiles Map</string>
<key>UTTypeIconFiles</key>
<array/>
<key>UTTypeIdentifier</key>
<string>gvh.MeshtasticClient.mbtiles</string>
<key>UTTypeTagSpecification</key>
<dict>
<key>public.filename-extension</key>
<array>
<string>mbtiles</string>
</array>
</dict>
</dict>
</array>
</dict>
</plist>

View file

@ -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
}
}

View file

@ -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

View file

@ -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)
}
}
}

View file

@ -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<String>("name")
let value = Expression<String>("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<SQLite.Blob>("tile_data")
let zoomLevel = Expression<Int64>("zoom_level")
let tileColumn = Expression<Int64>("tile_column")
let tileRow = Expression<Int64>("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)
}
}
}

View file

@ -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<NodeInfoEntity>
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

View file

@ -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<NodeInfoEntity>
@FetchRequest(sortDescriptors: [NSSortDescriptor(key: "time", ascending: false)], animation: .default)
private var positions: FetchedResults<PositionEntity>
//@State private var locationNodes: [NodeInfoEntity]
public init(
//region: Binding<MKCoordinateRegion> = .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<CLLocationCoordinate2D?> = .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

View file

@ -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<NodeInfoEntity>
private var locationNodes: FetchedResults<NodeInfoEntity>
/*@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 {

View file

@ -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")