mirror of
https://github.com/meshtastic/Meshtastic-Apple.git
synced 2026-04-20 22:13:56 +00:00
Initial offline maps setup
This commit is contained in:
parent
81138ade33
commit
60a1687839
13 changed files with 433 additions and 27 deletions
|
|
@ -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 = "<group>"; };
|
||||
DDB6ABE528B1406100384BA1 /* LoraConfigEnums.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoraConfigEnums.swift; sourceTree = "<group>"; };
|
||||
DDB759E12A04B264006ED576 /* MeshtasticDataModelV12.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV12.xcdatamodel; sourceTree = "<group>"; };
|
||||
DDB75A0E2A05920E006ED576 /* FileManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileManager.swift; sourceTree = "<group>"; };
|
||||
DDB75A102A059258006ED576 /* Url.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Url.swift; sourceTree = "<group>"; };
|
||||
DDB75A132A0593E2006ED576 /* OfflineTileManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OfflineTileManager.swift; sourceTree = "<group>"; };
|
||||
DDB75A152A0594AD006ED576 /* TileOverlay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TileOverlay.swift; sourceTree = "<group>"; };
|
||||
DDB75A172A05975A006ED576 /* TilesDownloadView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TilesDownloadView.swift; sourceTree = "<group>"; };
|
||||
DDBA45EC299ED78100DEEDDC /* MeshtasticDataModelV8.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV8.xcdatamodel; sourceTree = "<group>"; };
|
||||
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 = "<group>"; };
|
||||
|
|
@ -544,6 +554,16 @@
|
|||
path = Protobufs;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
DDB75A122A0593CD006ED576 /* Map */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
DDB75A132A0593E2006ED576 /* OfflineTileManager.swift */,
|
||||
DDB75A152A0594AD006ED576 /* TileOverlay.swift */,
|
||||
DDB75A172A05975A006ED576 /* TilesDownloadView.swift */,
|
||||
);
|
||||
path = Map;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
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 = "<group>";
|
||||
|
|
@ -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 */,
|
||||
|
|
|
|||
65
Meshtastic/Extensions/FileManager.swift
Normal file
65
Meshtastic/Extensions/FileManager.swift
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
//
|
||||
// FileManager.swift
|
||||
// Meshtastic
|
||||
//
|
||||
// Copyright(c) Garth Vander Houwen 5/5/23.
|
||||
//
|
||||
import Foundation
|
||||
|
||||
let allocatedSizeResourceKeys: Set<URLResourceKey> = [
|
||||
.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
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
21
Meshtastic/Extensions/Url.swift
Normal file
21
Meshtastic/Extensions/Url.swift
Normal file
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
161
Meshtastic/Helpers/Map/OfflineTileManager.swift
Normal file
161
Meshtastic/Helpers/Map/OfflineTileManager.swift
Normal file
|
|
@ -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..<filteredPaths.count {
|
||||
self.persistLocally(path: filteredPaths[i])
|
||||
self.progress = Float(i) / Float(filteredPaths.count)
|
||||
}
|
||||
DispatchQueue.main.async {
|
||||
//NotificationManager.shared.sendNotification(title: "\("DownloadedTitle".localized) (\((self.getDownloadedSize(for: boundingBox)).toBytes))", message: "\("Downloaded".localized) (\(name))")
|
||||
self.progress = 0
|
||||
self.status = .downloaded
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func getTileOverlay(for path: MKTileOverlayPath) -> 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: [:])
|
||||
}
|
||||
|
||||
}
|
||||
15
Meshtastic/Helpers/Map/TileOverlay.swift
Normal file
15
Meshtastic/Helpers/Map/TileOverlay.swift
Normal file
|
|
@ -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) }
|
||||
}
|
||||
87
Meshtastic/Helpers/Map/TilesDownloadView.swift
Normal file
87
Meshtastic/Helpers/Map/TilesDownloadView.swift
Normal file
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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<WaypointEntity>
|
||||
|
||||
|
||||
@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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue