Initial offline maps setup

This commit is contained in:
Garth Vander Houwen 2023-05-05 17:13:35 -07:00
parent 81138ade33
commit 60a1687839
13 changed files with 433 additions and 27 deletions

View file

@ -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 */,

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

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

View 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: [:])
}
}

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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