From 60a1687839e7e0073123ba400bb7fc6262fe2c36 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Fri, 5 May 2023 17:13:35 -0700 Subject: [PATCH 01/13] Initial offline maps setup --- Meshtastic.xcodeproj/project.pbxproj | 28 +++ Meshtastic/Extensions/FileManager.swift | 65 +++++++ Meshtastic/Extensions/Url.swift | 21 +++ .../Helpers/Map/OfflineTileManager.swift | 161 ++++++++++++++++++ Meshtastic/Helpers/Map/TileOverlay.swift | 15 ++ .../Helpers/Map/TilesDownloadView.swift | 87 ++++++++++ .../Views/Map/Custom/MapViewSwiftUI.swift | 38 +++-- Meshtastic/Views/Nodes/NodeDetail.swift | 6 +- Meshtastic/Views/Nodes/NodeMap.swift | 28 +-- Meshtastic/Views/Settings/UserConfig.swift | 2 +- de.lproj/Localizable.strings | 3 + en.lproj/Localizable.strings | 3 + zh-Hans.lproj/Localizable.strings | 3 + 13 files changed, 433 insertions(+), 27 deletions(-) create mode 100644 Meshtastic/Extensions/FileManager.swift create mode 100644 Meshtastic/Extensions/Url.swift create mode 100644 Meshtastic/Helpers/Map/OfflineTileManager.swift create mode 100644 Meshtastic/Helpers/Map/TileOverlay.swift create mode 100644 Meshtastic/Helpers/Map/TilesDownloadView.swift 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"; From e1bf4b021283711cdd1fa1de836690e6439cb716 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sat, 6 May 2023 16:15:12 -0700 Subject: [PATCH 02/13] Somewhat working offline maps --- Meshtastic.xcodeproj/project.pbxproj | 8 +- .../alpha.imageset/Contents.json | 21 --- Meshtastic/Enums/AppSettingsEnums.swift | 33 ++-- Meshtastic/Extensions/UserDefaults.swift | 21 ++- .../Helpers/Map/OfflineTileManager.swift | 10 +- .../Helpers/Map/TilesDownloadView.swift | 87 ---------- .../alpha.imageset => Resources}/alpha.png | Bin .../Views/Map/Custom/MapViewSwiftUI.swift | 152 +++++++++++------- Meshtastic/Views/Nodes/NodeDetail.swift | 34 ++-- Meshtastic/Views/Nodes/NodeList.swift | 4 +- Meshtastic/Views/Nodes/NodeMap.swift | 40 ++--- de.lproj/Localizable.strings | 1 + en.lproj/Localizable.strings | 1 + zh-Hans.lproj/Localizable.strings | 1 + 14 files changed, 183 insertions(+), 230 deletions(-) delete mode 100644 Meshtastic/Assets.xcassets/alpha.imageset/Contents.json delete mode 100644 Meshtastic/Helpers/Map/TilesDownloadView.swift rename Meshtastic/{Assets.xcassets/alpha.imageset => Resources}/alpha.png (100%) diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index 6de0398b..0d8f2191 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -102,7 +102,7 @@ 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 */; }; + DDB75A1A2A05EB67006ED576 /* alpha.png in Resources */ = {isa = PBXBuildFile; fileRef = DDB75A192A05EB67006ED576 /* alpha.png */; }; 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 */; }; @@ -290,7 +290,7 @@ 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 = ""; }; + DDB75A192A05EB67006ED576 /* alpha.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = alpha.png; 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 = ""; }; @@ -559,7 +559,6 @@ children = ( DDB75A132A0593E2006ED576 /* OfflineTileManager.swift */, DDB75A152A0594AD006ED576 /* TileOverlay.swift */, - DDB75A172A05975A006ED576 /* TilesDownloadView.swift */, ); path = Map; sourceTree = ""; @@ -661,6 +660,7 @@ DDC2E18926CE24F70042C5E4 /* Resources */ = { isa = PBXGroup; children = ( + DDB75A192A05EB67006ED576 /* alpha.png */, DDC2E15B26CE248F0042C5E4 /* Assets.xcassets */, ); path = Resources; @@ -908,6 +908,7 @@ DDC2E15F26CE248F0042C5E4 /* Preview Assets.xcassets in Resources */, DDCDC6CB29481FCC004C1DDA /* Localizable.strings in Resources */, DDDE5A1329AFEAB900490C6C /* Assets.xcassets in Resources */, + DDB75A1A2A05EB67006ED576 /* alpha.png in Resources */, DDC2E15C26CE248F0042C5E4 /* Assets.xcassets in Resources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -971,7 +972,6 @@ 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 */, diff --git a/Meshtastic/Assets.xcassets/alpha.imageset/Contents.json b/Meshtastic/Assets.xcassets/alpha.imageset/Contents.json deleted file mode 100644 index eb9d7ea9..00000000 --- a/Meshtastic/Assets.xcassets/alpha.imageset/Contents.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "images" : [ - { - "filename" : "alpha.png", - "idiom" : "universal", - "scale" : "1x" - }, - { - "idiom" : "universal", - "scale" : "2x" - }, - { - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Meshtastic/Enums/AppSettingsEnums.swift b/Meshtastic/Enums/AppSettingsEnums.swift index 9ed76137..e176cee1 100644 --- a/Meshtastic/Enums/AppSettingsEnums.swift +++ b/Meshtastic/Enums/AppSettingsEnums.swift @@ -128,6 +128,27 @@ enum LocationUpdateInterval: Int, CaseIterable, Identifiable { } } } + +enum MapLayer: String, CaseIterable, Equatable { + case standard + case hybrid + case satellite + case offline + var localized: String { self.rawValue.localized } + var zoomRange: [Int] { + switch self { + case .standard: + return [Int](0...24) + case .hybrid: + return [Int](0...24) + case .satellite: + return [Int](0...24) + case .offline: + return [Int](0...17) + } + } +} + enum MapTileServerLinks: Int, CaseIterable, Identifiable { case none = 0 @@ -159,16 +180,4 @@ enum MapTileServerLinks: Int, CaseIterable, Identifiable { return "https://basemap.nationalmap.gov/arcgis/rest/services/USGSImageryOnly/MapServer/tile/{z}/{y}/{x}" } } - var zoomRange: [Int] { - switch self { - case .none: - return [Int](0...1) - case .wikimedia: - return [Int](0...24) - case .openStreetMaps: - return [Int](0...24) - case .nationalMap: - return [Int](0...24) - } - } } diff --git a/Meshtastic/Extensions/UserDefaults.swift b/Meshtastic/Extensions/UserDefaults.swift index fdfdb575..179d76c8 100644 --- a/Meshtastic/Extensions/UserDefaults.swift +++ b/Meshtastic/Extensions/UserDefaults.swift @@ -15,12 +15,12 @@ extension UserDefaults { case preferredPeripheralId case provideLocation case provideLocationInterval - case meshMapType - case meshMapCenteringMode + //case meshMapType case meshMapRecentering - case meshMapCustomTileServer case meshMapShowNodeHistory case meshMapShowRouteLines + case enableOfflineMaps + case mapTileServer } func reset() { @@ -74,12 +74,21 @@ extension UserDefaults { } } - static var mapType: Int { +// static var mapType: Int { +// get { +// UserDefaults.standard.integer(forKey: "meshMapType") +// } +// set { +// UserDefaults.standard.set(newValue, forKey: "meshMapType") +// } +// } + + static var mapLayer: MapLayer { get { - UserDefaults.standard.integer(forKey: "meshMapType") + MapLayer(rawValue: UserDefaults.standard.string(forKey: "mapLayer") ?? MapLayer.standard.rawValue) ?? MapLayer.standard } set { - UserDefaults.standard.set(newValue, forKey: "meshMapType") + UserDefaults.standard.set(newValue.rawValue, forKey: "mapLayer") } } diff --git a/Meshtastic/Helpers/Map/OfflineTileManager.swift b/Meshtastic/Helpers/Map/OfflineTileManager.swift index c6084e02..4dcbe5ca 100644 --- a/Meshtastic/Helpers/Map/OfflineTileManager.swift +++ b/Meshtastic/Helpers/Map/OfflineTileManager.swift @@ -22,15 +22,15 @@ class OfflineTileManager: ObservableObject { } // MARK: - Private properties - private var overlay: MKTileOverlay { MKTileOverlay(urlTemplate: UserDefaults.mapTileServer) } + private var overlay: MKTileOverlay { MKTileOverlay(urlTemplate: UserDefaults.mapTileServer.count > 1 ? UserDefaults.mapTileServer : "https://maps.wikimedia.org/osm-intl/{z}/{x}/{y}.png") } 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 + var progress: Float = 0 + var status: DownloadStatus = .download // MARK: - Public methods func getAllDownloadedSize() -> String { @@ -100,7 +100,7 @@ class OfflineTileManager: ObservableObject { if fileManager.fileExists(atPath: tilesUrl.path){ return tilesUrl } else { - if !UserDefaults.enableOfflineMaps { // Get and persist newTile + 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")! @@ -140,7 +140,7 @@ class OfflineTileManager: ObservableObject { let data = try Data(contentsOf: url) try data.write(to: filename) } catch { - print("❤️ PersistLocallyError = \(error)") + print("💀 Save Tile Error = \(error)") } return url } diff --git a/Meshtastic/Helpers/Map/TilesDownloadView.swift b/Meshtastic/Helpers/Map/TilesDownloadView.swift deleted file mode 100644 index 2fc438d0..00000000 --- a/Meshtastic/Helpers/Map/TilesDownloadView.swift +++ /dev/null @@ -1,87 +0,0 @@ -// -// 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/Assets.xcassets/alpha.imageset/alpha.png b/Meshtastic/Resources/alpha.png similarity index 100% rename from Meshtastic/Assets.xcassets/alpha.imageset/alpha.png rename to Meshtastic/Resources/alpha.png diff --git a/Meshtastic/Views/Map/Custom/MapViewSwiftUI.swift b/Meshtastic/Views/Map/Custom/MapViewSwiftUI.swift index 373883c5..61ed0753 100644 --- a/Meshtastic/Views/Map/Custom/MapViewSwiftUI.swift +++ b/Meshtastic/Views/Map/Custom/MapViewSwiftUI.swift @@ -10,29 +10,30 @@ import MapKit func degreesToRadians(_ number: Double) -> Double { return number * .pi / 180 } +var currentMapLayer: MapLayer? struct MapViewSwiftUI: UIViewRepresentable { var onLongPress: (_ waypointCoordinate: CLLocationCoordinate2D) -> Void var onWaypointEdit: (_ waypointId: Int ) -> Void - @Binding var visibleMapRect: MKMapRect + let mapView = MKMapView() // Parameters + var selectedMapLayer: MapLayer let positions: [PositionEntity] let waypoints: [WaypointEntity] - let mapViewType: MKMapType + let userTrackingMode: MKUserTrackingMode let showNodeHistory: Bool let showRouteLines: Bool + + let mapViewType: MKMapType = MKMapType.standard // Offline Map Tiles @AppStorage("lastUpdatedLocalMapFile") private var lastUpdatedLocalMapFile = 0 @State private var loadedLastUpdatedLocalMapFile = 0 var customMapOverlay: CustomMapOverlay? @State private var presentCustomMapOverlayHash: CustomMapOverlay? - // Custom Tile Server - var tileRenderer: MKTileOverlayRenderer? - let tileServer: MapTileServerLinks = .openStreetMaps // MARK: Private methods @@ -89,7 +90,65 @@ struct MapViewSwiftUI: UIViewRepresentable { #endif } + private func setOverlays(mapView: MKMapView) { + // Avoid refreshing UI if selectedLayer has not changed + guard currentMapLayer != selectedMapLayer else { return } + currentMapLayer = selectedMapLayer + for overlay in mapView.overlays { + if overlay is TileOverlay { + mapView.removeOverlay(overlay) + } + } + switch selectedMapLayer { + case .offline: + let overlay = TileOverlay() + overlay.canReplaceMapContent = false + mapView.mapType = .standard + mapView.addOverlay(overlay, level: .aboveRoads) + case .satellite: + mapView.mapType = .satellite + case .hybrid: + mapView.mapType = .hybrid + default: + mapView.mapType = .standard + } + } + + private func setUserTracking(mapView: MKMapView, headingView: UIImageView?) { +// switch selectedTracking { +// case .bounding: +// guard let firstBoundingBox = trails.first?.polyline.boundingMapRect else { +// self.selectedTracking = .enabled +// return +// } +// let boundingBox = trails +// .map { $0.polyline.boundingMapRect } +// .reduce(firstBoundingBox) { (boundingBox, nextResult) -> MKMapRect in +// let minX = nextResult.minX < boundingBox.minX ? nextResult.minX : boundingBox.minX +// let maxX = nextResult.maxX > boundingBox.maxX ? nextResult.maxX : boundingBox.maxX +// let minY = nextResult.minY < boundingBox.minY ? nextResult.minY : boundingBox.minY +// let maxY = nextResult.maxY > boundingBox.maxY ? nextResult.maxY : boundingBox.maxY +// return MKMapRect(origin: MKMapPoint(x: minX, y: minY), size: MKMapSize(width: maxX-minX, height: maxY-minY)) +// } +// var region = MKCoordinateRegion(boundingBox) +// region.span.latitudeDelta += 0.01 +// region.span.longitudeDelta += 0.01 +// mapView.setRegion(region, animated: false) +// case .disabled: +// mapView.setUserTrackingMode(.none, animated: true) +// locationManager.updateHeading = false +// headingView?.isHidden = true +// case .enabled: +// mapView.setUserTrackingMode(.follow, animated: true) +// locationManager.updateHeading = true +// case .heading: +// mapView.setUserTrackingMode(.followWithHeading, animated: true) +// headingView?.isHidden = true +// } + } + func makeUIView(context: Context) -> MKMapView { + currentMapLayer = nil mapView.delegate = context.coordinator self.configureMap(mapView: mapView) return mapView @@ -97,65 +156,39 @@ struct MapViewSwiftUI: UIViewRepresentable { func updateUIView(_ mapView: MKMapView, context: Context) { - mapView.mapType = mapViewType - //visibleMapRect = mapView.visibleMapRect - // Offline maps and tile server settings - if false {// UserDefaults.enableOfflineMaps { + // MBTiles Offline maps and tile server settings +// if UserDefaults.enableOfflineMaps && UserDefaults.mapTileServer == "" { - visibleMapRect = mapView.visibleMapRect - - if UserDefaults.mapTileServer.count > 0 { - tileRenderer?.alpha = 0.0 - let overlays = mapView.overlays - if mapView.mapType == .standard { - let overlay = MKTileOverlay(urlTemplate: UserDefaults.mapTileServer) - if overlays.contains(where: {$0 is MKPolyline}) { - mapView.addOverlay(overlay, level: .aboveLabels) - if let poly_overlay = overlays.filter({$0 is MKPolyline}).first { - mapView.addOverlay(poly_overlay, level: .aboveLabels) - } - } else { - mapView.addOverlay(overlay, level: .aboveLabels) - - } - } else { - for overlay in overlays { - if let ove = overlay as? MKTileOverlay { - mapView.removeOverlay(ove) - } - } - } - } else if self.customMapOverlay != self.presentCustomMapOverlayHash || self.loadedLastUpdatedLocalMapFile != self.lastUpdatedLocalMapFile { - mapView.removeOverlays(mapView.overlays) - if self.customMapOverlay != nil { - - let fileManager = FileManager.default - let documentsDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first! - let tilePath = documentsDirectory.appendingPathComponent("offline_map.mbtiles", isDirectory: false).path - if fileManager.fileExists(atPath: tilePath) { - print("Loading local map file") - if let overlay = LocalMBTileOverlay(mbTilePath: tilePath) { - overlay.canReplaceMapContent = false// customMapOverlay.canReplaceMapContent - mapView.addOverlay(overlay) - } - } else { - print("Couldn't find a local map file to load") - } - } - DispatchQueue.main.async { - self.presentCustomMapOverlayHash = self.customMapOverlay - self.loadedLastUpdatedLocalMapFile = self.lastUpdatedLocalMapFile - } - } - } +// if self.customMapOverlay != self.presentCustomMapOverlayHash || self.loadedLastUpdatedLocalMapFile != self.lastUpdatedLocalMapFile { +// mapView.removeOverlays(mapView.overlays) +// if self.customMapOverlay != nil { +// +// let fileManager = FileManager.default +// let documentsDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first! +// let tilePath = documentsDirectory.appendingPathComponent("offline_map.mbtiles", isDirectory: false).path +// if fileManager.fileExists(atPath: tilePath) { +// print("Loading local map file") +// if let overlay = LocalMBTileOverlay(mbTilePath: tilePath) { +// overlay.canReplaceMapContent = true// customMapOverlay.canReplaceMapContent +// mapView.addOverlay(overlay) +// } +// } else { +// print("Couldn't find a local map file to load") +// } +// } +// DispatchQueue.main.async { +// self.presentCustomMapOverlayHash = self.customMapOverlay +// self.loadedLastUpdatedLocalMapFile = self.lastUpdatedLocalMapFile +// } +// } +// } let latest = positions .filter { $0.latest == true } .sorted { $0.nodePosition?.num ?? 0 > $1.nodePosition?.num ?? -1 } - // Node Route Lines - if showRouteLines { + if true {//showRouteLines { // Remove all existing PolyLine Overlays for overlay in mapView.overlays { if overlay is MKPolyline { @@ -188,6 +221,9 @@ struct MapViewSwiftUI: UIViewRepresentable { } } + /// Set selected map layer + // setOverlays(mapView: mapView) + let annotationCount = waypoints.count + (showNodeHistory ? positions.count : latest.count) if annotationCount != mapView.annotations.count { print("Annotation Count: \(annotationCount) Map Annotations: \(mapView.annotations.count)") @@ -240,7 +276,7 @@ struct MapViewSwiftUI: UIViewRepresentable { func mapViewDidChangeVisibleRegion(_ mapView: MKMapView) { - print (mapView.visibleMapRect) + //print (mapView.visibleMapRect) } func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? { diff --git a/Meshtastic/Views/Nodes/NodeDetail.swift b/Meshtastic/Views/Nodes/NodeDetail.swift index 4e2b96c3..917cac0a 100644 --- a/Meshtastic/Views/Nodes/NodeDetail.swift +++ b/Meshtastic/Views/Nodes/NodeDetail.swift @@ -16,7 +16,8 @@ struct NodeDetail: View { @AppStorage("meshMapType") private var meshMapType = 0 @AppStorage("meshMapShowNodeHistory") private var meshMapShowNodeHistory = false @AppStorage("meshMapShowRouteLines") private var meshMapShowRouteLines = false - @State private var mapType: MKMapType = .standard + //@State private var mapType: MKMapType = .standard + @State private var selectedMapLayer: MapLayer = .standard @State var mapRect: MKMapRect = MKMapRect() @State var waypointCoordinate: WaypointCoordinate? @State var editingWaypoint: Int = 0 @@ -70,10 +71,11 @@ struct NodeDetail: View { waypointCoordinate = WaypointCoordinate(id: .init(), coordinate: nil, waypointId: Int64(wpId)) } }, - visibleMapRect: $mapRect, + //visibleMapRect: $mapRect, + selectedMapLayer: selectedMapLayer, positions: lastTenThousand, waypoints: Array(waypoints), - mapViewType: mapType, + //mapViewType: mapType, userTrackingMode: MKUserTrackingMode.none, showNodeHistory: meshMapShowNodeHistory, showRouteLines: meshMapShowRouteLines, @@ -83,18 +85,18 @@ struct NodeDetail: View { Spacer() HStack(alignment: .bottom, spacing: 1) { - Picker("Map Type", selection: $mapType) { - ForEach(MeshMapTypes.allCases) { map in - Text(map.description) - .tag(map.MKMapTypeValue()) - } - } - .onChange(of: (mapType)) { newMapType in - UserDefaults.mapType = Int(newMapType.rawValue) - } - .background(.thinMaterial, in: RoundedRectangle(cornerRadius: 12, style: .continuous)) - .pickerStyle(.menu) - .padding(5) +// Picker("Map Type", selection: $mapType) { +// ForEach(MeshMapTypes.allCases) { map in +// Text(map.description) +// .tag(map.MKMapTypeValue()) +// } +// } +// .onChange(of: (mapType)) { newMapType in +// UserDefaults.mapType = Int(newMapType.rawValue) +// } +// .background(.thinMaterial, in: RoundedRectangle(cornerRadius: 12, style: .continuous)) +// .pickerStyle(.menu) +// .padding(5) VStack { Label(temperature?.formatted(.measurement(width: .narrow)) ?? "??", systemImage: symbolName) .font(.caption) @@ -229,7 +231,7 @@ struct NodeDetail: View { }) .onAppear { self.bleManager.context = context - mapType = MeshMapTypes(rawValue: meshMapType)?.MKMapTypeValue() ?? .standard + //mapType = .standard// MeshMapTypes(rawValue: meshMapType)?.MKMapTypeValue() ?? .standard } .task(id: node.num) { if !loadedWeather { diff --git a/Meshtastic/Views/Nodes/NodeList.swift b/Meshtastic/Views/Nodes/NodeList.swift index 2a30dad9..9b56f1dc 100644 --- a/Meshtastic/Views/Nodes/NodeList.swift +++ b/Meshtastic/Views/Nodes/NodeList.swift @@ -35,7 +35,9 @@ struct NodeList: View { let connected: Bool = (bleManager.connectedPeripheral != nil && bleManager.connectedPeripheral?.num ?? -1 == node.num) VStack(alignment: .leading) { HStack { - CircleText(text: node.user?.shortName ?? "???", color: Color(UIColor(hex: UInt32(node.num))), circleSize: 65, fontSize: 20, brightness: 0.0, textColor: UIColor(hex: UInt32(node.num)).isLight() ? .black : .white) + let characters = Array(node.user?.shortName ?? "??") + + CircleText(text: node.user?.shortName ?? "???", color: Color(UIColor(hex: UInt32(node.num))), circleSize: 65, fontSize: characters.count == 1 ? 40 : 20, brightness: 0.0, textColor: UIColor(hex: UInt32(node.num)).isLight() ? .black : .white) .padding(.trailing, 5) VStack(alignment: .leading) { Text(node.user?.longName ?? "unknown".localized).font(.headline) diff --git a/Meshtastic/Views/Nodes/NodeMap.swift b/Meshtastic/Views/Nodes/NodeMap.swift index 05a7aeb1..6511ee35 100644 --- a/Meshtastic/Views/Nodes/NodeMap.swift +++ b/Meshtastic/Views/Nodes/NodeMap.swift @@ -15,13 +15,12 @@ struct NodeMap: View { @Environment(\.managedObjectContext) var context @EnvironmentObject var bleManager: BLEManager - @State var meshMapType: Int = UserDefaults.mapType + @State var selectedMapLayer: MapLayer = UserDefaults.mapLayer @State var enableMapRecentering: Bool = UserDefaults.enableMapRecentering @State var enableMapRouteLines: Bool = UserDefaults.enableMapRouteLines @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,14 +31,12 @@ struct NodeMap: View { format: "expire == nil || expire >= %@", Date() as NSDate ), animation: .none) private var waypoints: FetchedResults + @State var waypointCoordinate: WaypointCoordinate? - - @State var mapType: MKMapType = .standard @State var selectedTracking: UserTrackingModes = .none @State var selectedTileServer: MapTileServerLinks = .wikimedia @State var isPresentingInfoSheet: Bool = false - @State var waypointCoordinate: WaypointCoordinate? @State private var customMapOverlay: MapViewSwiftUI.CustomMapOverlay? = MapViewSwiftUI.CustomMapOverlay( mapName: "offlinemap", tileType: "png", @@ -59,10 +56,9 @@ struct NodeMap: View { waypointCoordinate = WaypointCoordinate(id: .init(), coordinate: nil, waypointId: Int64(wpId)) } }, - visibleMapRect: $mapRect, + selectedMapLayer: selectedMapLayer, positions: Array(positions), waypoints: Array(waypoints), - mapViewType: mapType, userTrackingMode: selectedTracking.MKUserTrackingModeValue(), showNodeHistory: enableMapNodeHistoryPins, showRouteLines: enableMapRouteLines, @@ -92,15 +88,21 @@ struct NodeMap: View { VStack { Form { Section(header: Text("Map Options")) { - Picker("Map Type", selection: $mapType) { - ForEach(MeshMapTypes.allCases) { map in - Text(map.description).tag(map.MKMapTypeValue()) + Picker(selection: $selectedMapLayer, label: Text("")) { + ForEach(MapLayer.allCases, id: \.self) { layer in + if layer == MapLayer.offline && UserDefaults.enableOfflineMaps { + Text(layer.localized) + } else if layer != MapLayer.offline { + Text(layer.localized) + } } } - .pickerStyle(DefaultPickerStyle()) - .onChange(of: (mapType)) { newMapType in - UserDefaults.mapType = Int(newMapType.rawValue) + .pickerStyle(SegmentedPickerStyle()) + .onChange(of: (selectedMapLayer)) { newMapLayer in + UserDefaults.mapLayer = newMapLayer } + .padding(.top, 5) + .padding(.bottom, 5) Toggle(isOn: $enableMapRecentering) { @@ -141,16 +143,16 @@ struct NodeMap: View { self.enableOfflineMaps.toggle() UserDefaults.enableOfflineMaps = self.enableOfflineMaps } - Text("If you have shared a MBTiles file with meshtastic it will be loaded.") - .font(.caption) - .foregroundColor(.gray) +// Text("If you have shared a MBTiles file with meshtastic it will be loaded.") +// .font(.caption) +// .foregroundColor(.gray) if UserDefaults.enableOfflineMaps { VStack { // Picker("Tile Servers", selection: $selectedTileServer) { // ForEach(MapTileServerLinks.allCases) { ts in // Text(ts.description) -// .tag(ts.id) +// // .tag(ts.id) // } // } // .pickerStyle(.menu) @@ -159,7 +161,7 @@ struct NodeMap: View { // mapTileServer = selectedTileServer.tileUrl // } - TilesDownloadView(boundingBox: mapRect, name: "All tiles") +// TilesDownloadView(boundingBox: mapRect, name: "All tiles") HStack { Label("Tile Server", systemImage: "square.grid.3x2") TextField( @@ -207,8 +209,6 @@ struct NodeMap: View { .onAppear(perform: { UIApplication.shared.isIdleTimerDisabled = true self.bleManager.context = context - mapType = MeshMapTypes(rawValue: meshMapType)?.MKMapTypeValue() ?? .standard - }) .onDisappear(perform: { UIApplication.shared.isIdleTimerDisabled = false diff --git a/de.lproj/Localizable.strings b/de.lproj/Localizable.strings index 0bda4f53..243b8015 100644 --- a/de.lproj/Localizable.strings +++ b/de.lproj/Localizable.strings @@ -200,6 +200,7 @@ "not.connected"="Kein Gerät verbunden"; "numbers.punctuation"="Ziffern und Interpunktion"; "off"="Aus"; +"offline"="Offline"; "on.boot"="Nur beim Starten"; "options"="Optionen"; "password"="Passwort"; diff --git a/en.lproj/Localizable.strings b/en.lproj/Localizable.strings index a38802dc..87ab586b 100644 --- a/en.lproj/Localizable.strings +++ b/en.lproj/Localizable.strings @@ -200,6 +200,7 @@ "not.connected"="No device connected"; "numbers.punctuation"="Numbers and Punctuation"; "off"="Off"; +"offline"="Offline"; "on.boot"="On Boot Only"; "options"="Options"; "password"="Password"; diff --git a/zh-Hans.lproj/Localizable.strings b/zh-Hans.lproj/Localizable.strings index 3ed5fd3b..974693bc 100644 --- a/zh-Hans.lproj/Localizable.strings +++ b/zh-Hans.lproj/Localizable.strings @@ -200,6 +200,7 @@ "not.connected"="未连接到电台"; "numbers.punctuation"="数字和标点符号"; "off"="关闭"; +"offline"="Offline"; "on.boot"="仅在启动时"; "options"="选项"; "password"="密码"; From fa08cf89599cea67c6bc270d92eb98e3cdc05465 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sat, 6 May 2023 16:50:41 -0700 Subject: [PATCH 03/13] Clean up user defaults --- Meshtastic/Extensions/UserDefaults.swift | 25 +------------------ .../Views/Map/Custom/MapViewSwiftUI.swift | 9 ++----- Meshtastic/Views/Settings/AppSettings.swift | 8 ++---- 3 files changed, 5 insertions(+), 37 deletions(-) diff --git a/Meshtastic/Extensions/UserDefaults.swift b/Meshtastic/Extensions/UserDefaults.swift index 179d76c8..6150915e 100644 --- a/Meshtastic/Extensions/UserDefaults.swift +++ b/Meshtastic/Extensions/UserDefaults.swift @@ -10,12 +10,10 @@ import Foundation extension UserDefaults { enum Keys: String, CaseIterable { - case hasBeenLaunched case meshtasticUsername case preferredPeripheralId case provideLocation case provideLocationInterval - //case meshMapType case meshMapRecentering case meshMapShowNodeHistory case meshMapShowRouteLines @@ -27,16 +25,6 @@ extension UserDefaults { Keys.allCases.forEach { removeObject(forKey: $0.rawValue) } } - static var hasBeenLaunched: Bool { - get { - let result = UserDefaults.standard.bool(forKey: "hasBeenLaunched") - UserDefaults.standard.set(true, forKey: "hasBeenLaunched") - return result - } set { - UserDefaults.standard.set(newValue, forKey: "hasBeenLaunched") - } - } - static var meshtasticUsername: String { get { UserDefaults.standard.string(forKey: "meshtasticUsername") ?? "" @@ -57,9 +45,7 @@ extension UserDefaults { static var provideLocation: Bool { get { - let result = UserDefaults.standard.bool(forKey: "provideLocation") - UserDefaults.standard.set(true, forKey: "provideLocation") - return result + UserDefaults.standard.bool(forKey: "provideLocation") } set { UserDefaults.standard.set(newValue, forKey: "provideLocation") } @@ -74,15 +60,6 @@ extension UserDefaults { } } -// static var mapType: Int { -// get { -// UserDefaults.standard.integer(forKey: "meshMapType") -// } -// set { -// UserDefaults.standard.set(newValue, forKey: "meshMapType") -// } -// } - static var mapLayer: MapLayer { get { MapLayer(rawValue: UserDefaults.standard.string(forKey: "mapLayer") ?? MapLayer.standard.rawValue) ?? MapLayer.standard diff --git a/Meshtastic/Views/Map/Custom/MapViewSwiftUI.swift b/Meshtastic/Views/Map/Custom/MapViewSwiftUI.swift index 61ed0753..447d01b0 100644 --- a/Meshtastic/Views/Map/Custom/MapViewSwiftUI.swift +++ b/Meshtastic/Views/Map/Custom/MapViewSwiftUI.swift @@ -222,7 +222,7 @@ struct MapViewSwiftUI: UIViewRepresentable { } /// Set selected map layer - // setOverlays(mapView: mapView) + setOverlays(mapView: mapView) let annotationCount = waypoints.count + (showNodeHistory ? positions.count : latest.count) if annotationCount != mapView.annotations.count { @@ -274,11 +274,6 @@ 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 { @@ -448,7 +443,7 @@ struct MapViewSwiftUI: UIViewRepresentable { } public func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer { - + switch overlay { case let overlay as MKTileOverlay: return MKTileOverlayRenderer(tileOverlay: overlay) diff --git a/Meshtastic/Views/Settings/AppSettings.swift b/Meshtastic/Views/Settings/AppSettings.swift index 6debbe72..dc544509 100644 --- a/Meshtastic/Views/Settings/AppSettings.swift +++ b/Meshtastic/Views/Settings/AppSettings.swift @@ -60,10 +60,6 @@ struct AppSettings: View { Label("provide.location", systemImage: "location.circle.fill") } .toggleStyle(SwitchToggleStyle(tint: .accentColor)) - .onTapGesture { - self.provideLocation.toggle() - UserDefaults.provideLocation = self.provideLocation - } if UserDefaults.provideLocation { @@ -119,8 +115,8 @@ struct AppSettings: View { .onChange(of: (meshtasticUsername)) { newMeshtasticUsername in UserDefaults.meshtasticUsername = newMeshtasticUsername } - .onChange(of: provideLocation) { _ in - + .onChange(of: provideLocation) { newProvideLocation in + UserDefaults.provideLocation = newProvideLocation if bleManager.connectedPeripheral != nil { self.bleManager.sendWantConfig() } From 97a90dbb9085115172bd08a49109feff50d3f53d Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sat, 6 May 2023 16:56:41 -0700 Subject: [PATCH 04/13] Bump version --- Meshtastic.xcodeproj/project.pbxproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index 0d8f2191..3c424de9 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -1446,7 +1446,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.1.8; + MARKETING_VERSION = 2.1.9; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; @@ -1477,7 +1477,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.1.8; + MARKETING_VERSION = 2.1.9; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; From 0596e93bbfaef70cffb5666a353b2177a91738bc Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sat, 6 May 2023 16:57:26 -0700 Subject: [PATCH 05/13] Bump version --- Meshtastic.xcodeproj/project.pbxproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index 3c424de9..3367b256 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -1293,7 +1293,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.1.8; + MARKETING_VERSION = 2.1.9; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -1327,7 +1327,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.1.8; + MARKETING_VERSION = 2.1.9; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; From 8af95d724815aa1c3eb87931a702a372b9c404ba Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sat, 6 May 2023 18:40:11 -0700 Subject: [PATCH 06/13] Tidy up offline maps --- Meshtastic/Extensions/String.swift | 12 ++ Meshtastic/Extensions/UserDefaults.swift | 8 ++ .../Views/Map/Custom/MapViewSwiftUI.swift | 130 +++++++----------- Meshtastic/Views/Nodes/NodeMap.swift | 50 +++---- 4 files changed, 94 insertions(+), 106 deletions(-) diff --git a/Meshtastic/Extensions/String.swift b/Meshtastic/Extensions/String.swift index 93050df0..da401a00 100644 --- a/Meshtastic/Extensions/String.swift +++ b/Meshtastic/Extensions/String.swift @@ -30,6 +30,18 @@ extension String { var localized: String { NSLocalizedString(self, comment: self) } + + + func isEmoji() -> Bool { + // Emoji are no more than 4 bytes + if self.count > 4 { + return false + } else { + let characters = Array(self) + return characters[0].isEmoji + } + } + func onlyEmojis() -> Bool { return count > 0 && !contains { !$0.isEmoji } } diff --git a/Meshtastic/Extensions/UserDefaults.swift b/Meshtastic/Extensions/UserDefaults.swift index 6150915e..c0751635 100644 --- a/Meshtastic/Extensions/UserDefaults.swift +++ b/Meshtastic/Extensions/UserDefaults.swift @@ -104,6 +104,14 @@ extension UserDefaults { UserDefaults.standard.set(newValue, forKey: "enableOfflineMaps") } } + static var enableOfflineMapsMBTiles: Bool { + get { + UserDefaults.standard.bool(forKey: "enableOfflineMapsMBTiles") + } + set { + UserDefaults.standard.set(newValue, forKey: "enableOfflineMapsMBTiles") + } + } static var mapTileServer: String { get { diff --git a/Meshtastic/Views/Map/Custom/MapViewSwiftUI.swift b/Meshtastic/Views/Map/Custom/MapViewSwiftUI.swift index 447d01b0..d2927f70 100644 --- a/Meshtastic/Views/Map/Custom/MapViewSwiftUI.swift +++ b/Meshtastic/Views/Map/Custom/MapViewSwiftUI.swift @@ -90,21 +90,23 @@ struct MapViewSwiftUI: UIViewRepresentable { #endif } - private func setOverlays(mapView: MKMapView) { + private func setMapLayer(mapView: MKMapView) { // Avoid refreshing UI if selectedLayer has not changed guard currentMapLayer != selectedMapLayer else { return } currentMapLayer = selectedMapLayer for overlay in mapView.overlays { - if overlay is TileOverlay { + if overlay is MKTileOverlay { mapView.removeOverlay(overlay) } } switch selectedMapLayer { case .offline: - let overlay = TileOverlay() - overlay.canReplaceMapContent = false mapView.mapType = .standard - mapView.addOverlay(overlay, level: .aboveRoads) + if !UserDefaults.enableOfflineMapsMBTiles { + let overlay = TileOverlay() + overlay.canReplaceMapContent = false + mapView.addOverlay(overlay, level: .aboveLabels) + } case .satellite: mapView.mapType = .satellite case .hybrid: @@ -114,39 +116,6 @@ struct MapViewSwiftUI: UIViewRepresentable { } } - private func setUserTracking(mapView: MKMapView, headingView: UIImageView?) { -// switch selectedTracking { -// case .bounding: -// guard let firstBoundingBox = trails.first?.polyline.boundingMapRect else { -// self.selectedTracking = .enabled -// return -// } -// let boundingBox = trails -// .map { $0.polyline.boundingMapRect } -// .reduce(firstBoundingBox) { (boundingBox, nextResult) -> MKMapRect in -// let minX = nextResult.minX < boundingBox.minX ? nextResult.minX : boundingBox.minX -// let maxX = nextResult.maxX > boundingBox.maxX ? nextResult.maxX : boundingBox.maxX -// let minY = nextResult.minY < boundingBox.minY ? nextResult.minY : boundingBox.minY -// let maxY = nextResult.maxY > boundingBox.maxY ? nextResult.maxY : boundingBox.maxY -// return MKMapRect(origin: MKMapPoint(x: minX, y: minY), size: MKMapSize(width: maxX-minX, height: maxY-minY)) -// } -// var region = MKCoordinateRegion(boundingBox) -// region.span.latitudeDelta += 0.01 -// region.span.longitudeDelta += 0.01 -// mapView.setRegion(region, animated: false) -// case .disabled: -// mapView.setUserTrackingMode(.none, animated: true) -// locationManager.updateHeading = false -// headingView?.isHidden = true -// case .enabled: -// mapView.setUserTrackingMode(.follow, animated: true) -// locationManager.updateHeading = true -// case .heading: -// mapView.setUserTrackingMode(.followWithHeading, animated: true) -// headingView?.isHidden = true -// } - } - func makeUIView(context: Context) -> MKMapView { currentMapLayer = nil mapView.delegate = context.coordinator @@ -156,39 +125,41 @@ struct MapViewSwiftUI: UIViewRepresentable { func updateUIView(_ mapView: MKMapView, context: Context) { - - // MBTiles Offline maps and tile server settings -// if UserDefaults.enableOfflineMaps && UserDefaults.mapTileServer == "" { + // MBTiles Offline + if UserDefaults.enableOfflineMaps && UserDefaults.enableOfflineMapsMBTiles { -// if self.customMapOverlay != self.presentCustomMapOverlayHash || self.loadedLastUpdatedLocalMapFile != self.lastUpdatedLocalMapFile { -// mapView.removeOverlays(mapView.overlays) -// if self.customMapOverlay != nil { -// -// let fileManager = FileManager.default -// let documentsDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first! -// let tilePath = documentsDirectory.appendingPathComponent("offline_map.mbtiles", isDirectory: false).path -// if fileManager.fileExists(atPath: tilePath) { -// print("Loading local map file") -// if let overlay = LocalMBTileOverlay(mbTilePath: tilePath) { -// overlay.canReplaceMapContent = true// customMapOverlay.canReplaceMapContent -// mapView.addOverlay(overlay) -// } -// } else { -// print("Couldn't find a local map file to load") -// } -// } -// DispatchQueue.main.async { -// self.presentCustomMapOverlayHash = self.customMapOverlay -// self.loadedLastUpdatedLocalMapFile = self.lastUpdatedLocalMapFile -// } -// } -// } + if self.customMapOverlay != self.presentCustomMapOverlayHash || self.loadedLastUpdatedLocalMapFile != self.lastUpdatedLocalMapFile { + mapView.removeOverlays(mapView.overlays) + if self.customMapOverlay != nil { + + let fileManager = FileManager.default + let documentsDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first! + let tilePath = documentsDirectory.appendingPathComponent("offline_map.mbtiles", isDirectory: false).path + if fileManager.fileExists(atPath: tilePath) { + print("Loading local map file") + if let overlay = LocalMBTileOverlay(mbTilePath: tilePath) { + overlay.canReplaceMapContent = false// customMapOverlay.canReplaceMapContent + mapView.addOverlay(overlay) + } + } else { + print("Couldn't find a local map file to load") + } + } + DispatchQueue.main.async { + self.presentCustomMapOverlayHash = self.customMapOverlay + self.loadedLastUpdatedLocalMapFile = self.lastUpdatedLocalMapFile + } + } + } + // Set selected map layer + setMapLayer(mapView: mapView) let latest = positions .filter { $0.latest == true } .sorted { $0.nodePosition?.num ?? 0 > $1.nodePosition?.num ?? -1 } + // Node Route Lines - if true {//showRouteLines { + if showRouteLines { // Remove all existing PolyLine Overlays for overlay in mapView.overlays { if overlay is MKPolyline { @@ -221,9 +192,6 @@ struct MapViewSwiftUI: UIViewRepresentable { } } - /// Set selected map layer - setOverlays(mapView: mapView) - let annotationCount = waypoints.count + (showNodeHistory ? positions.count : latest.count) if annotationCount != mapView.annotations.count { print("Annotation Count: \(annotationCount) Map Annotations: \(mapView.annotations.count)") @@ -255,7 +223,6 @@ struct MapViewSwiftUI: UIViewRepresentable { } func makeCoordinator() -> MapCoordinator { - //return Coordinator(self) return Coordinator(self) } @@ -443,18 +410,19 @@ struct MapViewSwiftUI: UIViewRepresentable { } public func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> 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() + + 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() } } } diff --git a/Meshtastic/Views/Nodes/NodeMap.swift b/Meshtastic/Views/Nodes/NodeMap.swift index 6511ee35..5e054d14 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 enableOfflineMapsMBTiles: Bool = UserDefaults.enableOfflineMapsMBTiles @FetchRequest(sortDescriptors: [NSSortDescriptor(key: "time", ascending: true)], predicate: NSPredicate(format: "time >= %@ && nodePosition != nil", Calendar.current.startOfDay(for: Date()) as NSDate), animation: .none) @@ -149,35 +150,34 @@ struct NodeMap: View { if UserDefaults.enableOfflineMaps { VStack { -// Picker("Tile Servers", selection: $selectedTileServer) { -// ForEach(MapTileServerLinks.allCases) { ts in -// Text(ts.description) -// // .tag(ts.id) -// } -// } -// .pickerStyle(.menu) -// .onChange(of: (selectedTileServer)) { newTileServer in -// -// mapTileServer = selectedTileServer.tileUrl -// } -// 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 + if !enableOfflineMapsMBTiles { + + HStack { + Label("Tile Server", systemImage: "square.grid.3x2") + TextField( + "Tile Server", + text: $mapTileServer, + axis: .vertical + ) + .keyboardType(.asciiCapable) + .disableAutocorrection(true) + .foregroundColor(.gray) + .font(.caption) + .onChange(of: (mapTileServer)) { newMapTileServer in + UserDefaults.mapTileServer = newMapTileServer + } } } + Toggle(isOn: $enableOfflineMapsMBTiles) { + Text("Enable MB Tiles") + } + .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + .onTapGesture { + self.enableOfflineMapsMBTiles.toggle() + UserDefaults.enableOfflineMapsMBTiles = self.enableOfflineMapsMBTiles + } } - .keyboardType(.asciiCapable) - .disableAutocorrection(true) } } } From 7b39ba5e9e40f917f86f3b69d948c9c7f6acb7a6 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sat, 6 May 2023 19:26:09 -0700 Subject: [PATCH 07/13] Bigger font for emoji short names in CircleText --- Meshtastic/Views/Helpers/Node/NodeInfoView.swift | 4 ++-- Meshtastic/Views/Messages/Contacts.swift | 2 +- Meshtastic/Views/Nodes/NodeList.swift | 4 +--- Meshtastic/Views/Nodes/NodeMap.swift | 2 +- 4 files changed, 5 insertions(+), 7 deletions(-) diff --git a/Meshtastic/Views/Helpers/Node/NodeInfoView.swift b/Meshtastic/Views/Helpers/Node/NodeInfoView.swift index 90f039b5..a829d27e 100644 --- a/Meshtastic/Views/Helpers/Node/NodeInfoView.swift +++ b/Meshtastic/Views/Helpers/Node/NodeInfoView.swift @@ -28,7 +28,7 @@ struct NodeInfoView: View { if UIDevice.current.userInterfaceIdiom == .pad || UIDevice.current.userInterfaceIdiom == .mac { HStack { VStack(alignment: .center) { - CircleText(text: node.user?.shortName ?? "???", color: Color(UIColor(hex: UInt32(node.num))), circleSize: 75, fontSize: 24, textColor: UIColor(hex: UInt32(node.num)).isLight() ? .black : .white ) + CircleText(text: node.user?.shortName ?? "???", color: Color(UIColor(hex: UInt32(node.num))), circleSize: 75, fontSize: (node.user?.shortName ?? "???").isEmoji() ? 48 : 24, textColor: UIColor(hex: UInt32(node.num)).isLight() ? .black : .white ) } Divider() VStack { @@ -142,7 +142,7 @@ struct NodeInfoView: View { HStack { VStack(alignment: .center) { - CircleText(text: node.user?.shortName ?? "???", color: Color(UIColor(hex: UInt32(node.num))), circleSize: 65, fontSize: 20, textColor: UIColor(hex: UInt32(node.num)).isLight() ? .black : .white ) + CircleText(text: node.user?.shortName ?? "???", color: Color(UIColor(hex: UInt32(node.num))), circleSize: 65, fontSize: (node.user?.shortName ?? "???").isEmoji() ? 40 : 24, textColor: UIColor(hex: UInt32(node.num)).isLight() ? .black : .white ) } Divider() VStack { diff --git a/Meshtastic/Views/Messages/Contacts.swift b/Meshtastic/Views/Messages/Contacts.swift index 03f02805..caf7cced 100644 --- a/Meshtastic/Views/Messages/Contacts.swift +++ b/Meshtastic/Views/Messages/Contacts.swift @@ -148,7 +148,7 @@ struct Contacts: View { HStack { VStack { HStack { - CircleText(text: user.shortName ?? "???", color: Color(UIColor(hex: UInt32(user.num))), circleSize: 60, fontSize: 18, textColor: UIColor(hex: UInt32(user.num)).isLight() ? .black : .white) + CircleText(text: user.shortName ?? "???", color: Color(UIColor(hex: UInt32(user.num))), circleSize: 60, fontSize: (user.shortName ?? "???").isEmoji() ? 36 : 18, textColor: UIColor(hex: UInt32(user.num)).isLight() ? .black : .white) .padding(.trailing, 5) VStack { HStack { diff --git a/Meshtastic/Views/Nodes/NodeList.swift b/Meshtastic/Views/Nodes/NodeList.swift index 9b56f1dc..511136d2 100644 --- a/Meshtastic/Views/Nodes/NodeList.swift +++ b/Meshtastic/Views/Nodes/NodeList.swift @@ -35,9 +35,7 @@ struct NodeList: View { let connected: Bool = (bleManager.connectedPeripheral != nil && bleManager.connectedPeripheral?.num ?? -1 == node.num) VStack(alignment: .leading) { HStack { - let characters = Array(node.user?.shortName ?? "??") - - CircleText(text: node.user?.shortName ?? "???", color: Color(UIColor(hex: UInt32(node.num))), circleSize: 65, fontSize: characters.count == 1 ? 40 : 20, brightness: 0.0, textColor: UIColor(hex: UInt32(node.num)).isLight() ? .black : .white) + CircleText(text: node.user?.shortName ?? "???", color: Color(UIColor(hex: UInt32(node.num))), circleSize: 65, fontSize: (node.user?.shortName ?? "???").isEmoji() ? 40 : 20, brightness: 0.0, textColor: UIColor(hex: UInt32(node.num)).isLight() ? .black : .white) .padding(.trailing, 5) VStack(alignment: .leading) { Text(node.user?.longName ?? "unknown".localized).font(.headline) diff --git a/Meshtastic/Views/Nodes/NodeMap.swift b/Meshtastic/Views/Nodes/NodeMap.swift index 5e054d14..38daa2ac 100644 --- a/Meshtastic/Views/Nodes/NodeMap.swift +++ b/Meshtastic/Views/Nodes/NodeMap.swift @@ -193,7 +193,7 @@ struct NodeMap: View { .padding(.bottom) #endif } - .presentationDetents([.medium, .large]) + .presentationDetents([UserDefaults.enableOfflineMaps ? .large : .medium]) .presentationDragIndicator(.visible) } } From d16708b337f01591d0f0722d00f0552af92d5c2e Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 7 May 2023 08:01:16 -0700 Subject: [PATCH 08/13] Add tiles above labels setting, add delete cached tiles button --- Meshtastic.xcodeproj/project.pbxproj | 4 ++ Meshtastic/Extensions/UserDefaults.swift | 11 +++++ .../Views/Map/Custom/MapViewSwiftUI.swift | 2 +- Meshtastic/Views/Map/Custom/TilesView.swift | 48 +++++++++++++++++++ Meshtastic/Views/Nodes/NodeMap.swift | 27 +++++++++-- de.lproj/Localizable.strings | 4 +- en.lproj/Localizable.strings | 4 +- zh-Hans.lproj/Localizable.strings | 4 +- 8 files changed, 90 insertions(+), 14 deletions(-) create mode 100644 Meshtastic/Views/Map/Custom/TilesView.swift diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index 3367b256..49095803 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -103,6 +103,7 @@ DDB75A142A0593E2006ED576 /* OfflineTileManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDB75A132A0593E2006ED576 /* OfflineTileManager.swift */; }; DDB75A162A0594AD006ED576 /* TileOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDB75A152A0594AD006ED576 /* TileOverlay.swift */; }; DDB75A1A2A05EB67006ED576 /* alpha.png in Resources */ = {isa = PBXBuildFile; fileRef = DDB75A192A05EB67006ED576 /* alpha.png */; }; + DDB75A1C2A076DFA006ED576 /* TilesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDB75A1B2A076DFA006ED576 /* TilesView.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 */; }; @@ -291,6 +292,7 @@ DDB75A132A0593E2006ED576 /* OfflineTileManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OfflineTileManager.swift; sourceTree = ""; }; DDB75A152A0594AD006ED576 /* TileOverlay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TileOverlay.swift; sourceTree = ""; }; DDB75A192A05EB67006ED576 /* alpha.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = alpha.png; sourceTree = ""; }; + DDB75A1B2A076DFA006ED576 /* TilesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TilesView.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 = ""; }; @@ -399,6 +401,7 @@ DD964FC32974767D007C176F /* MapViewFitExtension.swift */, DD2AD8A7296D2DF9001FF0E7 /* MapViewSwiftUI.swift */, DDDB443529F6287000EE2349 /* MapButtons.swift */, + DDB75A1B2A076DFA006ED576 /* TilesView.swift */, ); path = Custom; sourceTree = ""; @@ -1004,6 +1007,7 @@ DD2160AF28C5552500C17253 /* MQTTConfig.swift in Sources */, DDDB444229F8A88700EE2349 /* Double.swift in Sources */, DD5E520F298EE33B00D21B61 /* cannedmessages.pb.swift in Sources */, + DDB75A1C2A076DFA006ED576 /* TilesView.swift in Sources */, DDB75A162A0594AD006ED576 /* TileOverlay.swift in Sources */, DDF924CA26FBB953009FE055 /* ConnectedDevice.swift in Sources */, DD3CC6BE28E4CD9800FA9159 /* BatteryGauge.swift in Sources */, diff --git a/Meshtastic/Extensions/UserDefaults.swift b/Meshtastic/Extensions/UserDefaults.swift index c0751635..49a2bf4c 100644 --- a/Meshtastic/Extensions/UserDefaults.swift +++ b/Meshtastic/Extensions/UserDefaults.swift @@ -14,11 +14,13 @@ extension UserDefaults { case preferredPeripheralId case provideLocation case provideLocationInterval + case mapLayer case meshMapRecentering case meshMapShowNodeHistory case meshMapShowRouteLines case enableOfflineMaps case mapTileServer + case mapTilesAboveLabels } func reset() { @@ -121,4 +123,13 @@ extension UserDefaults { UserDefaults.standard.set(newValue, forKey: "mapTileServer") } } + + static var mapTilesAboveLabels: Bool { + get { + UserDefaults.standard.bool(forKey: "mapTilesAboveLabels") + } + set { + UserDefaults.standard.set(newValue, forKey: "mapTilesAboveLabels") + } + } } diff --git a/Meshtastic/Views/Map/Custom/MapViewSwiftUI.swift b/Meshtastic/Views/Map/Custom/MapViewSwiftUI.swift index d2927f70..ce96e651 100644 --- a/Meshtastic/Views/Map/Custom/MapViewSwiftUI.swift +++ b/Meshtastic/Views/Map/Custom/MapViewSwiftUI.swift @@ -105,7 +105,7 @@ struct MapViewSwiftUI: UIViewRepresentable { if !UserDefaults.enableOfflineMapsMBTiles { let overlay = TileOverlay() overlay.canReplaceMapContent = false - mapView.addOverlay(overlay, level: .aboveLabels) + mapView.addOverlay(overlay, level: UserDefaults.mapTilesAboveLabels ? .aboveLabels : .aboveRoads) } case .satellite: mapView.mapType = .satellite diff --git a/Meshtastic/Views/Map/Custom/TilesView.swift b/Meshtastic/Views/Map/Custom/TilesView.swift new file mode 100644 index 00000000..77127d40 --- /dev/null +++ b/Meshtastic/Views/Map/Custom/TilesView.swift @@ -0,0 +1,48 @@ +// +// TilesView.swift +// Meshtastic +// +// Copyright(c) Garth Vander Houwen on 5/6/23. +// + +import SwiftUI +import MapKit + +struct TilesView: View { + + @ObservedObject var tileManager = OfflineTileManager.shared + @State var totalDownloadedTileSize = "" + + var body: some View { + + Button(action: { + tileManager.removeAll() + totalDownloadedTileSize = tileManager.getAllDownloadedSize() + print("delete all tiles") + }) { + + HStack { + Image(systemName: "trash") + .foregroundColor(.red) + Text("\("map.tiles.delete".localized) (\(totalDownloadedTileSize))") + .foregroundColor(.red) + Spacer() + } + } + .onAppear(perform: { + totalDownloadedTileSize = tileManager.getAllDownloadedSize() + }) + Divider() + } +} + +// MARK: Previews +struct TilesView_Previews: PreviewProvider { + + static var previews: some View { + + TilesView() + .previewLayout(.fixed(width: 300, height: 80)) + .environment(\.colorScheme, .light) + } +} diff --git a/Meshtastic/Views/Nodes/NodeMap.swift b/Meshtastic/Views/Nodes/NodeMap.swift index 38daa2ac..068201ff 100644 --- a/Meshtastic/Views/Nodes/NodeMap.swift +++ b/Meshtastic/Views/Nodes/NodeMap.swift @@ -22,6 +22,7 @@ struct NodeMap: View { @State var enableOfflineMaps: Bool = UserDefaults.enableOfflineMaps @State var mapTileServer: String = UserDefaults.mapTileServer @State var enableOfflineMapsMBTiles: Bool = UserDefaults.enableOfflineMapsMBTiles + @State var mapTilesAboveLabels: Bool = UserDefaults.mapTilesAboveLabels @FetchRequest(sortDescriptors: [NSSortDescriptor(key: "time", ascending: true)], predicate: NSPredicate(format: "time >= %@ && nodePosition != nil", Calendar.current.startOfDay(for: Date()) as NSDate), animation: .none) @@ -143,11 +144,12 @@ struct NodeMap: View { .onTapGesture { self.enableOfflineMaps.toggle() UserDefaults.enableOfflineMaps = self.enableOfflineMaps + if !self.enableOfflineMaps { + if self.selectedMapLayer == .offline { + self.selectedMapLayer = .standard + } + } } -// Text("If you have shared a MBTiles file with meshtastic it will be loaded.") -// .font(.caption) -// .foregroundColor(.gray) - if UserDefaults.enableOfflineMaps { VStack { @@ -168,6 +170,20 @@ struct NodeMap: View { UserDefaults.mapTileServer = newMapTileServer } } + Text("A tile server will be used (defaults to wikimedia OSM) to load and cache map tiles as you browse the Offline map type.") + .font(.caption) + .foregroundColor(.gray) + Divider() + Toggle(isOn: $mapTilesAboveLabels) { + Text("Tiles above Labels") + } + .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + .onTapGesture { + self.mapTilesAboveLabels.toggle() + UserDefaults.mapTilesAboveLabels = self.mapTilesAboveLabels + } + Divider() + TilesView() } Toggle(isOn: $enableOfflineMapsMBTiles) { Text("Enable MB Tiles") @@ -177,6 +193,9 @@ struct NodeMap: View { self.enableOfflineMapsMBTiles.toggle() UserDefaults.enableOfflineMapsMBTiles = self.enableOfflineMapsMBTiles } + Text("The latest MBTiles file shared with meshtastic will be loaded into the map.") + .font(.caption) + .foregroundColor(.gray) } } } diff --git a/de.lproj/Localizable.strings b/de.lproj/Localizable.strings index 243b8015..cc289015 100644 --- a/de.lproj/Localizable.strings +++ b/de.lproj/Localizable.strings @@ -138,9 +138,7 @@ "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.tiles.delete"="Delete Cached 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 87ab586b..04841901 100644 --- a/en.lproj/Localizable.strings +++ b/en.lproj/Localizable.strings @@ -139,9 +139,7 @@ "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.tiles.delete"="Delete Cached 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 974693bc..cf4e6b3b 100644 --- a/zh-Hans.lproj/Localizable.strings +++ b/zh-Hans.lproj/Localizable.strings @@ -138,9 +138,7 @@ "lora.config"="LoRa 配置"; "map"="Mesh 地图"; "map.centering"="Centering"; -"map.tiles.download"="Download Tiles"; -"map.tiles.delete"="Delete Tiles"; -"map.tiles.downloading"="Downloading Tiles"; +"map.tiles.delete"="Delete Cached Tiles"; "map.recentering"="Automatic Re-centering"; "map.type"="地图类型"; "map.usertrackingmode"="User tracking mode"; From 7cc3e2c5e4ecd40851afd20fdc661e5ccd8ecf44 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Mon, 8 May 2023 07:35:31 -0700 Subject: [PATCH 09/13] Clean up offline maps settings --- Meshtastic/Enums/AppSettingsEnums.swift | 81 ++++++++++++------- Meshtastic/Extensions/UserDefaults.swift | 7 +- .../Helpers/Map/OfflineTileManager.swift | 2 +- Meshtastic/Views/Map/Custom/TilesView.swift | 3 +- Meshtastic/Views/Nodes/NodeMap.swift | 35 ++++---- Meshtastic/Views/Settings/AppSettings.swift | 2 + de.lproj/Localizable.strings | 2 +- en.lproj/Localizable.strings | 2 +- zh-Hans.lproj/Localizable.strings | 2 +- 9 files changed, 81 insertions(+), 55 deletions(-) diff --git a/Meshtastic/Enums/AppSettingsEnums.swift b/Meshtastic/Enums/AppSettingsEnums.swift index e176cee1..e882fd08 100644 --- a/Meshtastic/Enums/AppSettingsEnums.swift +++ b/Meshtastic/Enums/AppSettingsEnums.swift @@ -135,49 +135,72 @@ enum MapLayer: String, CaseIterable, Equatable { case satellite case offline var localized: String { self.rawValue.localized } - var zoomRange: [Int] { - switch self { - case .standard: - return [Int](0...24) - case .hybrid: - return [Int](0...24) - case .satellite: - return [Int](0...24) - case .offline: - return [Int](0...17) - } - } } -enum MapTileServerLinks: Int, CaseIterable, Identifiable { +enum MapTileServerLinks: String, CaseIterable, Identifiable { - case none = 0 - case openStreetMaps = 1 - case wikimedia = 2 - case nationalMap = 3 - var id: Int { self.rawValue } + case openStreetMaps + case usgsTopo + case usgsImageryTopo + case usgsImageryOnly + case watercolor + var id: String { self.rawValue } + var attribution: String { + switch self { + + case .openStreetMaps: + return "OpenStreetMap is a map of the world, created by people like you and free to use under an open license. © [OpenStreetMap](http://osm.org/copyright) contributors" + case .usgsTopo: + return "[USGS Topo](https://basemap.nationalmap.gov/arcgis/rest/services/USGSTopo/MapServer) is a tile cache base map service that combines the most current data in The National Map (TNM), and other public-domain data, into a multi-scale topographic reference map." + case .usgsImageryTopo: + return "[USGS Imagery Topo](https://basemap.nationalmap.gov/arcgis/rest/services/USGSImageryTopo/MapServer) is a tile cache base map of orthoimagery in The National Map and US Topo vector data." + case .usgsImageryOnly: + return "[USGS Imagery Only](https://basemap.nationalmap.gov/arcgis/rest/services/USGSImageryOnly/MapServer) is a tile cache base map service of orthoimagery in The National Map." + case .watercolor: + return "Cooper Hewitt, Smithsonian Design Museum's [Watercolor Maptiles](https://watercolormaps.collection.cooperhewitt.org/) is a open-source mapping tool created by Stamen Design and built on OpenStreetMap data." + } + } var description: String { switch self { - case .none: - return "Please Select" - case .wikimedia: - return "Wikimedia" case .openStreetMaps: return "Open Street Maps" - case .nationalMap: - return "US National Map" + case .usgsTopo: + return "USGS Topographic" + case .usgsImageryTopo: + return "USGS Topo Imagery" + case .usgsImageryOnly: + return "USGS Imagery Only" + case .watercolor: + return "Watercolor Maptiles" } } var tileUrl: String { switch self { - case .none: - return "" - case .wikimedia: - return "https://maps.wikimedia.org/osm-intl/{z}/{x}/{y}.png" case .openStreetMaps: return "https://tile.openstreetmap.org/{z}/{x}/{y}.png" - case .nationalMap: + case .usgsTopo: + return "https://basemap.nationalmap.gov/arcgis/rest/services/USGSTopo/MapServer/tile/{z}/{y}/{x}" + case .usgsImageryTopo: + return "https://basemap.nationalmap.gov/arcgis/rest/services/USGSImageryTopo/MapServer/tile/{z}/{y}/{x}" + case .usgsImageryOnly: return "https://basemap.nationalmap.gov/arcgis/rest/services/USGSImageryOnly/MapServer/tile/{z}/{y}/{x}" + case .watercolor: + return "https://watercolormaps.collection.cooperhewitt.org/tile/watercolor/{z}/{x}/{y}.jpg" + + } + } + var zoomRange: [Int] { + switch self { + case .openStreetMaps: + return [Int](0...17) + case .usgsTopo: + return [Int](0...17) + case .usgsImageryTopo: + return [Int](0...17) + case .usgsImageryOnly: + return [Int](0...17) + case .watercolor: + return [Int](0...17) } } } diff --git a/Meshtastic/Extensions/UserDefaults.swift b/Meshtastic/Extensions/UserDefaults.swift index 49a2bf4c..d8ee5e28 100644 --- a/Meshtastic/Extensions/UserDefaults.swift +++ b/Meshtastic/Extensions/UserDefaults.swift @@ -115,12 +115,13 @@ extension UserDefaults { } } - static var mapTileServer: String { + static var mapTileServer: MapTileServerLinks { get { - UserDefaults.standard.string(forKey: "mapTileServer") ?? "" + + MapTileServerLinks(rawValue: UserDefaults.standard.string(forKey: "mapTileServer") ?? MapTileServerLinks.openStreetMaps.rawValue) ?? MapTileServerLinks.openStreetMaps } set { - UserDefaults.standard.set(newValue, forKey: "mapTileServer") + UserDefaults.standard.set(newValue.rawValue, forKey: "mapTileServer") } } diff --git a/Meshtastic/Helpers/Map/OfflineTileManager.swift b/Meshtastic/Helpers/Map/OfflineTileManager.swift index 4dcbe5ca..7a5e7e4b 100644 --- a/Meshtastic/Helpers/Map/OfflineTileManager.swift +++ b/Meshtastic/Helpers/Map/OfflineTileManager.swift @@ -22,7 +22,7 @@ class OfflineTileManager: ObservableObject { } // MARK: - Private properties - private var overlay: MKTileOverlay { MKTileOverlay(urlTemplate: UserDefaults.mapTileServer.count > 1 ? UserDefaults.mapTileServer : "https://maps.wikimedia.org/osm-intl/{z}/{x}/{y}.png") } + private var overlay: MKTileOverlay { MKTileOverlay(urlTemplate: UserDefaults.mapTileServer.tileUrl.count > 1 ? UserDefaults.mapTileServer.tileUrl : MapTileServerLinks.openStreetMaps.tileUrl) } private var documentsDirectory: URL { fileManager.urls(for: .documentDirectory, in: .userDomainMask).first! } diff --git a/Meshtastic/Views/Map/Custom/TilesView.swift b/Meshtastic/Views/Map/Custom/TilesView.swift index 77127d40..b5927945 100644 --- a/Meshtastic/Views/Map/Custom/TilesView.swift +++ b/Meshtastic/Views/Map/Custom/TilesView.swift @@ -23,8 +23,10 @@ struct TilesView: View { HStack { Image(systemName: "trash") + .font(.callout) .foregroundColor(.red) Text("\("map.tiles.delete".localized) (\(totalDownloadedTileSize))") + .font(.callout) .foregroundColor(.red) Spacer() } @@ -32,7 +34,6 @@ struct TilesView: View { .onAppear(perform: { totalDownloadedTileSize = tileManager.getAllDownloadedSize() }) - Divider() } } diff --git a/Meshtastic/Views/Nodes/NodeMap.swift b/Meshtastic/Views/Nodes/NodeMap.swift index 068201ff..160063c0 100644 --- a/Meshtastic/Views/Nodes/NodeMap.swift +++ b/Meshtastic/Views/Nodes/NodeMap.swift @@ -14,13 +14,15 @@ struct NodeMap: View { @Environment(\.managedObjectContext) var context @EnvironmentObject var bleManager: BLEManager + + @ObservedObject var tileManager = OfflineTileManager.shared @State var selectedMapLayer: MapLayer = UserDefaults.mapLayer @State var enableMapRecentering: Bool = UserDefaults.enableMapRecentering @State var enableMapRouteLines: Bool = UserDefaults.enableMapRouteLines @State var enableMapNodeHistoryPins: Bool = UserDefaults.enableMapNodeHistoryPins @State var enableOfflineMaps: Bool = UserDefaults.enableOfflineMaps - @State var mapTileServer: String = UserDefaults.mapTileServer + @State var selectedTileServer: MapTileServerLinks = UserDefaults.mapTileServer @State var enableOfflineMapsMBTiles: Bool = UserDefaults.enableOfflineMapsMBTiles @State var mapTilesAboveLabels: Bool = UserDefaults.mapTilesAboveLabels @@ -36,7 +38,7 @@ struct NodeMap: View { @State var waypointCoordinate: WaypointCoordinate? @State var selectedTracking: UserTrackingModes = .none - @State var selectedTileServer: MapTileServerLinks = .wikimedia + @State var isPresentingInfoSheet: Bool = false @State private var customMapOverlay: MapViewSwiftUI.CustomMapOverlay? = MapViewSwiftUI.CustomMapOverlay( @@ -155,22 +157,19 @@ struct NodeMap: View { if !enableOfflineMapsMBTiles { - HStack { - Label("Tile Server", systemImage: "square.grid.3x2") - TextField( - "Tile Server", - text: $mapTileServer, - axis: .vertical - ) - .keyboardType(.asciiCapable) - .disableAutocorrection(true) - .foregroundColor(.gray) - .font(.caption) - .onChange(of: (mapTileServer)) { newMapTileServer in - UserDefaults.mapTileServer = newMapTileServer + Picker(selection: $selectedTileServer, + label: Text("Tile Server")) { + ForEach(MapTileServerLinks.allCases, id: \.self) { tsl in + Text(tsl.description) } } - Text("A tile server will be used (defaults to wikimedia OSM) to load and cache map tiles as you browse the Offline map type.") + .pickerStyle(DefaultPickerStyle()) + .onChange(of: (selectedTileServer)) { newSelectedTileServer in + UserDefaults.mapTileServer = newSelectedTileServer + tileManager.removeAll() + selectedMapLayer = .standard + } + Text(LocalizedStringKey(selectedTileServer.attribution)) .font(.caption) .foregroundColor(.gray) Divider() @@ -182,9 +181,9 @@ struct NodeMap: View { self.mapTilesAboveLabels.toggle() UserDefaults.mapTilesAboveLabels = self.mapTilesAboveLabels } - Divider() - TilesView() + } + Divider() Toggle(isOn: $enableOfflineMapsMBTiles) { Text("Enable MB Tiles") } diff --git a/Meshtastic/Views/Settings/AppSettings.swift b/Meshtastic/Views/Settings/AppSettings.swift index dc544509..20c89de8 100644 --- a/Meshtastic/Views/Settings/AppSettings.swift +++ b/Meshtastic/Views/Settings/AppSettings.swift @@ -77,7 +77,9 @@ struct AppSettings: View { .font(.caption) .foregroundColor(.gray) } + } + TilesView() } HStack { Button { diff --git a/de.lproj/Localizable.strings b/de.lproj/Localizable.strings index cc289015..720cccf5 100644 --- a/de.lproj/Localizable.strings +++ b/de.lproj/Localizable.strings @@ -138,7 +138,7 @@ "lora.config"="LoRa Einstellungen"; "map"="Mesh Karte"; "map.centering"="Centering"; -"map.tiles.delete"="Delete Cached Tiles"; +"map.tiles.delete"="Delete Cached Map 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 04841901..1ecdb327 100644 --- a/en.lproj/Localizable.strings +++ b/en.lproj/Localizable.strings @@ -139,7 +139,7 @@ "map"="Mesh Map"; "map.type"="Default Type"; "map.centering"="Centering Mode"; -"map.tiles.delete"="Delete Cached Tiles"; +"map.tiles.delete"="Delete Cached Map 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 cf4e6b3b..949d7d8a 100644 --- a/zh-Hans.lproj/Localizable.strings +++ b/zh-Hans.lproj/Localizable.strings @@ -138,7 +138,7 @@ "lora.config"="LoRa 配置"; "map"="Mesh 地图"; "map.centering"="Centering"; -"map.tiles.delete"="Delete Cached Tiles"; +"map.tiles.delete"="Delete Cached Map Tiles"; "map.recentering"="Automatic Re-centering"; "map.type"="地图类型"; "map.usertrackingmode"="User tracking mode"; From f3d38d729f05e11aa067bb0737c3aeb828a70947 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Mon, 8 May 2023 19:12:51 -0700 Subject: [PATCH 10/13] Additional 45 second intervals Add Noisy Environment badge on node details if (node.rssi > -115) && (node.snr <= -13) --- Meshtastic/Enums/IntervalEnums.swift | 6 +++++ .../Views/Helpers/Node/NodeInfoView.swift | 27 +++++++++++++++++-- 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/Meshtastic/Enums/IntervalEnums.swift b/Meshtastic/Enums/IntervalEnums.swift index aee8d0f0..68bdf882 100644 --- a/Meshtastic/Enums/IntervalEnums.swift +++ b/Meshtastic/Enums/IntervalEnums.swift @@ -54,6 +54,7 @@ enum SenderIntervals: Int, CaseIterable, Identifiable { case off = 0 case fifteenSeconds = 15 case thirtySeconds = 30 + case fortyFiveSeconds = 45 case oneMinute = 60 case fiveMinutes = 300 case tenMinutes = 600 @@ -70,6 +71,8 @@ enum SenderIntervals: Int, CaseIterable, Identifiable { return "interval.fifteen.seconds".localized case .thirtySeconds: return "interval.thirty.seconds".localized + case .fortyFiveSeconds: + return "interval.fortyfive.seconds".localized case .oneMinute: return "interval.one.minute".localized case .fiveMinutes: @@ -91,6 +94,7 @@ enum UpdateIntervals: Int, CaseIterable, Identifiable { case tenSeconds = 10 case fifteenSeconds = 15 case thirtySeconds = 30 + case fortyFiveSeconds = 45 case oneMinute = 60 case twoMinutes = 120 case fiveMinutes = 300 @@ -120,6 +124,8 @@ enum UpdateIntervals: Int, CaseIterable, Identifiable { return "interval.fifteen.seconds".localized case .thirtySeconds: return "interval.thirty.seconds".localized + case .fortyFiveSeconds: + return "interval.fortyfive.seconds".localized case .oneMinute: return "interval.one.minute".localized case .twoMinutes: diff --git a/Meshtastic/Views/Helpers/Node/NodeInfoView.swift b/Meshtastic/Views/Helpers/Node/NodeInfoView.swift index a829d27e..818e0d51 100644 --- a/Meshtastic/Views/Helpers/Node/NodeInfoView.swift +++ b/Meshtastic/Views/Helpers/Node/NodeInfoView.swift @@ -41,7 +41,7 @@ struct NodeInfoView: View { Text(String(hwModelString)) .foregroundColor(.gray) - .font(.largeTitle).fixedSize() + .font(.title).fixedSize() } } @@ -59,7 +59,19 @@ struct NodeInfoView: View { .font(.largeTitle) .foregroundColor(.gray) .fixedSize() + + if (node.rssi > -115) && (node.snr <= -13) { + Image(systemName: "waveform.slash") + .font(.title) + .foregroundColor(.orange) + .symbolRenderingMode(.hierarchical) + Text("Noisy Environment") + .font(.title3) + .foregroundColor(.orange) + .multilineTextAlignment(.center) + } } + } let deviceMetrics = node.telemetries?.filtered(using: NSPredicate(format: "metricsType == 0")) if deviceMetrics?.count ?? 0 >= 1 { @@ -152,7 +164,7 @@ struct NodeInfoView: View { .frame(width: 75, height: 75) .cornerRadius(5) Text(String(node.user!.hwModel ?? "unset".localized)) - .font(.callout).fixedSize() + .font(.caption).fixedSize() } } @@ -169,6 +181,17 @@ struct NodeInfoView: View { .font(.title2) .foregroundColor(.gray) .fixedSize() + + if (node.rssi > -115) && (node.snr <= -13) { + Image(systemName: "waveform.slash") + .font(.callout) + .foregroundColor(.orange) + .symbolRenderingMode(.hierarchical) + Text("Noisy Environment") + .font(.caption2) + .multilineTextAlignment(.center) + .foregroundColor(.orange) + } } } From e0c5c0aa98e4f9b45389fa55a9a159304afaeaef Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Tue, 9 May 2023 07:48:02 -0700 Subject: [PATCH 11/13] Centering updates --- .../Views/Map/Custom/MapViewSwiftUI.swift | 20 ++++--------------- 1 file changed, 4 insertions(+), 16 deletions(-) diff --git a/Meshtastic/Views/Map/Custom/MapViewSwiftUI.swift b/Meshtastic/Views/Map/Custom/MapViewSwiftUI.swift index ce96e651..8a365612 100644 --- a/Meshtastic/Views/Map/Custom/MapViewSwiftUI.swift +++ b/Meshtastic/Views/Map/Custom/MapViewSwiftUI.swift @@ -105,6 +105,8 @@ struct MapViewSwiftUI: UIViewRepresentable { if !UserDefaults.enableOfflineMapsMBTiles { let overlay = TileOverlay() overlay.canReplaceMapContent = false + //overlay.minimumZ = 0 + //overlay.maximumZ = 17 mapView.addOverlay(overlay, level: UserDefaults.mapTilesAboveLabels ? .aboveLabels : .aboveRoads) } case .satellite: @@ -197,28 +199,14 @@ struct MapViewSwiftUI: UIViewRepresentable { print("Annotation Count: \(annotationCount) Map Annotations: \(mapView.annotations.count)") mapView.removeAnnotations(mapView.annotations) mapView.addAnnotations(waypoints) - + mapView.addAnnotations(showNodeHistory ? positions : latest) } if userTrackingMode == MKUserTrackingMode.none { mapView.showsUserLocation = false - - if UserDefaults.enableMapRecentering { - if annotationCount != mapView.annotations.count { - mapView.addAnnotations(showNodeHistory ? positions : latest) - } - if latest.count > 1 { - mapView.fitAllAnnotations() - } else { - mapView.fit(annotations:showNodeHistory ? positions : latest, andShow: false) - } - } } else { - // Centering Done by tracking mode - if annotationCount != mapView.annotations.count { - mapView.addAnnotations(showNodeHistory ? positions : latest) - } mapView.showsUserLocation = true } + mapView.fitAllAnnotations() mapView.setUserTrackingMode(userTrackingMode, animated: true) } From a6ca91d640e98db56c6f19ca9da521e4dce3d459 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Tue, 9 May 2023 07:49:22 -0700 Subject: [PATCH 12/13] Touch up RSSI display --- Meshtastic/Views/Helpers/Node/NodeInfoView.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Meshtastic/Views/Helpers/Node/NodeInfoView.swift b/Meshtastic/Views/Helpers/Node/NodeInfoView.swift index 818e0d51..48cc8515 100644 --- a/Meshtastic/Views/Helpers/Node/NodeInfoView.swift +++ b/Meshtastic/Views/Helpers/Node/NodeInfoView.swift @@ -65,8 +65,8 @@ struct NodeInfoView: View { .font(.title) .foregroundColor(.orange) .symbolRenderingMode(.hierarchical) - Text("Noisy Environment") - .font(.title3) + Text("Noisy") + .font(.title2) .foregroundColor(.orange) .multilineTextAlignment(.center) } @@ -187,8 +187,8 @@ struct NodeInfoView: View { .font(.callout) .foregroundColor(.orange) .symbolRenderingMode(.hierarchical) - Text("Noisy Environment") - .font(.caption2) + Text("Noisy") + .font(.caption) .multilineTextAlignment(.center) .foregroundColor(.orange) } From c42548665804b37a48411f4949ec694acfc5d949 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Tue, 9 May 2023 19:31:25 -0700 Subject: [PATCH 13/13] More tile sources tile server attribution Signal strength indicator --- Meshtastic.xcodeproj/project.pbxproj | 12 ++- Meshtastic/Enums/AppSettingsEnums.swift | 87 +++++++++++++++---- Meshtastic/Extensions/UserDefaults.swift | 2 +- .../Helpers/Map/OfflineTileManager.swift | 12 +-- Meshtastic/Model/PeripheralModel.swift | 8 +- ...swift => BLESignalStrengthIndicator.swift} | 6 +- .../Helpers/LoRaSignalStrengthIndicator.swift | 71 +++++++++++++++ .../Views/Helpers/Node/NodeInfoView.swift | 77 +++++----------- .../Views/Map/Custom/MapViewSwiftUI.swift | 12 ++- Meshtastic/Views/Messages/Contacts.swift | 2 +- Meshtastic/Views/Nodes/NodeList.swift | 2 +- Meshtastic/Views/Nodes/NodeMap.swift | 12 ++- 12 files changed, 209 insertions(+), 94 deletions(-) rename Meshtastic/Views/Helpers/{SignalStrengthIndicator.swift => BLESignalStrengthIndicator.swift} (95%) create mode 100644 Meshtastic/Views/Helpers/LoRaSignalStrengthIndicator.swift diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index 49095803..aec6aa6a 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -30,7 +30,7 @@ DD415828285859C4009B0E59 /* TelemetryConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD415827285859C4009B0E59 /* TelemetryConfig.swift */; }; DD41582A28585C32009B0E59 /* RangeTestConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD41582928585C32009B0E59 /* RangeTestConfig.swift */; }; DD41A61529AB0035003C5A37 /* NodeWeatherForecast.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD41A61429AB0035003C5A37 /* NodeWeatherForecast.swift */; }; - DD457188293C7E63000C49FB /* SignalStrengthIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD457187293C7E63000C49FB /* SignalStrengthIndicator.swift */; }; + DD457188293C7E63000C49FB /* BLESignalStrengthIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD457187293C7E63000C49FB /* BLESignalStrengthIndicator.swift */; }; DD47E3CE26F103C600029299 /* NodeList.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD47E3CD26F103C600029299 /* NodeList.swift */; }; DD47E3D626F17ED900029299 /* CircleText.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD47E3D526F17ED900029299 /* CircleText.swift */; }; DD4A911E2708C65400501B7E /* AppSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD4A911D2708C65400501B7E /* AppSettings.swift */; }; @@ -104,6 +104,7 @@ DDB75A162A0594AD006ED576 /* TileOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDB75A152A0594AD006ED576 /* TileOverlay.swift */; }; DDB75A1A2A05EB67006ED576 /* alpha.png in Resources */ = {isa = PBXBuildFile; fileRef = DDB75A192A05EB67006ED576 /* alpha.png */; }; DDB75A1C2A076DFA006ED576 /* TilesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDB75A1B2A076DFA006ED576 /* TilesView.swift */; }; + DDB75A1E2A0B0CD0006ED576 /* LoRaSignalStrengthIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDB75A1D2A0B0CD0006ED576 /* LoRaSignalStrengthIndicator.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 */; }; @@ -214,7 +215,7 @@ DD41A61429AB0035003C5A37 /* NodeWeatherForecast.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeWeatherForecast.swift; sourceTree = ""; }; DD41A61C29AE7E8E003C5A37 /* WidgetKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WidgetKit.framework; path = System/Library/Frameworks/WidgetKit.framework; sourceTree = SDKROOT; }; DD41A61E29AE7E8F003C5A37 /* SwiftUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftUI.framework; path = System/Library/Frameworks/SwiftUI.framework; sourceTree = SDKROOT; }; - DD457187293C7E63000C49FB /* SignalStrengthIndicator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignalStrengthIndicator.swift; sourceTree = ""; }; + DD457187293C7E63000C49FB /* BLESignalStrengthIndicator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BLESignalStrengthIndicator.swift; sourceTree = ""; }; DD457BC4295D5E35004BCE4D /* MeshtasticDataModelV5.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV5.xcdatamodel; sourceTree = ""; }; DD47E3CD26F103C600029299 /* NodeList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeList.swift; sourceTree = ""; }; DD47E3D526F17ED900029299 /* CircleText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CircleText.swift; sourceTree = ""; }; @@ -293,6 +294,7 @@ DDB75A152A0594AD006ED576 /* TileOverlay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TileOverlay.swift; sourceTree = ""; }; DDB75A192A05EB67006ED576 /* alpha.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = alpha.png; sourceTree = ""; }; DDB75A1B2A076DFA006ED576 /* TilesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TilesView.swift; sourceTree = ""; }; + DDB75A1D2A0B0CD0006ED576 /* LoRaSignalStrengthIndicator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoRaSignalStrengthIndicator.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 = ""; }; @@ -692,7 +694,8 @@ DDB6ABDA28B0AC6000384BA1 /* DistanceText.swift */, DD3CC6BD28E4CD9800FA9159 /* BatteryGauge.swift */, DD97E96528EFD9820056DDA4 /* MeshtasticLogo.swift */, - DD457187293C7E63000C49FB /* SignalStrengthIndicator.swift */, + DD457187293C7E63000C49FB /* BLESignalStrengthIndicator.swift */, + DDB75A1D2A0B0CD0006ED576 /* LoRaSignalStrengthIndicator.swift */, ); path = Helpers; sourceTree = ""; @@ -967,7 +970,7 @@ files = ( DDDB444829F8A9C900EE2349 /* String.swift in Sources */, DD5E520C298EE33B00D21B61 /* portnums.pb.swift in Sources */, - DD457188293C7E63000C49FB /* SignalStrengthIndicator.swift in Sources */, + DD457188293C7E63000C49FB /* BLESignalStrengthIndicator.swift in Sources */, DDFEB3BB29900C1200EE7472 /* CurrentConditionsCompact.swift in Sources */, DD836AE726F6B38600ABCC23 /* Connect.swift in Sources */, DD5E523F298F5A9E00D21B61 /* AirQualityIndexCompact.swift in Sources */, @@ -1057,6 +1060,7 @@ DDDB445029F8AC9C00EE2349 /* UIImage.swift in Sources */, DD86D40F2881BE4C00BAEB7A /* CsvDocument.swift in Sources */, DDB75A142A0593E2006ED576 /* OfflineTileManager.swift in Sources */, + DDB75A1E2A0B0CD0006ED576 /* LoRaSignalStrengthIndicator.swift in Sources */, DDA6B2E928419CF2003E8C16 /* MeshPackets.swift in Sources */, DDCE4E2C2869F92900BE9F8F /* UserConfig.swift in Sources */, DD6193752862F6E600E59241 /* ExternalNotificationConfig.swift in Sources */, diff --git a/Meshtastic/Enums/AppSettingsEnums.swift b/Meshtastic/Enums/AppSettingsEnums.swift index e882fd08..d805bdfd 100644 --- a/Meshtastic/Enums/AppSettingsEnums.swift +++ b/Meshtastic/Enums/AppSettingsEnums.swift @@ -139,51 +139,94 @@ enum MapLayer: String, CaseIterable, Equatable { enum MapTileServerLinks: String, CaseIterable, Identifiable { - case openStreetMaps + case openStreetMap + case openStreetMapDE + case openStreetMapFR + case openCycleMap + case openStreetMapHot + case openTopoMap case usgsTopo case usgsImageryTopo case usgsImageryOnly + case toner case watercolor var id: String { self.rawValue } var attribution: String { switch self { - case .openStreetMaps: - return "OpenStreetMap is a map of the world, created by people like you and free to use under an open license. © [OpenStreetMap](http://osm.org/copyright) contributors" + + case .openStreetMap: + return "Map and data © [OpenStreetMap](http://www.openstreetmap.org) and contributors, [CC-BY-SA](http://creativecommons.org/licenses/by-sa/2.0/)" + case .openStreetMapDE: + return "[OpenStreetMap DE](https://openstreetmap.de) map and data © [OpenStreetMap](http://www.openstreetmap.org) and contributors, [CC-BY-SA](http://creativecommons.org/licenses/by-sa/2.0/)" + case .openStreetMapFR: + return "[OpenStreetMap FR](https://www.openstreetmap.fr) map and data © [OpenStreetMap](http://www.openstreetmap.org) and contributors, [CC-BY-SA](http://creativecommons.org/licenses/by-sa/2.0/)" + case .openCycleMap: + return "[OpenCycleMap](https://www.cyclosm.org) map and data © [OpenStreetMap](http://www.openstreetmap.org) and contributors, [CC-BY-SA](http://creativecommons.org/licenses/by-sa/2.0/)" + case .openTopoMap: + return "[OpenTopoMap](https://opentopomap.org) map and data © [OpenStreetMap](http://www.openstreetmap.org) and contributors, [CC-BY-SA](http://creativecommons.org/licenses/by-sa/2.0/)" + case .openStreetMapHot: + return "[OpenStreetMap FR](https://www.openstreetmap.fr) map and data © [OpenStreetMap](http://www.openstreetmap.org) and contributors, [CC-BY-SA](http://creativecommons.org/licenses/by-sa/2.0/)" case .usgsTopo: - return "[USGS Topo](https://basemap.nationalmap.gov/arcgis/rest/services/USGSTopo/MapServer) is a tile cache base map service that combines the most current data in The National Map (TNM), and other public-domain data, into a multi-scale topographic reference map." + return "[USGS](https://basemap.nationalmap.gov/arcgis/rest/services/USGSTopo/MapServer) [National Map](http://nationalmap.gov/) topographic overlay." case .usgsImageryTopo: - return "[USGS Imagery Topo](https://basemap.nationalmap.gov/arcgis/rest/services/USGSImageryTopo/MapServer) is a tile cache base map of orthoimagery in The National Map and US Topo vector data." + return "[USGS](https://basemap.nationalmap.gov/arcgis/rest/services/USGSImageryTopo/MapServer) [National Map](http://nationalmap.gov/) imagery and topographic overlay." case .usgsImageryOnly: - return "[USGS Imagery Only](https://basemap.nationalmap.gov/arcgis/rest/services/USGSImageryOnly/MapServer) is a tile cache base map service of orthoimagery in The National Map." + return "[USGS](https://basemap.nationalmap.gov/arcgis/rest/services/USGSImageryOnly/MapServer) [National Map](http://nationalmap.gov/) imagery only overlay." + case .toner: + return "[Stamen Design's](https://github.com/stamen/toner-carto) black and white map tiles." case .watercolor: - return "Cooper Hewitt, Smithsonian Design Museum's [Watercolor Maptiles](https://watercolormaps.collection.cooperhewitt.org/) is a open-source mapping tool created by Stamen Design and built on OpenStreetMap data." + return "Cooper Hewitt, Smithsonian Design Museum's [Watercolor Maptiles](https://watercolormaps.collection.cooperhewitt.org/) is a open-source mapping tool created by Stamen Design and built on [OpenStreetMap](http://www.openstreetmap.org) data." } } var description: String { switch self { - case .openStreetMaps: - return "Open Street Maps" + case .openStreetMap: + return "Open Street Map" + case .openStreetMapDE: + return "Open Street Map DE" + case .openStreetMapFR: + return "Open Street Map FR" + case .openCycleMap: + return "Open Cycle Map" + case .openStreetMapHot: + return "Humanitarian (OSM)" + case.openTopoMap: + return "Open Topo Map" case .usgsTopo: return "USGS Topographic" case .usgsImageryTopo: return "USGS Topo Imagery" case .usgsImageryOnly: return "USGS Imagery Only" + case .toner: + return "Toner" case .watercolor: return "Watercolor Maptiles" } } var tileUrl: String { switch self { - case .openStreetMaps: + case .openStreetMap: return "https://tile.openstreetmap.org/{z}/{x}/{y}.png" + case .openStreetMapDE: + return "https://tile.openstreetmap.de/{z}/{x}/{y}.png" + case .openStreetMapFR: + return "https://a.tile.openstreetmap.fr/osmfr/{z}/{x}/{y}.png" + case .openCycleMap: + return "https://c.tile-cyclosm.openstreetmap.fr/cyclosm/{z}/{x}/{y}.png" + case .openStreetMapHot: + return "https://a.tile.openstreetmap.fr/hot/{z}/{x}/{y}.png" + case .openTopoMap: + return "https://a.tile.opentopomap.org/{z}/{x}/{y}.png" case .usgsTopo: return "https://basemap.nationalmap.gov/arcgis/rest/services/USGSTopo/MapServer/tile/{z}/{y}/{x}" case .usgsImageryTopo: return "https://basemap.nationalmap.gov/arcgis/rest/services/USGSImageryTopo/MapServer/tile/{z}/{y}/{x}" case .usgsImageryOnly: return "https://basemap.nationalmap.gov/arcgis/rest/services/USGSImageryOnly/MapServer/tile/{z}/{y}/{x}" + case .toner: + return "https://stamen-tiles.a.ssl.fastly.net/toner/{z}/{x}/{y}.png" case .watercolor: return "https://watercolormaps.collection.cooperhewitt.org/tile/watercolor/{z}/{x}/{y}.jpg" @@ -191,16 +234,28 @@ enum MapTileServerLinks: String, CaseIterable, Identifiable { } var zoomRange: [Int] { switch self { - case .openStreetMaps: - return [Int](0...17) + case .openStreetMap: + return [Int](0...18) + case .openStreetMapDE: + return [Int](0...18) + case .openStreetMapFR: + return [Int](0...18) + case .openCycleMap: + return [Int](0...18) + case .openTopoMap: + return [Int](0...18) + case .openStreetMapHot: + return [Int](0...18) case .usgsTopo: - return [Int](0...17) + return [Int](6...15) case .usgsImageryTopo: - return [Int](0...17) + return [Int](6...15) case .usgsImageryOnly: - return [Int](0...17) + return [Int](6...15) + case .toner: + return [Int](0...18) case .watercolor: - return [Int](0...17) + return [Int](0...18) } } } diff --git a/Meshtastic/Extensions/UserDefaults.swift b/Meshtastic/Extensions/UserDefaults.swift index d8ee5e28..94764f97 100644 --- a/Meshtastic/Extensions/UserDefaults.swift +++ b/Meshtastic/Extensions/UserDefaults.swift @@ -118,7 +118,7 @@ extension UserDefaults { static var mapTileServer: MapTileServerLinks { get { - MapTileServerLinks(rawValue: UserDefaults.standard.string(forKey: "mapTileServer") ?? MapTileServerLinks.openStreetMaps.rawValue) ?? MapTileServerLinks.openStreetMaps + MapTileServerLinks(rawValue: UserDefaults.standard.string(forKey: "mapTileServer") ?? MapTileServerLinks.openStreetMap.rawValue) ?? MapTileServerLinks.openStreetMap } set { UserDefaults.standard.set(newValue.rawValue, forKey: "mapTileServer") diff --git a/Meshtastic/Helpers/Map/OfflineTileManager.swift b/Meshtastic/Helpers/Map/OfflineTileManager.swift index 7a5e7e4b..4c8e5dba 100644 --- a/Meshtastic/Helpers/Map/OfflineTileManager.swift +++ b/Meshtastic/Helpers/Map/OfflineTileManager.swift @@ -22,7 +22,7 @@ class OfflineTileManager: ObservableObject { } // MARK: - Private properties - private var overlay: MKTileOverlay { MKTileOverlay(urlTemplate: UserDefaults.mapTileServer.tileUrl.count > 1 ? UserDefaults.mapTileServer.tileUrl : MapTileServerLinks.openStreetMaps.tileUrl) } + private var overlay: MKTileOverlay { MKTileOverlay(urlTemplate: UserDefaults.mapTileServer.tileUrl.count > 1 ? UserDefaults.mapTileServer.tileUrl : MapTileServerLinks.openStreetMap.tileUrl) } private var documentsDirectory: URL { fileManager.urls(for: .documentDirectory, in: .userDomainMask).first! } @@ -52,7 +52,7 @@ class OfflineTileManager: ObservableObject { 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 file = "tiles/\(UserDefaults.mapTileServer.id)-z\(path.z)x\(path.x)y\(path.y).png" let url = documentsDirectory.appendingPathComponent(file) accumulatedSize += (try? url.regularFileAllocatedSize()) ?? 0 } @@ -67,7 +67,7 @@ class OfflineTileManager: ObservableObject { 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 file = "tiles/\(UserDefaults.mapTileServer.id)-z\(path.z)x\(path.x)y\(path.y).png" let url = documentsDirectory.appendingPathComponent(file) try? fileManager.removeItem(at: url) } @@ -94,7 +94,7 @@ class OfflineTileManager: ObservableObject { } func getTileOverlay(for path: MKTileOverlayPath) -> URL { - let file = "z\(path.z)x\(path.x)y\(path.y).png" + let file = "\(UserDefaults.mapTileServer.id)-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){ @@ -134,7 +134,7 @@ class OfflineTileManager: ObservableObject { @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 file = "tiles/\(UserDefaults.mapTileServer.id)-z\(path.z)x\(path.x)y\(path.y).png" let filename = documentsDirectory.appendingPathComponent(file) do { let data = try Data(contentsOf: url) @@ -147,7 +147,7 @@ class OfflineTileManager: ObservableObject { private func filterTilesAlreadyExisting(paths: [MKTileOverlayPath]) -> [MKTileOverlayPath] { paths.filter { - let file = "z\($0.z)x\($0.x)y\($0.y).png" + let file = "\(UserDefaults.mapTileServer.id)-z\($0.z)x\($0.x)y\($0.y).png" let tilesPath = documentsDirectory.appendingPathComponent("tiles").appendingPathComponent(file).path return !fileManager.fileExists(atPath: tilesPath) } diff --git a/Meshtastic/Model/PeripheralModel.swift b/Meshtastic/Model/PeripheralModel.swift index 1b71e9e7..61cbfcca 100644 --- a/Meshtastic/Model/PeripheralModel.swift +++ b/Meshtastic/Model/PeripheralModel.swift @@ -24,13 +24,13 @@ struct Peripheral: Identifiable { self.peripheral = peripheral } - func getSignalStrength() -> SignalStrength { + func getSignalStrength() -> BLESignalStrength { if NSNumber(value: rssi).compare(NSNumber(-65)) == ComparisonResult.orderedDescending { - return SignalStrength.strong + return BLESignalStrength.strong } else if NSNumber(value: rssi).compare(NSNumber(-85)) == ComparisonResult.orderedDescending { - return SignalStrength.normal + return BLESignalStrength.normal } else { - return SignalStrength.weak + return BLESignalStrength.weak } } } diff --git a/Meshtastic/Views/Helpers/SignalStrengthIndicator.swift b/Meshtastic/Views/Helpers/BLESignalStrengthIndicator.swift similarity index 95% rename from Meshtastic/Views/Helpers/SignalStrengthIndicator.swift rename to Meshtastic/Views/Helpers/BLESignalStrengthIndicator.swift index afb84f43..c5d17f16 100644 --- a/Meshtastic/Views/Helpers/SignalStrengthIndicator.swift +++ b/Meshtastic/Views/Helpers/BLESignalStrengthIndicator.swift @@ -32,7 +32,7 @@ import Foundation import SwiftUI struct SignalStrengthIndicator: View { - let signalStrength: SignalStrength + let signalStrength: BLESignalStrength var body: some View { HStack { @@ -40,7 +40,7 @@ struct SignalStrengthIndicator: View { RoundedRectangle(cornerRadius: 3) .divided(amount: (CGFloat(bar) + 1) / CGFloat(3)) .fill(getColor().opacity(bar <= signalStrength.rawValue ? 1 : 0.3)) - .frame(width: 8, height: 30) + .frame(width: 8, height: 40) } } } @@ -71,7 +71,7 @@ extension Shape { } } -enum SignalStrength: Int { +enum BLESignalStrength: Int { case weak = 0 case normal = 1 case strong = 2 diff --git a/Meshtastic/Views/Helpers/LoRaSignalStrengthIndicator.swift b/Meshtastic/Views/Helpers/LoRaSignalStrengthIndicator.swift new file mode 100644 index 00000000..56357168 --- /dev/null +++ b/Meshtastic/Views/Helpers/LoRaSignalStrengthIndicator.swift @@ -0,0 +1,71 @@ +// +// LoRaSignalStrengthIndicator.swift +// Meshtastic +// +// Copyright Garth Vander Houwen 5/9/23. +// + +import Foundation + +import Foundation +import SwiftUI + +struct LoRaSignalStrengthIndicator: View { + let signalStrength: LoRaSignalStrength + + var body: some View { + HStack { + ForEach(0..<3) { bar in + RoundedRectangle(cornerRadius: 3) + .divided(amount: (CGFloat(bar) + 1) / CGFloat(3)) + .fill(getColor().opacity(bar <= signalStrength.rawValue ? 1 : 0.3)) + .frame(width: 8, height: 40) + } + } + } + + private func getColor() -> Color { + switch signalStrength { + case .none: + return Color.red + case .bad: + return Color.orange + case .fair: + return Color.yellow + case .good: + return Color.green + } + } +} + +enum LoRaSignalStrength: Int { + case none = 0 + case bad = 1 + case fair = 2 + case good = 3 + var description: String { + switch self { + case .none: + return "None" + case .bad: + return "Bad" + case .fair: + return "Fair" + case .good: + return "Good" + } + } +} + +func getLoRaSignalStrength(snr: Float, rssi: Int32) -> LoRaSignalStrength { + + if rssi > -115 && snr > -7 { + return .good + } else if rssi < -126 && snr < -15 { + return .none + } else if rssi <= -120 || snr <= -13 { + return .bad + } else { + return .fair + } +} diff --git a/Meshtastic/Views/Helpers/Node/NodeInfoView.swift b/Meshtastic/Views/Helpers/Node/NodeInfoView.swift index 48cc8515..255d6788 100644 --- a/Meshtastic/Views/Helpers/Node/NodeInfoView.swift +++ b/Meshtastic/Views/Helpers/Node/NodeInfoView.swift @@ -28,7 +28,7 @@ struct NodeInfoView: View { if UIDevice.current.userInterfaceIdiom == .pad || UIDevice.current.userInterfaceIdiom == .mac { HStack { VStack(alignment: .center) { - CircleText(text: node.user?.shortName ?? "???", color: Color(UIColor(hex: UInt32(node.num))), circleSize: 75, fontSize: (node.user?.shortName ?? "???").isEmoji() ? 48 : 24, textColor: UIColor(hex: UInt32(node.num)).isLight() ? .black : .white ) + CircleText(text: node.user?.shortName ?? "???", color: Color(UIColor(hex: UInt32(node.num))), circleSize: 150, fontSize: (node.user?.shortName ?? "???").isEmoji() ? 105 : 55, textColor: UIColor(hex: UInt32(node.num)).isLight() ? .black : .white ) } Divider() VStack { @@ -44,40 +44,25 @@ struct NodeInfoView: View { .font(.title).fixedSize() } } - - if node.snr > 0 { - Divider() + Divider() + if node.snr != 0 { VStack(alignment: .center) { - - Image(systemName: "waveform.path") - .font(.title) - .foregroundColor(.accentColor) - .symbolRenderingMode(.hierarchical) - .padding(.bottom, 10) - Text("SNR").font(.largeTitle).fixedSize() - Text("\(String(format: "%.2f", node.snr)) dB") - .font(.largeTitle) + let signalStrength = getLoRaSignalStrength(snr: node.snr, rssi: node.rssi) + LoRaSignalStrengthIndicator(signalStrength: signalStrength) + Text("Signal \(signalStrength.description)").font(.title) + Text("SNR \(String(format: "%.2f", node.snr))dB") .foregroundColor(.gray) - .fixedSize() - - if (node.rssi > -115) && (node.snr <= -13) { - Image(systemName: "waveform.slash") - .font(.title) - .foregroundColor(.orange) - .symbolRenderingMode(.hierarchical) - Text("Noisy") - .font(.title2) - .foregroundColor(.orange) - .multilineTextAlignment(.center) - } + .font(.title3) + Text("RSSI \(node.rssi)dB") + .foregroundColor(.gray) + .font(.title3) } - + Divider() } let deviceMetrics = node.telemetries?.filtered(using: NSPredicate(format: "metricsType == 0")) if deviceMetrics?.count ?? 0 >= 1 { let mostRecent = deviceMetrics?.lastObject as? TelemetryEntity - Divider() VStack(alignment: .center) { BatteryGauge(batteryLevel: Double(mostRecent?.batteryLevel ?? 0)) if mostRecent?.voltage ?? 0 > 0.0 { @@ -154,7 +139,7 @@ struct NodeInfoView: View { HStack { VStack(alignment: .center) { - CircleText(text: node.user?.shortName ?? "???", color: Color(UIColor(hex: UInt32(node.num))), circleSize: 65, fontSize: (node.user?.shortName ?? "???").isEmoji() ? 40 : 24, textColor: UIColor(hex: UInt32(node.num)).isLight() ? .black : .white ) + CircleText(text: node.user?.shortName ?? "???", color: Color(UIColor(hex: UInt32(node.num))), circleSize: 65, fontSize: (node.user?.shortName ?? "???").isEmoji() ? 42 : 20, textColor: UIColor(hex: UInt32(node.num)).isLight() ? .black : .white ) } Divider() VStack { @@ -167,39 +152,25 @@ struct NodeInfoView: View { .font(.caption).fixedSize() } } - - if node.snr > 0 { - Divider() + Divider() + if node.snr != 0 { VStack(alignment: .center) { - - Image(systemName: "waveform.path") - .font(.title) - .foregroundColor(.accentColor) - .symbolRenderingMode(.hierarchical) - Text("SNR").font(.title2).fixedSize() - Text("\(String(format: "%.2f", node.snr)) dB") - .font(.title2) + let signalStrength = getLoRaSignalStrength(snr: node.snr, rssi: node.rssi) + LoRaSignalStrengthIndicator(signalStrength: signalStrength) + Text("Signal \(signalStrength.description)").font(.footnote) + Text("SNR \(String(format: "%.2f", node.snr))dB") .foregroundColor(.gray) - .fixedSize() - - if (node.rssi > -115) && (node.snr <= -13) { - Image(systemName: "waveform.slash") - .font(.callout) - .foregroundColor(.orange) - .symbolRenderingMode(.hierarchical) - Text("Noisy") - .font(.caption) - .multilineTextAlignment(.center) - .foregroundColor(.orange) - } + .font(.caption2) + Text("RSSI \(node.rssi)dB") + .foregroundColor(.gray) + .font(.caption2) } + Divider() } - let deviceMetrics = node.telemetries?.filtered(using: NSPredicate(format: "metricsType == 0")) if deviceMetrics?.count ?? 0 >= 1 { let mostRecent = deviceMetrics?.lastObject as? TelemetryEntity - Divider() VStack(alignment: .center) { BatteryGauge(batteryLevel: Double(mostRecent?.batteryLevel ?? 0)) if mostRecent?.voltage ?? 0 > 0 { diff --git a/Meshtastic/Views/Map/Custom/MapViewSwiftUI.swift b/Meshtastic/Views/Map/Custom/MapViewSwiftUI.swift index 8a365612..1bfaa379 100644 --- a/Meshtastic/Views/Map/Custom/MapViewSwiftUI.swift +++ b/Meshtastic/Views/Map/Custom/MapViewSwiftUI.swift @@ -203,10 +203,20 @@ struct MapViewSwiftUI: UIViewRepresentable { } if userTrackingMode == MKUserTrackingMode.none { mapView.showsUserLocation = false + + if UserDefaults.enableMapRecentering { + + if latest.count == 1 { + mapView.fit(annotations: showNodeHistory ? positions : latest, andShow: true) + } else { + mapView.fitAllAnnotations() + } + } + } else { mapView.showsUserLocation = true } - mapView.fitAllAnnotations() + mapView.setUserTrackingMode(userTrackingMode, animated: true) } diff --git a/Meshtastic/Views/Messages/Contacts.swift b/Meshtastic/Views/Messages/Contacts.swift index caf7cced..72cfc3de 100644 --- a/Meshtastic/Views/Messages/Contacts.swift +++ b/Meshtastic/Views/Messages/Contacts.swift @@ -148,7 +148,7 @@ struct Contacts: View { HStack { VStack { HStack { - CircleText(text: user.shortName ?? "???", color: Color(UIColor(hex: UInt32(user.num))), circleSize: 60, fontSize: (user.shortName ?? "???").isEmoji() ? 36 : 18, textColor: UIColor(hex: UInt32(user.num)).isLight() ? .black : .white) + CircleText(text: user.shortName ?? "???", color: Color(UIColor(hex: UInt32(user.num))), circleSize: 60, fontSize: (user.shortName ?? "???").isEmoji() ? 42 : 20, textColor: UIColor(hex: UInt32(user.num)).isLight() ? .black : .white) .padding(.trailing, 5) VStack { HStack { diff --git a/Meshtastic/Views/Nodes/NodeList.swift b/Meshtastic/Views/Nodes/NodeList.swift index 511136d2..45ffb00f 100644 --- a/Meshtastic/Views/Nodes/NodeList.swift +++ b/Meshtastic/Views/Nodes/NodeList.swift @@ -35,7 +35,7 @@ struct NodeList: View { let connected: Bool = (bleManager.connectedPeripheral != nil && bleManager.connectedPeripheral?.num ?? -1 == node.num) VStack(alignment: .leading) { HStack { - CircleText(text: node.user?.shortName ?? "???", color: Color(UIColor(hex: UInt32(node.num))), circleSize: 65, fontSize: (node.user?.shortName ?? "???").isEmoji() ? 40 : 20, brightness: 0.0, textColor: UIColor(hex: UInt32(node.num)).isLight() ? .black : .white) + CircleText(text: node.user?.shortName ?? "???", color: Color(UIColor(hex: UInt32(node.num))), circleSize: 65, fontSize: (node.user?.shortName ?? "???").isEmoji() ? 44 : 22, brightness: 0.0, textColor: UIColor(hex: UInt32(node.num)).isLight() ? .black : .white) .padding(.trailing, 5) VStack(alignment: .leading) { Text(node.user?.longName ?? "unknown".localized).font(.headline) diff --git a/Meshtastic/Views/Nodes/NodeMap.swift b/Meshtastic/Views/Nodes/NodeMap.swift index 160063c0..f271c24d 100644 --- a/Meshtastic/Views/Nodes/NodeMap.swift +++ b/Meshtastic/Views/Nodes/NodeMap.swift @@ -153,7 +153,7 @@ struct NodeMap: View { } } if UserDefaults.enableOfflineMaps { - VStack { + VStack (alignment: .leading) { if !enableOfflineMapsMBTiles { @@ -166,12 +166,16 @@ struct NodeMap: View { .pickerStyle(DefaultPickerStyle()) .onChange(of: (selectedTileServer)) { newSelectedTileServer in UserDefaults.mapTileServer = newSelectedTileServer - tileManager.removeAll() + //tileManager.removeAll() selectedMapLayer = .standard } + Text("Attribution:") + .fontWeight(.semibold) + .font(.footnote) Text(LocalizedStringKey(selectedTileServer.attribution)) - .font(.caption) + .font(.footnote) .foregroundColor(.gray) + .padding(0) Divider() Toggle(isOn: $mapTilesAboveLabels) { Text("Tiles above Labels") @@ -193,7 +197,7 @@ struct NodeMap: View { UserDefaults.enableOfflineMapsMBTiles = self.enableOfflineMapsMBTiles } Text("The latest MBTiles file shared with meshtastic will be loaded into the map.") - .font(.caption) + .font(.footnote) .foregroundColor(.gray) } }