diff --git a/Localizable.xcstrings b/Localizable.xcstrings index ca088018..d9b1d317 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -1086,6 +1086,9 @@ } } } + }, + "%f%%" : { + }, "%lf" : { "localizations" : { diff --git a/Meshtastic/Model/CoreData/TelemetryEntity+CoreDataClass.swift b/Meshtastic/Model/CoreData/TelemetryEntity+CoreDataClass.swift index 48f8263b..79dd0485 100644 --- a/Meshtastic/Model/CoreData/TelemetryEntity+CoreDataClass.swift +++ b/Meshtastic/Model/CoreData/TelemetryEntity+CoreDataClass.swift @@ -52,4 +52,10 @@ public class TelemetryEntity: NSManagedObject, Identifiable { @ManagedAttribute(attributeName: "soilTemperature") public var soilTemperature: Float? @ManagedAttribute(attributeName: "soilMoisture") public var soilMoisture: UInt32? + public var dewPoint: Float? { + guard let temp = self.temperature, let rh = self.relativeHumidity else { + return nil + } + return Float(calculateDewPoint(temp: temp, relativeHumidity: rh, convertToLocale: false)) + } } diff --git a/Meshtastic/Model/Metrics Visualization/MetricsChartSeries.swift b/Meshtastic/Model/Metrics Visualization/MetricsChartSeries.swift index 42e35ce0..2a11bd7e 100644 --- a/Meshtastic/Model/Metrics Visualization/MetricsChartSeries.swift +++ b/Meshtastic/Model/Metrics Visualization/MetricsChartSeries.swift @@ -30,6 +30,9 @@ class MetricsChartSeries: ObservableObject { // A closure that will provide the foreground style given the data set and overall chart range let foregroundStyle: (ClosedRange?) -> AnyShapeStyle? + // For drawing the lines in the chart + let strokeStyle: StrokeStyle + // A closure that will provide the Chart Content for this series let chartBodyClosure: (MetricsChartSeries, ClosedRange?, TelemetryEntity) -> AnyChartContent? // Closure to render the chart @@ -51,6 +54,7 @@ class MetricsChartSeries: ObservableObject { minumumYAxisSpan: Float? = nil, conversion: ((Value) -> Value)? = nil, visible: Bool = true, + strokeStyle: StrokeStyle = StrokeStyle(lineWidth: 4), foregroundStyle: @escaping ((ClosedRange?) -> ForegroundStyle?) = { _ in nil }, @ChartContentBuilder chartBody: @escaping (MetricsChartSeries, ClosedRange?, Date, Value) -> ChartBody? ) { @@ -62,7 +66,8 @@ class MetricsChartSeries: ObservableObject { self.initialYAxisRange = initialYAxisRange self.minumumYAxisSpan = minumumYAxisSpan self.visible = visible - + self.strokeStyle = strokeStyle + // By saving these closures, MetricsChartSeries can be type agnostic // This is a less elegant form of type erasure, but doesn't require a new Any-type self.foregroundStyle = { range in foregroundStyle(range).map({ AnyShapeStyle($0) }) } diff --git a/Meshtastic/Views/Helpers/Weather/LocalWeatherConditions.swift b/Meshtastic/Views/Helpers/Weather/LocalWeatherConditions.swift index 6682f18c..f8ba74a8 100644 --- a/Meshtastic/Views/Helpers/Weather/LocalWeatherConditions.swift +++ b/Meshtastic/Views/Helpers/Weather/LocalWeatherConditions.swift @@ -90,12 +90,18 @@ struct LocalWeatherConditions: View { } /// Magnus Formula -func calculateDewPoint(temp: Float, relativeHumidity: Float) -> Double { +func calculateDewPoint(temp: Float, relativeHumidity: Float, convertToLocale: Bool = true) -> Double { let a: Float = 17.27 let b: Float = 237.7 let alpha = ((a * temp) / (b + temp)) + log(relativeHumidity / 100.0) let dewPoint = (b * alpha) / (a - alpha) let dewPointUnit = Measurement(value: Double(dewPoint), unit: .celsius) + + if !convertToLocale { + return Double(dewPoint) + } + + // Otherwise convert to locale units, default behavior let locale = NSLocale.current as NSLocale let localeUnit = locale.object(forKey: NSLocale.Key(rawValue: "kCFLocaleTemperatureUnitKey")) var format: UnitTemperature = .celsius diff --git a/Meshtastic/Views/Nodes/Helpers/Metrics Columns/EnvironmentDefaultColumns.swift b/Meshtastic/Views/Nodes/Helpers/Metrics Columns/EnvironmentDefaultColumns.swift index 0f14331d..d22b5ffb 100644 --- a/Meshtastic/Views/Nodes/Helpers/Metrics Columns/EnvironmentDefaultColumns.swift +++ b/Meshtastic/Views/Nodes/Helpers/Metrics Columns/EnvironmentDefaultColumns.swift @@ -39,6 +39,19 @@ extension MetricsColumnList { } ?? Text(verbatim: Constants.nilValueIndicator) }), + // Relative Humidity Series Configuration + MetricsTableColumn( + id: "dewPoint", + keyPath: \.dewPoint, + name: "Dew Point", + abbreviatedName: "Dew", + minWidth: 30, maxWidth: 45, + tableBody: { _, dewPoint in + dewPoint.map { + Text("\($0.formattedTemperature())") + } ?? Text(verbatim: Constants.nilValueIndicator) + }), + // Barometric Pressure Series Configuration MetricsTableColumn( id: "barometricPressure", diff --git a/Meshtastic/Views/Nodes/Helpers/Metrics Columns/EnvironmentDefaultSeries.swift b/Meshtastic/Views/Nodes/Helpers/Metrics Columns/EnvironmentDefaultSeries.swift index b2590b51..760ec46b 100644 --- a/Meshtastic/Views/Nodes/Helpers/Metrics Columns/EnvironmentDefaultSeries.swift +++ b/Meshtastic/Views/Nodes/Helpers/Metrics Columns/EnvironmentDefaultSeries.swift @@ -50,7 +50,7 @@ extension MetricsSeriesList { ) .interpolationMethod(.catmullRom) .foregroundStyle(by: .value("Series", series.abbreviatedName)) - .lineStyle(StrokeStyle(lineWidth: 4)) + .lineStyle(series.strokeStyle) .alignsMarkStylesWithPlotArea() } }), @@ -76,11 +76,42 @@ extension MetricsSeriesList { ) .interpolationMethod(.catmullRom) .foregroundStyle(by: .value("Series", series.abbreviatedName)) - .lineStyle(StrokeStyle(lineWidth: 4)) + .lineStyle(series.strokeStyle) .alignsMarkStylesWithPlotArea() } }), + MetricsChartSeries( + id: "dewPoint", + keyPath: \.dewPoint, + name: "Dew Point", + abbreviatedName: "Dew", + minumumYAxisSpan: 50.0, + conversion: { t in t.map { Float($0.localeTemperature()) } }, + strokeStyle: StrokeStyle(lineWidth: 4, dash: [2, 2]), + foregroundStyle: { chartRange in + let locale = NSLocale.current as NSLocale + let localeUnit = locale.object(forKey: NSLocale.Key(rawValue: "kCFLocaleTemperatureUnitKey")) + let format: UnitTemperature = localeUnit as? String ?? "Celsius" == "Fahrenheit" ? .fahrenheit : .celsius + let lowerBound = chartRange.map { Double($0.lowerBound) } ?? 0.0 + let upperBound = chartRange.map { Double($0.upperBound) } ?? 100.0 + let stops: [Gradient.Stop] = generateStops(minTemp: lowerBound, maxTemp: upperBound, tempUnit: format, opacity: 1.0) + return LinearGradient(stops: stops, startPoint: .bottom, endPoint: .top) + }, + chartBody: { series, chartRange, time, dewPoint in + if let dewPoint { + LineMark( + x: .value("Time", time), + y: .value( + series.abbreviatedName, dewPoint.localeTemperature()) + ) + .interpolationMethod(.catmullRom) + .foregroundStyle(by: .value("Series", series.abbreviatedName)) + .lineStyle(series.strokeStyle) + .alignsMarkStylesWithPlotArea() + } + }), + // Barometric Pressure Series Configuration MetricsChartSeries( id: "barometricPressure", @@ -102,7 +133,7 @@ extension MetricsSeriesList { ) .interpolationMethod(.catmullRom) .foregroundStyle(by: .value("Series", series.abbreviatedName)) - .lineStyle(StrokeStyle(lineWidth: 4)) + .lineStyle(series.strokeStyle) .alignsMarkStylesWithPlotArea() } }), @@ -130,7 +161,7 @@ extension MetricsSeriesList { ) .interpolationMethod(.catmullRom) .foregroundStyle(by: .value("Series", series.abbreviatedName)) - .lineStyle(StrokeStyle(lineWidth: 4)) + .lineStyle(series.strokeStyle) .alignsMarkStylesWithPlotArea() } }), @@ -156,7 +187,7 @@ extension MetricsSeriesList { ) .interpolationMethod(.catmullRom) .foregroundStyle(by: .value("Series", series.abbreviatedName)) - .lineStyle(StrokeStyle(lineWidth: 4)) + .lineStyle(series.strokeStyle) .alignsMarkStylesWithPlotArea() } }), @@ -182,7 +213,7 @@ extension MetricsSeriesList { ) .interpolationMethod(.catmullRom) .foregroundStyle(by: .value("Series", series.abbreviatedName)) - .lineStyle(StrokeStyle(lineWidth: 4)) + .lineStyle(series.strokeStyle) .alignsMarkStylesWithPlotArea() } }), @@ -208,7 +239,7 @@ extension MetricsSeriesList { ) .interpolationMethod(.catmullRom) .foregroundStyle(by: .value("Series", series.abbreviatedName)) - .lineStyle(StrokeStyle(lineWidth: 4)) + .lineStyle(series.strokeStyle) .alignsMarkStylesWithPlotArea() } }), @@ -234,7 +265,7 @@ extension MetricsSeriesList { ) .interpolationMethod(.catmullRom) .foregroundStyle(by: .value("Series", series.abbreviatedName)) - .lineStyle(StrokeStyle(lineWidth: 4)) + .lineStyle(series.strokeStyle) .alignsMarkStylesWithPlotArea() } }), @@ -261,7 +292,7 @@ extension MetricsSeriesList { ) .interpolationMethod(.catmullRom) .foregroundStyle(by: .value("Series", series.abbreviatedName)) - .lineStyle(StrokeStyle(lineWidth: 4)) + .lineStyle(series.strokeStyle) .alignsMarkStylesWithPlotArea() } }), @@ -288,7 +319,7 @@ extension MetricsSeriesList { ) .interpolationMethod(.catmullRom) .foregroundStyle(by: .value("Series", series.abbreviatedName)) - .lineStyle(StrokeStyle(lineWidth: 4)) + .lineStyle(series.strokeStyle) .alignsMarkStylesWithPlotArea() PointMark( x: .value("Time", time), @@ -327,7 +358,7 @@ extension MetricsSeriesList { ) .interpolationMethod(.catmullRom) .foregroundStyle(by: .value("Series", series.abbreviatedName)) - .lineStyle(StrokeStyle(lineWidth: 4)) + .lineStyle(series.strokeStyle) .alignsMarkStylesWithPlotArea() } }), @@ -353,7 +384,7 @@ extension MetricsSeriesList { ) .interpolationMethod(.catmullRom) .foregroundStyle(by: .value("Series", series.abbreviatedName)) - .lineStyle(StrokeStyle(lineWidth: 4)) + .lineStyle(series.strokeStyle) .alignsMarkStylesWithPlotArea() } }), @@ -379,7 +410,7 @@ extension MetricsSeriesList { ) .interpolationMethod(.catmullRom) .foregroundStyle(by: .value("Series", series.abbreviatedName)) - .lineStyle(StrokeStyle(lineWidth: 4)) + .lineStyle(series.strokeStyle) .alignsMarkStylesWithPlotArea() } }), @@ -405,7 +436,7 @@ extension MetricsSeriesList { ) .interpolationMethod(.catmullRom) .foregroundStyle(by: .value("Series", series.abbreviatedName)) - .lineStyle(StrokeStyle(lineWidth: 4)) + .lineStyle(series.strokeStyle) .alignsMarkStylesWithPlotArea() } }), @@ -431,7 +462,7 @@ extension MetricsSeriesList { ) .interpolationMethod(.catmullRom) .foregroundStyle(by: .value("Series", series.abbreviatedName)) - .lineStyle(StrokeStyle(lineWidth: 4)) + .lineStyle(series.strokeStyle) .alignsMarkStylesWithPlotArea() } }), @@ -457,7 +488,7 @@ extension MetricsSeriesList { ) .interpolationMethod(.catmullRom) .foregroundStyle(by: .value("Series", series.abbreviatedName)) - .lineStyle(StrokeStyle(lineWidth: 4)) + .lineStyle(series.strokeStyle) .alignsMarkStylesWithPlotArea() } }) diff --git a/Meshtastic/Views/Nodes/Helpers/Metrics Columns/MetricsColumnDetail.swift b/Meshtastic/Views/Nodes/Helpers/Metrics Columns/MetricsColumnDetail.swift index 24cc6f96..dbe3303e 100644 --- a/Meshtastic/Views/Nodes/Helpers/Metrics Columns/MetricsColumnDetail.swift +++ b/Meshtastic/Views/Nodes/Helpers/Metrics Columns/MetricsColumnDetail.swift @@ -21,9 +21,14 @@ struct MetricsColumnDetail: View { Section("Chart") { ForEach(seriesList) { series in HStack { - Circle() - .fill(series.foregroundStyle(0.0...100.0) ?? AnyShapeStyle(.clear)) - .frame(width: 20.0, height: 20.0) + Path { path in + path.move(to: CGPoint(x: 10, y: 0)) + path.addLine(to: CGPoint(x: 10, y: 20)) + } + .stroke(series.foregroundStyle(0.0...100.0) ?? AnyShapeStyle(.clear), + style: series.strokeStyle) + .frame(width: 20.0, height: 20.0) + .rotationEffect(.degrees(90.0)) Text(series.name) Spacer() if series.visible {