Somewhat working offline maps

This commit is contained in:
Garth Vander Houwen 2023-05-06 16:15:12 -07:00
parent 28bfa99612
commit e1bf4b0212
14 changed files with 183 additions and 230 deletions

View file

@ -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 = "<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>"; };
DDB75A192A05EB67006ED576 /* alpha.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = alpha.png; 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>"; };
@ -559,7 +559,6 @@
children = (
DDB75A132A0593E2006ED576 /* OfflineTileManager.swift */,
DDB75A152A0594AD006ED576 /* TileOverlay.swift */,
DDB75A172A05975A006ED576 /* TilesDownloadView.swift */,
);
path = Map;
sourceTree = "<group>";
@ -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 */,

View file

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

View file

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

View file

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

View file

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

View file

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

View file

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

Before After
Before After

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -200,6 +200,7 @@
"not.connected"="未连接到电台";
"numbers.punctuation"="数字和标点符号";
"off"="关闭";
"offline"="Offline";
"on.boot"="仅在启动时";
"options"="选项";
"password"="密码";