From 0c852a52024968f5ee0e068c422c8d038193d6b1 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 14 May 2023 00:16:55 -0700 Subject: [PATCH] Weather layer --- Meshtastic/Enums/AppSettingsEnums.swift | 89 +++++++++++++++++- Meshtastic/Extensions/UserDefaults.swift | 23 ++++- .../Helpers/Map/OfflineTileManager.swift | 6 +- .../Views/Map/Custom/MapViewSwiftUI.swift | 14 ++- Meshtastic/Views/Nodes/NodeMap.swift | 94 +++++++++++++------ Meshtastic/Views/Settings/AppSettings.swift | 52 +++++----- 6 files changed, 212 insertions(+), 66 deletions(-) diff --git a/Meshtastic/Enums/AppSettingsEnums.swift b/Meshtastic/Enums/AppSettingsEnums.swift index 8044cde8..5c964f0c 100644 --- a/Meshtastic/Enums/AppSettingsEnums.swift +++ b/Meshtastic/Enums/AppSettingsEnums.swift @@ -137,7 +137,7 @@ enum MapLayer: String, CaseIterable, Equatable { var localized: String { self.rawValue.localized } } -enum MapTileServerLinks: String, CaseIterable, Identifiable { +enum MapTileServer: String, CaseIterable, Identifiable { case openStreetMap case openStreetMapDE @@ -269,7 +269,86 @@ enum MapTileServerLinks: String, CaseIterable, Identifiable { } } -//enum MapOverlayServerLinks: String, CaseIterable, Identifiable { -// -// -//} +enum MapOverlayServer: String, CaseIterable, Identifiable { + + case baseReReflectivityCurrent + case baseReReflectivityOneHourAgo + case echoTopsEetCurrent + case echoTopsEetOneHourAgo + case q2OneHourPrecipitation + case q2TwentyFourHourPrecipitation + case q2FortyEightHourPrecipitation + case q2SeventyTwoHourPrecipitation + case mrmsHybridScanReflectivityComposite + + var id: String { self.rawValue } + var attribution: String { + return "Weather layers via Iowa State University Iowa Environmental Mesonet [OGC Web Services](https://mesonet.agron.iastate.edu/ogc/)" + } + var description: String { + switch self { + case .baseReReflectivityCurrent: + return "NEXRAD Base Reflectivity current" + case .baseReReflectivityOneHourAgo: + return "NEXRAD Base Reflectivity one hour ago" + case .echoTopsEetCurrent: + return "NEXRAD Echo Tops EET current" + case .echoTopsEetOneHourAgo: + return "NEXRAD Echo Tops EET one hour ago" + case .q2OneHourPrecipitation: + return "Q2 1 Hour Precipitation" + case .q2TwentyFourHourPrecipitation: + return "Q2 24 Hour Precipitation" + case .q2FortyEightHourPrecipitation: + return "Q2 48 Hour Precipitation" + case .q2SeventyTwoHourPrecipitation: + return "Q2 72 Hour Precipitation" + case .mrmsHybridScanReflectivityComposite: + return "MRMS Hybrid-Scan Reflectivity Composite" + } + } + var tileUrl: String { + switch self { + case .baseReReflectivityCurrent: + return "https://mesonet.agron.iastate.edu/cache/tile.py/1.0.0/nexrad-n0q-900913/{z}/{x}/{y}" + case .baseReReflectivityOneHourAgo: + return "https://mesonet.agron.iastate.edu/cache/tile.py/1.0.0/nexrad-n0q-900913-m55m/{z}/{x}/{y}" + case .echoTopsEetCurrent: + return "https://mesonet.agron.iastate.edu/cache/tile.py/1.0.0/nexrad-eet-900913/{z}/{x}/{y}" + case .echoTopsEetOneHourAgo: + return "https://mesonet.agron.iastate.edu/cache/tile.py/1.0.0/nexrad-eet-900913-m55m/{z}/{x}/{y}" + case .q2OneHourPrecipitation: + return "https://mesonet.agron.iastate.edu/cache/tile.py/1.0.0/q2-n1p-900913/{z}/{x}/{y}" + case .q2TwentyFourHourPrecipitation: + return "https://mesonet.agron.iastate.edu/cache/tile.py/1.0.0/q2-p24h-900913/{z}/{x}/{y}" + case .q2FortyEightHourPrecipitation: + return "https://mesonet.agron.iastate.edu/cache/tile.py/1.0.0/q2-p48h-900913/{z}/{x}/{y}" + case .q2SeventyTwoHourPrecipitation: + return "https://mesonet.agron.iastate.edu/cache/tile.py/1.0.0/q2-p72h-900913/{z}/{x}/{y}" + case .mrmsHybridScanReflectivityComposite: + return "https://mesonet.agron.iastate.edu/cache/tile.py/1.0.0/q2-hsr-900913/{z}/{x}/{y}" + } + } + var zoomRange: [Int] { + switch self { + case .baseReReflectivityCurrent: + return [Int](0...18) + case .baseReReflectivityOneHourAgo: + return [Int](0...18) + case .echoTopsEetCurrent: + return [Int](0...18) + case .echoTopsEetOneHourAgo: + return [Int](0...18) + case .q2OneHourPrecipitation: + return [Int](0...18) + case .q2TwentyFourHourPrecipitation: + return [Int](0...18) + case .q2FortyEightHourPrecipitation: + return [Int](0...18) + case .q2SeventyTwoHourPrecipitation: + return [Int](0...18) + case .mrmsHybridScanReflectivityComposite: + return [Int](0...18) + } + } +} diff --git a/Meshtastic/Extensions/UserDefaults.swift b/Meshtastic/Extensions/UserDefaults.swift index 94764f97..b711f89c 100644 --- a/Meshtastic/Extensions/UserDefaults.swift +++ b/Meshtastic/Extensions/UserDefaults.swift @@ -115,16 +115,35 @@ extension UserDefaults { } } - static var mapTileServer: MapTileServerLinks { + static var mapTileServer: MapTileServer { get { - MapTileServerLinks(rawValue: UserDefaults.standard.string(forKey: "mapTileServer") ?? MapTileServerLinks.openStreetMap.rawValue) ?? MapTileServerLinks.openStreetMap + MapTileServer(rawValue: UserDefaults.standard.string(forKey: "mapTileServer") ?? MapTileServer.openStreetMap.rawValue) ?? MapTileServer.openStreetMap } set { UserDefaults.standard.set(newValue.rawValue, forKey: "mapTileServer") } } + static var enableOverlayServer: Bool { + get { + UserDefaults.standard.bool(forKey: "enableOverlayServer") + } + set { + UserDefaults.standard.set(newValue, forKey: "enableOverlayServer") + } + } + + static var mapOverlayServer: MapOverlayServer { + get { + + MapOverlayServer(rawValue: UserDefaults.standard.string(forKey: "mapOverlayServer") ?? MapOverlayServer.baseReReflectivityCurrent.rawValue) ?? MapOverlayServer.baseReReflectivityCurrent + } + set { + UserDefaults.standard.set(newValue.rawValue, forKey: "mapOverlayServer") + } + } + static var mapTilesAboveLabels: Bool { get { UserDefaults.standard.bool(forKey: "mapTilesAboveLabels") diff --git a/Meshtastic/Helpers/Map/OfflineTileManager.swift b/Meshtastic/Helpers/Map/OfflineTileManager.swift index 640fa078..fe6e4508 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.openStreetMap.tileUrl) } + private var overlay: MKTileOverlay { MKTileOverlay(urlTemplate: UserDefaults.mapTileServer.tileUrl.count > 1 ? UserDefaults.mapTileServer.tileUrl : MapTileServer.openStreetMap.tileUrl) } private var documentsDirectory: URL { fileManager.urls(for: .documentDirectory, in: .userDomainMask).first! } @@ -48,7 +48,7 @@ class OfflineTileManager: ObservableObject { return Double(count) * size } - func getDownloadedSize(for mapTileLink: MapTileServerLinks) -> Double { + func getDownloadedSize(for mapTileLink: MapTileServer) -> Double { var accumulatedSize: UInt64 = 0 let mapTiles = try! fileManager.contentsOfDirectory(at: documentsDirectory.appendingPathComponent("tiles"), includingPropertiesForKeys: []) @@ -90,7 +90,7 @@ class OfflineTileManager: ObservableObject { createDirectoriesIfNecessary() } - func remove(for mapTileLink: MapTileServerLinks) { + func remove(for mapTileLink: MapTileServer) { let mapTiles = try! fileManager.contentsOfDirectory(at: documentsDirectory.appendingPathComponent("tiles"), includingPropertiesForKeys: []) let matchingTiles = mapTiles.filter { fileName in diff --git a/Meshtastic/Views/Map/Custom/MapViewSwiftUI.swift b/Meshtastic/Views/Map/Custom/MapViewSwiftUI.swift index bd4e9e36..76a1a840 100644 --- a/Meshtastic/Views/Map/Custom/MapViewSwiftUI.swift +++ b/Meshtastic/Views/Map/Custom/MapViewSwiftUI.swift @@ -19,7 +19,8 @@ struct MapViewSwiftUI: UIViewRepresentable { let mapView = MKMapView() // Parameters - var selectedMapLayer: MapLayer + let selectedMapLayer: MapLayer + let selectedWeatherLayer: MapOverlayServer = UserDefaults.mapOverlayServer let positions: [PositionEntity] let waypoints: [WaypointEntity] @@ -116,6 +117,17 @@ struct MapViewSwiftUI: UIViewRepresentable { default: mapView.mapType = .standard } + // Weather radar + if UserDefaults.enableOverlayServer { + let locale = Locale.current + if locale.region?.identifier ?? "no locale" == "US" { + let overlay = MKTileOverlay(urlTemplate: selectedWeatherLayer.tileUrl) + overlay.canReplaceMapContent = false + overlay.minimumZ = selectedWeatherLayer.zoomRange.startIndex + overlay.maximumZ = selectedWeatherLayer.zoomRange.endIndex + mapView.addOverlay(overlay, level: .aboveLabels) + } + } } func makeUIView(context: Context) -> MKMapView { diff --git a/Meshtastic/Views/Nodes/NodeMap.swift b/Meshtastic/Views/Nodes/NodeMap.swift index 30e76b4b..9ebe55ee 100644 --- a/Meshtastic/Views/Nodes/NodeMap.swift +++ b/Meshtastic/Views/Nodes/NodeMap.swift @@ -11,62 +11,64 @@ import CoreLocation import CoreData 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 selectedTileServer: MapTileServerLinks = UserDefaults.mapTileServer + @State var selectedTileServer: MapTileServer = UserDefaults.mapTileServer @State var enableOfflineMapsMBTiles: Bool = UserDefaults.enableOfflineMapsMBTiles + @State var enableOverlayServer: Bool = UserDefaults.enableOverlayServer + @State var selectedOverlayServer: MapOverlayServer = UserDefaults.mapOverlayServer @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) private var positions: FetchedResults - + @FetchRequest(sortDescriptors: [NSSortDescriptor(key: "name", ascending: false)], predicate: NSPredicate( format: "expire == nil || expire >= %@", Date() as NSDate ), animation: .none) private var waypoints: FetchedResults @State var waypointCoordinate: WaypointCoordinate? - + @State var selectedTracking: UserTrackingModes = .none @State var isPresentingInfoSheet: Bool = false @State private var customMapOverlay: MapViewSwiftUI.CustomMapOverlay? = MapViewSwiftUI.CustomMapOverlay( - mapName: "offlinemap", - tileType: "png", - canReplaceMapContent: true - ) - + mapName: "offlinemap", + tileType: "png", + canReplaceMapContent: true + ) + var body: some View { - + NavigationStack { ZStack { - + MapViewSwiftUI( - onLongPress: { coord in + onLongPress: { coord in waypointCoordinate = WaypointCoordinate(id: .init(), coordinate: coord, waypointId: 0) }, onWaypointEdit: { wpId in if wpId > 0 { waypointCoordinate = WaypointCoordinate(id: .init(), coordinate: nil, waypointId: Int64(wpId)) } }, - selectedMapLayer: selectedMapLayer, - positions: Array(positions), - waypoints: Array(waypoints), - userTrackingMode: selectedTracking.MKUserTrackingModeValue(), - showNodeHistory: enableMapNodeHistoryPins, - showRouteLines: enableMapRouteLines, - customMapOverlay: self.customMapOverlay + selectedMapLayer: selectedMapLayer, + positions: Array(positions), + waypoints: Array(waypoints), + userTrackingMode: selectedTracking.MKUserTrackingModeValue(), + showNodeHistory: enableMapNodeHistoryPins, + showRouteLines: enableMapRouteLines, + customMapOverlay: self.customMapOverlay ) VStack(alignment: .trailing) { @@ -84,9 +86,9 @@ struct NodeMap: View { .ignoresSafeArea(.all, edges: [.top, .leading, .trailing]) .frame(maxHeight: .infinity) .sheet(item: $waypointCoordinate, content: { wpc in - WaypointFormView(coordinate: wpc) - .presentationDetents([.medium, .large]) - .presentationDragIndicator(.automatic) + WaypointFormView(coordinate: wpc) + .presentationDetents([.medium, .large]) + .presentationDragIndicator(.automatic) }) .sheet(isPresented: $isPresentingInfoSheet) { VStack { @@ -137,6 +139,38 @@ struct NodeMap: View { self.enableMapRouteLines.toggle() UserDefaults.enableMapRouteLines = self.enableMapRouteLines } + + let locale = Locale.current + if locale.region?.identifier ?? "no locale" == "US" { + + Toggle(isOn: $enableOverlayServer) { + + Label("Show Weather", systemImage: "cloud.fill") + } + .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + .onTapGesture { + self.enableOverlayServer.toggle() + UserDefaults.enableOverlayServer = self.enableOverlayServer + } + + if enableOverlayServer { + Picker(selection: $selectedOverlayServer, + label: Text("Radar")) { + ForEach(MapOverlayServer.allCases, id: \.self) { mos in + Text(mos.description) + .font(.footnote) + } + } + .pickerStyle(DefaultPickerStyle()) + .onChange(of: (selectedOverlayServer)) { newSelectedOverlayServer in + UserDefaults.mapOverlayServer = newSelectedOverlayServer + } + Text(LocalizedStringKey(selectedOverlayServer.attribution)) + .font(.footnote) + .foregroundColor(.gray) + .padding(0) + } + } } Section(header: Text("Offline Maps")) { Toggle(isOn: $enableOfflineMaps) { @@ -158,14 +192,14 @@ struct NodeMap: View { Picker(selection: $selectedTileServer, label: Text("Tile Server")) { - ForEach(MapTileServerLinks.allCases, id: \.self) { tsl in + ForEach(MapTileServer.allCases, id: \.self) { tsl in Text(tsl.description) } } - .pickerStyle(DefaultPickerStyle()) - .onChange(of: (selectedTileServer)) { newSelectedTileServer in - UserDefaults.mapTileServer = newSelectedTileServer - } + .pickerStyle(DefaultPickerStyle()) + .onChange(of: (selectedTileServer)) { newSelectedTileServer in + UserDefaults.mapTileServer = newSelectedTileServer + } Text("Attribution:") .fontWeight(.semibold) .font(.footnote) @@ -212,7 +246,7 @@ struct NodeMap: View { .padding(.bottom) #endif } - .presentationDetents([UserDefaults.enableOfflineMaps ? .large : .medium]) + .presentationDetents([UserDefaults.enableOfflineMaps || UserDefaults.enableOverlayServer ? .large : .medium]) .presentationDragIndicator(.visible) } } diff --git a/Meshtastic/Views/Settings/AppSettings.swift b/Meshtastic/Views/Settings/AppSettings.swift index 4310d105..4ad86d70 100644 --- a/Meshtastic/Views/Settings/AppSettings.swift +++ b/Meshtastic/Views/Settings/AppSettings.swift @@ -16,7 +16,6 @@ struct AppSettings: View { @State var provideLocationInterval: Int = UserDefaults.provideLocationInterval @State private var isPresentingCoreDataResetConfirm = false @State private var isPresentingDeleteMapTilesConfirm = false - @State var selectedTileServer: MapTileServerLinks? = nil var body: some View { VStack { @@ -106,33 +105,36 @@ struct AppSettings: View { } } } - Section(header: Text("Map Tile Data")) { - Button { - isPresentingDeleteMapTilesConfirm = true - } label: { - Label("\("map.tiles.delete".localized) (\(totalDownloadedTileSize))", systemImage: "trash") - .foregroundColor(.red) - } - .confirmationDialog( - "are.you.sure", - isPresented: $isPresentingDeleteMapTilesConfirm, - titleVisibility: .visible - ) { - Button("Delete all map tiles?", role: .destructive) { - tileManager.removeAll() - totalDownloadedTileSize = tileManager.getAllDownloadedSize() - print("delete all tiles") - } - } - ForEach(MapTileServerLinks.allCases, id: \.self) { tsl in - + if totalDownloadedTileSize != "0MB" { + Section(header: Text("Map Tile Data")) { Button { - tileManager.remove(for: tsl) - totalDownloadedTileSize = tileManager.getAllDownloadedSize() + isPresentingDeleteMapTilesConfirm = true } label: { - Label("Delete \(tsl.description) Tiles", systemImage: "trash") + Label("\("map.tiles.delete".localized) (\(totalDownloadedTileSize))", systemImage: "trash") .foregroundColor(.red) - .font(.footnote) + } + .confirmationDialog( + "are.you.sure", + isPresented: $isPresentingDeleteMapTilesConfirm, + titleVisibility: .visible + ) { + Button("Delete all map tiles?", role: .destructive) { + tileManager.removeAll() + totalDownloadedTileSize = tileManager.getAllDownloadedSize() + print("delete all tiles") + } + } + + ForEach(MapTileServer.allCases, id: \.self) { tsl in + + Button { + tileManager.remove(for: tsl) + totalDownloadedTileSize = tileManager.getAllDownloadedSize() + } label: { + Label("Delete \(tsl.description) Tiles", systemImage: "trash") + .foregroundColor(.red) + .font(.footnote) + } } } }