Weather layer

This commit is contained in:
Garth Vander Houwen 2023-05-14 00:16:55 -07:00
parent ba3ef4af3e
commit 0c852a5202
6 changed files with 212 additions and 66 deletions

View file

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

View file

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

View file

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

View file

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

View file

@ -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<PositionEntity>
@FetchRequest(sortDescriptors: [NSSortDescriptor(key: "name", ascending: false)],
predicate: NSPredicate(
format: "expire == nil || expire >= %@", Date() as NSDate
), animation: .none)
private var waypoints: FetchedResults<WaypointEntity>
@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)
}
}

View file

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