diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index 4a34a19a..6de0398b 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -98,6 +98,11 @@ DDB6ABE228B13FB500384BA1 /* PositionConfigEnums.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDB6ABE128B13FB500384BA1 /* PositionConfigEnums.swift */; }; DDB6ABE428B13FFF00384BA1 /* DisplayEnums.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDB6ABE328B13FFF00384BA1 /* DisplayEnums.swift */; }; DDB6ABE628B1406100384BA1 /* LoraConfigEnums.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDB6ABE528B1406100384BA1 /* LoraConfigEnums.swift */; }; + DDB75A0F2A05920E006ED576 /* FileManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDB75A0E2A05920E006ED576 /* FileManager.swift */; }; + DDB75A112A059258006ED576 /* Url.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDB75A102A059258006ED576 /* Url.swift */; }; + DDB75A142A0593E2006ED576 /* OfflineTileManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDB75A132A0593E2006ED576 /* OfflineTileManager.swift */; }; + DDB75A162A0594AD006ED576 /* TileOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDB75A152A0594AD006ED576 /* TileOverlay.swift */; }; + DDB75A182A05975A006ED576 /* TilesDownloadView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDB75A172A05975A006ED576 /* TilesDownloadView.swift */; }; DDC2E15826CE248E0042C5E4 /* MeshtasticApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC2E15726CE248E0042C5E4 /* MeshtasticApp.swift */; }; DDC2E15C26CE248F0042C5E4 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DDC2E15B26CE248F0042C5E4 /* Assets.xcassets */; }; DDC2E15F26CE248F0042C5E4 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DDC2E15E26CE248F0042C5E4 /* Preview Assets.xcassets */; }; @@ -281,6 +286,11 @@ DDB6ABE328B13FFF00384BA1 /* DisplayEnums.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisplayEnums.swift; sourceTree = ""; }; DDB6ABE528B1406100384BA1 /* LoraConfigEnums.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoraConfigEnums.swift; sourceTree = ""; }; DDB759E12A04B264006ED576 /* MeshtasticDataModelV12.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV12.xcdatamodel; sourceTree = ""; }; + DDB75A0E2A05920E006ED576 /* FileManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileManager.swift; sourceTree = ""; }; + DDB75A102A059258006ED576 /* Url.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Url.swift; sourceTree = ""; }; + DDB75A132A0593E2006ED576 /* OfflineTileManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OfflineTileManager.swift; sourceTree = ""; }; + DDB75A152A0594AD006ED576 /* TileOverlay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TileOverlay.swift; sourceTree = ""; }; + DDB75A172A05975A006ED576 /* TilesDownloadView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TilesDownloadView.swift; sourceTree = ""; }; DDBA45EC299ED78100DEEDDC /* MeshtasticDataModelV8.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV8.xcdatamodel; sourceTree = ""; }; DDC2E15426CE248E0042C5E4 /* Meshtastic.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Meshtastic.app; sourceTree = BUILT_PRODUCTS_DIR; }; DDC2E15726CE248E0042C5E4 /* MeshtasticApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeshtasticApp.swift; sourceTree = ""; }; @@ -544,6 +554,16 @@ path = Protobufs; sourceTree = ""; }; + DDB75A122A0593CD006ED576 /* Map */ = { + isa = PBXGroup; + children = ( + DDB75A132A0593E2006ED576 /* OfflineTileManager.swift */, + DDB75A152A0594AD006ED576 /* TileOverlay.swift */, + DDB75A172A05975A006ED576 /* TilesDownloadView.swift */, + ); + path = Map; + sourceTree = ""; + }; DDC2E14B26CE248E0042C5E4 = { isa = PBXGroup; children = ( @@ -677,6 +697,7 @@ DDC2E1A526CEB32B0042C5E4 /* Helpers */ = { isa = PBXGroup; children = ( + DDB75A122A0593CD006ED576 /* Map */, DDAF8C5226EB1DF10058C060 /* BLEManager.swift */, DDC2E1A626CEB3400042C5E4 /* LocationHelper.swift */, DD913638270DFF4C00D7ACF3 /* LocalNotificationManager.swift */, @@ -716,6 +737,8 @@ DDDB444729F8A9C900EE2349 /* String.swift */, DDDB444F29F8AC9C00EE2349 /* UIImage.swift */, DDDB443F29F79AB000EE2349 /* UserDefaults.swift */, + DDB75A0E2A05920E006ED576 /* FileManager.swift */, + DDB75A102A059258006ED576 /* Url.swift */, ); path = Extensions; sourceTree = ""; @@ -948,6 +971,7 @@ DD3501892852FC3B000FC853 /* Settings.swift in Sources */, DDDB443629F6287000EE2349 /* MapButtons.swift in Sources */, DD5D0A9C2931B9F200F7EA61 /* EthernetModes.swift in Sources */, + DDB75A182A05975A006ED576 /* TilesDownloadView.swift in Sources */, DD5E5203298EE33B00D21B61 /* config.pb.swift in Sources */, DD798B072915928D005217CD /* ChannelMessageList.swift in Sources */, DDA6B2EB28420A7B003E8C16 /* NodeAnnotation.swift in Sources */, @@ -955,6 +979,7 @@ DD5394FE276BA0EF00AD86B1 /* PositionEntityExtension.swift in Sources */, DD913639270DFF4C00D7ACF3 /* LocalNotificationManager.swift in Sources */, DDDB444C29F8AAA600EE2349 /* Color.swift in Sources */, + DDB75A0F2A05920E006ED576 /* FileManager.swift in Sources */, DD4F23CD28779A3C001D37CB /* EnvironmentMetricsLog.swift in Sources */, DD41A61529AB0035003C5A37 /* NodeWeatherForecast.swift in Sources */, DDB6ABD628AE742000384BA1 /* BluetoothConfig.swift in Sources */, @@ -979,6 +1004,7 @@ DD2160AF28C5552500C17253 /* MQTTConfig.swift in Sources */, DDDB444229F8A88700EE2349 /* Double.swift in Sources */, DD5E520F298EE33B00D21B61 /* cannedmessages.pb.swift in Sources */, + DDB75A162A0594AD006ED576 /* TileOverlay.swift in Sources */, DDF924CA26FBB953009FE055 /* ConnectedDevice.swift in Sources */, DD3CC6BE28E4CD9800FA9159 /* BatteryGauge.swift in Sources */, DD6193772862F90F00E59241 /* CannedMessagesConfig.swift in Sources */, @@ -1016,6 +1042,7 @@ DD8ED9C8289CE4B900B3B0AB /* RoutingError.swift in Sources */, DD5E5202298EE33B00D21B61 /* admin.pb.swift in Sources */, DD964FC62975DBFD007C176F /* QueryCoreData.swift in Sources */, + DDB75A112A059258006ED576 /* Url.swift in Sources */, DD8169FB271F1F3A00F4AB02 /* MeshLog.swift in Sources */, DD8ED9C52898D51F00B3B0AB /* NetworkConfig.swift in Sources */, DDC3B274283F411B00AC321C /* LastHeardText.swift in Sources */, @@ -1025,6 +1052,7 @@ DD86D4112881D16900BAEB7A /* WriteCsvFile.swift in Sources */, DDDB445029F8AC9C00EE2349 /* UIImage.swift in Sources */, DD86D40F2881BE4C00BAEB7A /* CsvDocument.swift in Sources */, + DDB75A142A0593E2006ED576 /* OfflineTileManager.swift in Sources */, DDA6B2E928419CF2003E8C16 /* MeshPackets.swift in Sources */, DDCE4E2C2869F92900BE9F8F /* UserConfig.swift in Sources */, DD6193752862F6E600E59241 /* ExternalNotificationConfig.swift in Sources */, diff --git a/Meshtastic/Extensions/FileManager.swift b/Meshtastic/Extensions/FileManager.swift new file mode 100644 index 00000000..36e58395 --- /dev/null +++ b/Meshtastic/Extensions/FileManager.swift @@ -0,0 +1,65 @@ +// +// FileManager.swift +// Meshtastic +// +// Copyright(c) Garth Vander Houwen 5/5/23. +// +import Foundation + +let allocatedSizeResourceKeys: Set = [ + .isRegularFileKey, + .fileAllocatedSizeKey, + .totalFileAllocatedSizeKey, +] + +public extension FileManager { + + /// Calculate the allocated size of a directory and all its contents on the volume. + /// + /// As there's no simple way to get this information from the file system the method + /// has to crawl the entire hierarchy, accumulating the overall sum on the way. + /// The resulting value is roughly equivalent with the amount of bytes + /// that would become available on the volume if the directory would be deleted. + /// + /// - note: There are a couple of oddities that are not taken into account (like symbolic links, meta data of + /// directories, hard links, ...). + func allocatedSizeOfDirectory(at directoryURL: URL) -> String { + + // The error handler simply stores the error and stops traversal + var enumeratorError: Error? = nil + func errorHandler(_: URL, error: Error) -> Bool { + enumeratorError = error + return false + } + + // We have to enumerate all directory contents, including subdirectories. + let enumerator = self.enumerator(at: directoryURL, + includingPropertiesForKeys: Array(allocatedSizeResourceKeys), + options: [], + errorHandler: errorHandler)! + + // We'll sum up content size here: + var accumulatedSize: UInt64 = 0 + + // Perform the traversal. + for item in enumerator { + + // Bail out on errors from the errorHandler. + if enumeratorError != nil { break } + + // Add up individual file sizes. + guard let contentItemURL = item as? URL else { continue } + do { + accumulatedSize += try contentItemURL.regularFileAllocatedSize() + } catch { + print("❤️ \(error.localizedDescription)") + } + + } + if let error = enumeratorError { print("❤️ AllocatedSizeOfDirectory enumeratorError = \(error.localizedDescription)") } + + return Double(accumulatedSize).toBytes + + } + +} diff --git a/Meshtastic/Extensions/Url.swift b/Meshtastic/Extensions/Url.swift new file mode 100644 index 00000000..0cac18a7 --- /dev/null +++ b/Meshtastic/Extensions/Url.swift @@ -0,0 +1,21 @@ +// +// Url.swift +// Meshtastic +// +// Copyright(c) Garth Vander Houwen 5/5/23. +// + +import Foundation + +extension URL { + + func regularFileAllocatedSize() throws -> UInt64 { + let resourceValues = try self.resourceValues(forKeys: allocatedSizeResourceKeys) + + guard resourceValues.isRegularFile ?? false else { + return 0 + } + + return UInt64(resourceValues.totalFileAllocatedSize ?? resourceValues.fileAllocatedSize ?? 0) + } +} diff --git a/Meshtastic/Helpers/Map/OfflineTileManager.swift b/Meshtastic/Helpers/Map/OfflineTileManager.swift new file mode 100644 index 00000000..c6084e02 --- /dev/null +++ b/Meshtastic/Helpers/Map/OfflineTileManager.swift @@ -0,0 +1,161 @@ +// +// OfflineTileManager.swift +// Meshtastic +// +// Copyright(c) Garth Vander Houwen 4/23/23. +// + +import Foundation +import MapKit + +class OfflineTileManager: ObservableObject { + + enum DownloadStatus { + case download, downloading, downloaded + } + + static let shared = OfflineTileManager() + + init() { + print("Documents Directory = \(documentsDirectory)") + createDirectoriesIfNecessary() + } + + // MARK: - Private properties + private var overlay: MKTileOverlay { MKTileOverlay(urlTemplate: UserDefaults.mapTileServer) } + + private var documentsDirectory: URL { fileManager.urls(for: .documentDirectory, in: .userDomainMask).first! } + + private let fileManager = FileManager.default + + // MARK: - Public property + @Published var progress: Float = 0 + @Published var status: DownloadStatus = .download + + // MARK: - Public methods + func getAllDownloadedSize() -> String { + fileManager.allocatedSizeOfDirectory(at: documentsDirectory.appendingPathComponent("tiles")) + } + + func hasBeenDownloaded(for boundingBox: MKMapRect) -> Bool { + getEstimatedDownloadSize(for: boundingBox) == 0 + } + + func getEstimatedDownloadSize(for boundingBox: MKMapRect) -> Double { + let paths = self.computeTileOverlayPaths(boundingBox: boundingBox) + let count = self.filterTilesAlreadyExisting(paths: paths).count + let size: Double = 30000 // Bytes (average size) + return Double(count) * size + } + + func getDownloadedSize(for boundingBox: MKMapRect) -> Double { + let paths = self.computeTileOverlayPaths(boundingBox: boundingBox) + var accumulatedSize: UInt64 = 0 + for path in paths { + let file = "tiles/z\(path.z)x\(path.x)y\(path.y).png" + let url = documentsDirectory.appendingPathComponent(file) + accumulatedSize += (try? url.regularFileAllocatedSize()) ?? 0 + } + return Double(accumulatedSize) + } + + func removeAll() { + try? fileManager.removeItem(at: documentsDirectory.appendingPathComponent("tiles")) + createDirectoriesIfNecessary() + } + + func remove(for boundingBox: MKMapRect) { + let paths = self.computeTileOverlayPaths(boundingBox: boundingBox) + for path in paths { + let file = "tiles/z\(path.z)x\(path.x)y\(path.y).png" + let url = documentsDirectory.appendingPathComponent(file) + try? fileManager.removeItem(at: url) + } + self.status = .download + } + + /// Download and persist all tiles within the boundingBox + func download(boundingBox: MKMapRect, name: String) { + NetworkManager.shared.runIfNetwork { + self.status = .downloading + self.progress = 0.01 + let paths = self.computeTileOverlayPaths(boundingBox: boundingBox) + let filteredPaths = self.filterTilesAlreadyExisting(paths: paths) + for i in 0.. URL { + let file = "z\(path.z)x\(path.x)y\(path.y).png" + // Check is tile is already available + let tilesUrl = documentsDirectory.appendingPathComponent("tiles").appendingPathComponent(file) + if fileManager.fileExists(atPath: tilesUrl.path){ + return tilesUrl + } else { + if !UserDefaults.enableOfflineMaps { // Get and persist newTile + return persistLocally(path: path) + } else { // Else display empty tile (transparent over Maps tiles) + return Bundle.main.url(forResource: "alpha", withExtension: "png")! + } + } + } + + // MARK: - Private methods + private func computeTileOverlayPaths(boundingBox box: MKMapRect, maxZ: Int = 17) -> [MKTileOverlayPath] { + var paths = [MKTileOverlayPath]() + for z in 1...maxZ { + let topLeft = tranformCoordinate(coordinates: MKMapPoint(x: box.minX, y: box.minY).coordinate, zoom: z) + let topRight = tranformCoordinate(coordinates: MKMapPoint(x: box.maxX, y: box.minY).coordinate, zoom: z) + let bottomLeft = tranformCoordinate(coordinates: MKMapPoint(x: box.minX, y: box.maxY).coordinate, zoom: z) + for x in topLeft.x...topRight.x { + for y in topLeft.y...bottomLeft.y { + paths.append(MKTileOverlayPath(x: x, y: y, z: z, contentScaleFactor: 2)) + } + } + } + return paths + } + + private func tranformCoordinate(coordinates: CLLocationCoordinate2D , zoom: Int) -> TileCoordinates { + let lng = coordinates.longitude + let lat = coordinates.latitude + let tileX = Int(floor((lng + 180) / 360.0 * pow(2.0, Double(zoom)))) + let tileY = Int(floor((1 - log( tan( lat * Double.pi / 180.0 ) + 1 / cos( lat * Double.pi / 180.0 )) / Double.pi ) / 2 * pow(2.0, Double(zoom)))) + return (tileX, tileY, zoom) + } + + @discardableResult private func persistLocally(path: MKTileOverlayPath) -> URL { + let url = overlay.url(forTilePath: path) + let file = "tiles/z\(path.z)x\(path.x)y\(path.y).png" + let filename = documentsDirectory.appendingPathComponent(file) + do { + let data = try Data(contentsOf: url) + try data.write(to: filename) + } catch { + print("❤️ PersistLocallyError = \(error)") + } + return url + } + + private func filterTilesAlreadyExisting(paths: [MKTileOverlayPath]) -> [MKTileOverlayPath] { + paths.filter { + let file = "z\($0.z)x\($0.x)y\($0.y).png" + let tilesPath = documentsDirectory.appendingPathComponent("tiles").appendingPathComponent(file).path + return !fileManager.fileExists(atPath: tilesPath) + } + } + + private func createDirectoriesIfNecessary() { + let tiles = documentsDirectory.appendingPathComponent("tiles") + try? fileManager.createDirectory(at: tiles, withIntermediateDirectories: true, attributes: [:]) + } + +} diff --git a/Meshtastic/Helpers/Map/TileOverlay.swift b/Meshtastic/Helpers/Map/TileOverlay.swift new file mode 100644 index 00000000..020e30a9 --- /dev/null +++ b/Meshtastic/Helpers/Map/TileOverlay.swift @@ -0,0 +1,15 @@ +// +// TileOverlay.swift +// Meshtastic +// +// Copyright(c) Garth Vander Houwen 5/5/23. +// + +import Foundation +import MapKit + +typealias TileCoordinates = (x: Int, y: Int, z: Int) + +class TileOverlay: MKTileOverlay { + override func url(forTilePath path: MKTileOverlayPath) -> URL { OfflineTileManager.shared.getTileOverlay(for: path) } +} diff --git a/Meshtastic/Helpers/Map/TilesDownloadView.swift b/Meshtastic/Helpers/Map/TilesDownloadView.swift new file mode 100644 index 00000000..2fc438d0 --- /dev/null +++ b/Meshtastic/Helpers/Map/TilesDownloadView.swift @@ -0,0 +1,87 @@ +// +// TilesDownloadView.swift +// Meshtastic +// +// Copyright © Garth Vander Houwen 5/5/23. +// + +import SwiftUI +import MapKit + +struct TilesDownloadView: View { + + @ObservedObject var tileManager = OfflineTileManager.shared + @State private var showAlert = false + @State var otherDownloadInProgress = false + + var boundingBox: MKMapRect + var name: String + + + var body: some View { + + Button(action: { + if self.tileManager.status == .download { + //Feedback.selected() + self.tileManager.download(boundingBox: self.boundingBox, name: self.name) + } else if self.tileManager.status == .downloaded { + //Feedback.selected() + self.showAlert = true + } + }) { + HStack() { + if tileManager.status == .downloaded { + Image(systemName: "trash") + .accentColor(.red) + } else { + Image(systemName: "map") + } + + VStack(alignment: .leading) { + if tileManager.status == .download { + Text("\("map.tiles.download".localized) (\(tileManager.getEstimatedDownloadSize(for: boundingBox).toBytes))") + } else if tileManager.status == .downloading { + Text("\("map.tiles.downloading".localized) (\(tileManager.getEstimatedDownloadSize(for: boundingBox).toBytes) \("Left".localized))") + } else { + Text("\("map.tiles.delete".localized) (\(tileManager.getDownloadedSize(for: boundingBox).toBytes))") + .accentColor(.red) + } + if tileManager.status == .downloading { + ProgressView(value: tileManager.progress) + .frame(height: 10) + } + } + Spacer() + } + //.isHidden(otherDownloadInProgress, remove: true) + } + .onAppear { + guard self.tileManager.status != .downloading else { + self.otherDownloadInProgress = true + return + } + self.tileManager.status = self.tileManager.hasBeenDownloaded(for: self.boundingBox) ? .downloaded : .download + } + .actionSheet(isPresented: $showAlert) { + ActionSheet( + title: Text("\("Delete".localized) (\(self.tileManager.getDownloadedSize(for: boundingBox).toBytes))"), + message: Text("DeleteTiles".localized), + buttons: [ + .destructive(Text("Delete".localized), action: { self.tileManager.remove(for: self.boundingBox) }), + .cancel(Text("Cancel".localized)) + ] + ) + } + } +} + +// MARK: Previews +struct TilesRow_Previews: PreviewProvider { + + static var previews: some View { + + TilesDownloadView(boundingBox: MKMapRect(), name: "test") + .previewLayout(.fixed(width: 300, height: 80)) + .environment(\.colorScheme, .light) + } +} diff --git a/Meshtastic/Views/Map/Custom/MapViewSwiftUI.swift b/Meshtastic/Views/Map/Custom/MapViewSwiftUI.swift index cefc0060..373883c5 100644 --- a/Meshtastic/Views/Map/Custom/MapViewSwiftUI.swift +++ b/Meshtastic/Views/Map/Custom/MapViewSwiftUI.swift @@ -15,6 +15,7 @@ struct MapViewSwiftUI: UIViewRepresentable { var onLongPress: (_ waypointCoordinate: CLLocationCoordinate2D) -> Void var onWaypointEdit: (_ waypointId: Int ) -> Void + @Binding var visibleMapRect: MKMapRect let mapView = MKMapView() // Parameters let positions: [PositionEntity] @@ -23,6 +24,7 @@ struct MapViewSwiftUI: UIViewRepresentable { let userTrackingMode: MKUserTrackingMode let showNodeHistory: Bool let showRouteLines: Bool + // Offline Map Tiles @AppStorage("lastUpdatedLocalMapFile") private var lastUpdatedLocalMapFile = 0 @State private var loadedLastUpdatedLocalMapFile = 0 @@ -96,9 +98,12 @@ struct MapViewSwiftUI: UIViewRepresentable { func updateUIView(_ mapView: MKMapView, context: Context) { mapView.mapType = mapViewType - + //visibleMapRect = mapView.visibleMapRect + // Offline maps and tile server settings - if UserDefaults.enableOfflineMaps { + if false {// UserDefaults.enableOfflineMaps { + + visibleMapRect = mapView.visibleMapRect if UserDefaults.mapTileServer.count > 0 { tileRenderer?.alpha = 0.0 @@ -214,6 +219,7 @@ struct MapViewSwiftUI: UIViewRepresentable { } func makeCoordinator() -> MapCoordinator { + //return Coordinator(self) return Coordinator(self) } @@ -232,6 +238,11 @@ struct MapViewSwiftUI: UIViewRepresentable { self.parent.mapView.addGestureRecognizer(longPressRecognizer) } + func mapViewDidChangeVisibleRegion(_ mapView: MKMapView) { + + print (mapView.visibleMapRect) + } + func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? { switch annotation { @@ -402,18 +413,17 @@ struct MapViewSwiftUI: UIViewRepresentable { public func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer { - if let tileOverlay = overlay as? MKTileOverlay { - return MKTileOverlayRenderer(tileOverlay: tileOverlay) - } else { - if let routePolyline = overlay as? MKPolyline { - - let titleString = routePolyline.title ?? "0" - let renderer = MKPolylineRenderer(polyline: routePolyline) - renderer.strokeColor = UIColor(hex: UInt32(titleString) ?? 0) - renderer.lineWidth = 8 - return renderer - } - return MKOverlayRenderer() + switch overlay { + case let overlay as MKTileOverlay: + return MKTileOverlayRenderer(tileOverlay: overlay) + case let polyline as MKPolyline: + let polylineRenderer = MKPolylineRenderer(overlay: overlay) + let titleString = polyline.title ?? "0" + let renderer = MKPolylineRenderer(polyline: polyline) + renderer.strokeColor = UIColor(hex: UInt32(titleString) ?? 0) + renderer.lineWidth = 8 + return polylineRenderer + default: return MKOverlayRenderer() } } } diff --git a/Meshtastic/Views/Nodes/NodeDetail.swift b/Meshtastic/Views/Nodes/NodeDetail.swift index 47587417..4e2b96c3 100644 --- a/Meshtastic/Views/Nodes/NodeDetail.swift +++ b/Meshtastic/Views/Nodes/NodeDetail.swift @@ -17,6 +17,7 @@ struct NodeDetail: View { @AppStorage("meshMapShowNodeHistory") private var meshMapShowNodeHistory = false @AppStorage("meshMapShowRouteLines") private var meshMapShowRouteLines = false @State private var mapType: MKMapType = .standard + @State var mapRect: MKMapRect = MKMapRect() @State var waypointCoordinate: WaypointCoordinate? @State var editingWaypoint: Int = 0 @State private var loadedWeather: Bool = false @@ -68,7 +69,10 @@ struct NodeDetail: View { if wpId > 0 { waypointCoordinate = WaypointCoordinate(id: .init(), coordinate: nil, waypointId: Int64(wpId)) } - }, positions: lastTenThousand, waypoints: Array(waypoints), + }, + visibleMapRect: $mapRect, + positions: lastTenThousand, + waypoints: Array(waypoints), mapViewType: mapType, userTrackingMode: MKUserTrackingMode.none, showNodeHistory: meshMapShowNodeHistory, diff --git a/Meshtastic/Views/Nodes/NodeMap.swift b/Meshtastic/Views/Nodes/NodeMap.swift index d0f4f418..05a7aeb1 100644 --- a/Meshtastic/Views/Nodes/NodeMap.swift +++ b/Meshtastic/Views/Nodes/NodeMap.swift @@ -21,6 +21,7 @@ struct NodeMap: View { @State var enableMapNodeHistoryPins: Bool = UserDefaults.enableMapNodeHistoryPins @State var enableOfflineMaps: Bool = UserDefaults.enableOfflineMaps @State var mapTileServer: String = UserDefaults.mapTileServer + @State var mapRect: MKMapRect = MKMapRect() @FetchRequest(sortDescriptors: [NSSortDescriptor(key: "time", ascending: true)], predicate: NSPredicate(format: "time >= %@ && nodePosition != nil", Calendar.current.startOfDay(for: Date()) as NSDate), animation: .none) @@ -32,6 +33,7 @@ struct NodeMap: View { ), animation: .none) private var waypoints: FetchedResults + @State var mapType: MKMapType = .standard @State var selectedTracking: UserTrackingModes = .none @State var selectedTileServer: MapTileServerLinks = .wikimedia @@ -57,6 +59,7 @@ struct NodeMap: View { waypointCoordinate = WaypointCoordinate(id: .init(), coordinate: nil, waypointId: Int64(wpId)) } }, + visibleMapRect: $mapRect, positions: Array(positions), waypoints: Array(waypoints), mapViewType: mapType, @@ -143,7 +146,7 @@ struct NodeMap: View { .foregroundColor(.gray) if UserDefaults.enableOfflineMaps { - HStack { + VStack { // Picker("Tile Servers", selection: $selectedTileServer) { // ForEach(MapTileServerLinks.allCases) { ts in // Text(ts.description) @@ -156,16 +159,19 @@ struct NodeMap: View { // mapTileServer = selectedTileServer.tileUrl // } - Label("Tile Server", systemImage: "square.grid.3x2") - TextField( - "Tile Server", - text: $mapTileServer, - axis: .vertical - ) - .foregroundColor(.gray) - .font(.caption) - .onChange(of: (mapTileServer)) { newMapTileServer in - UserDefaults.mapTileServer = newMapTileServer + TilesDownloadView(boundingBox: mapRect, name: "All tiles") + HStack { + Label("Tile Server", systemImage: "square.grid.3x2") + TextField( + "Tile Server", + text: $mapTileServer, + axis: .vertical + ) + .foregroundColor(.gray) + .font(.caption) + .onChange(of: (mapTileServer)) { newMapTileServer in + UserDefaults.mapTileServer = newMapTileServer + } } } .keyboardType(.asciiCapable) diff --git a/Meshtastic/Views/Settings/UserConfig.swift b/Meshtastic/Views/Settings/UserConfig.swift index f920527e..a61d4228 100644 --- a/Meshtastic/Views/Settings/UserConfig.swift +++ b/Meshtastic/Views/Settings/UserConfig.swift @@ -79,7 +79,7 @@ struct UserConfig: View { }) .foregroundColor(.gray) } - .keyboardType(.asciiCapable) + .keyboardType(.default) .disableAutocorrection(true) Text("The last 4 of the device MAC address will be appended to the short name to set the device's BLE Name. Short name can be up to 4 bytes long.") .font(.caption2) diff --git a/de.lproj/Localizable.strings b/de.lproj/Localizable.strings index 41c5c20b..0bda4f53 100644 --- a/de.lproj/Localizable.strings +++ b/de.lproj/Localizable.strings @@ -138,6 +138,9 @@ "lora.config"="LoRa Einstellungen"; "map"="Mesh Karte"; "map.centering"="Centering"; +"map.tiles.download"="Download Tiles"; +"map.tiles.delete"="Delete Tiles"; +"map.tiles.downloading"="Downloading Tiles"; "map.recentering"="Automatic Re-centering"; "map.type"="kartentyp"; "map.usertrackingmode"="User tracking mode"; diff --git a/en.lproj/Localizable.strings b/en.lproj/Localizable.strings index 0f356c64..a38802dc 100644 --- a/en.lproj/Localizable.strings +++ b/en.lproj/Localizable.strings @@ -139,6 +139,9 @@ "map"="Mesh Map"; "map.type"="Default Type"; "map.centering"="Centering Mode"; +"map.tiles.download"="Download Tiles"; +"map.tiles.delete"="Delete Tiles"; +"map.tiles.downloading"="Downloading Tiles"; "map.recentering"="Automatic Re-centering"; "map.usertrackingmode"="User tracking mode"; "map.usertrackingmode.follow"="Follow"; diff --git a/zh-Hans.lproj/Localizable.strings b/zh-Hans.lproj/Localizable.strings index 00355830..3ed5fd3b 100644 --- a/zh-Hans.lproj/Localizable.strings +++ b/zh-Hans.lproj/Localizable.strings @@ -138,6 +138,9 @@ "lora.config"="LoRa 配置"; "map"="Mesh 地图"; "map.centering"="Centering"; +"map.tiles.download"="Download Tiles"; +"map.tiles.delete"="Delete Tiles"; +"map.tiles.downloading"="Downloading Tiles"; "map.recentering"="Automatic Re-centering"; "map.type"="地图类型"; "map.usertrackingmode"="User tracking mode";