mirror of
https://github.com/meshtastic/Meshtastic-Apple.git
synced 2026-04-20 22:13:56 +00:00
Weather layer
This commit is contained in:
parent
ba3ef4af3e
commit
0c852a5202
6 changed files with 212 additions and 66 deletions
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue