feat: Completes hvac-system-performance views and api call.
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -32,7 +32,7 @@ extension ApiController: DependencyKey {
|
|||||||
|
|
||||||
case let .calculateHVACSystemPerformance(request):
|
case let .calculateHVACSystemPerformance(request):
|
||||||
logger.debug("Calculating hvac system performance: \(request)")
|
logger.debug("Calculating hvac system performance: \(request)")
|
||||||
fatalError()
|
return try await request.respond(logger: logger)
|
||||||
|
|
||||||
case let .calculateMoldRisk(request):
|
case let .calculateMoldRisk(request):
|
||||||
logger.debug("Calculating mold risk: \(request)")
|
logger.debug("Calculating mold risk: \(request)")
|
||||||
|
|||||||
92
Sources/ApiController/Extensions/HVACSystemPerformance.swift
Normal file
92
Sources/ApiController/Extensions/HVACSystemPerformance.swift
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
import Dependencies
|
||||||
|
import Foundation
|
||||||
|
import Logging
|
||||||
|
import PsychrometricClient
|
||||||
|
import Routes
|
||||||
|
|
||||||
|
public extension HVACSystemPerformance.Request {
|
||||||
|
|
||||||
|
// TODO: Check if we should use psychrometrics for the request conditions instead
|
||||||
|
private static let airDensity = 0.075 // lb/ft^3 at standard conditions.
|
||||||
|
private static let specificHeat = 0.24 // BTU/lb at standard conditions (imperial).
|
||||||
|
|
||||||
|
func respond(logger: Logger) async throws -> HVACSystemPerformance.Response {
|
||||||
|
@Dependency(\.psychrometricClient) var psychrometricClient
|
||||||
|
|
||||||
|
try validate()
|
||||||
|
|
||||||
|
let altitude = parseAltitude()
|
||||||
|
let temperatureSplit = returnAirTemperature - supplyAirTemperature
|
||||||
|
|
||||||
|
let returnAirProperties = try await psychrometricClient.psychrometricProperties(.dryBulb(
|
||||||
|
.init(.init(returnAirTemperature)),
|
||||||
|
relativeHumidity: returnAirHumidity%,
|
||||||
|
altitude: altitude
|
||||||
|
))
|
||||||
|
|
||||||
|
let supplyAirProperties = try await psychrometricClient.psychrometricProperties(.dryBulb(
|
||||||
|
.init(.init(supplyAirTemperature)),
|
||||||
|
relativeHumidity: supplyAirHumidity%,
|
||||||
|
altitude: altitude
|
||||||
|
))
|
||||||
|
|
||||||
|
let airMassFlow = airflow * 60 * Self.airDensity
|
||||||
|
logger.debug("Air mass flow: \(airMassFlow)")
|
||||||
|
let condensationRate = airMassFlow * (
|
||||||
|
returnAirProperties.humidityRatio.value - supplyAirProperties.humidityRatio.value
|
||||||
|
) // lb/hr
|
||||||
|
let sensibleCapacity = airMassFlow * Self.specificHeat * temperatureSplit
|
||||||
|
logger.debug("Return enthalpy: \(returnAirProperties.enthalpy.value)")
|
||||||
|
logger.debug("Supply enthalpy: \(supplyAirProperties.enthalpy.value)")
|
||||||
|
let deltaEnthalpy = returnAirProperties.enthalpy.value - supplyAirProperties.enthalpy.value
|
||||||
|
logger.debug("Delta enthalpy: \(deltaEnthalpy)")
|
||||||
|
let totalCapacity = airMassFlow * deltaEnthalpy
|
||||||
|
|
||||||
|
let capacity = HVACSystemPerformance.Capacity(
|
||||||
|
total: totalCapacity,
|
||||||
|
sensible: sensibleCapacity,
|
||||||
|
latent: totalCapacity - sensibleCapacity
|
||||||
|
)
|
||||||
|
|
||||||
|
let systemMetrics = HVACSystemPerformance.SystemMetrics(
|
||||||
|
cfmPerTon: airflow / systemSize,
|
||||||
|
targetTemperatureSplit: (systemSize * 12000 * 0.75) / (1.08 * airflow),
|
||||||
|
actualTemperatureSplit: temperatureSplit,
|
||||||
|
condensationRatePoundsPerHour: condensationRate
|
||||||
|
)
|
||||||
|
|
||||||
|
return .init(
|
||||||
|
returnAirProperties: returnAirProperties,
|
||||||
|
supplyAirProperties: supplyAirProperties,
|
||||||
|
capacity: capacity,
|
||||||
|
systemMetrics: systemMetrics
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func parseAltitude() -> Length {
|
||||||
|
guard let altitude, altitude > 0 else { return .seaLevel }
|
||||||
|
return .init(altitude)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func validate() throws {
|
||||||
|
guard returnAirTemperature > 0 else {
|
||||||
|
throw ValidationError(message: "Return air temperature should be greater than 0.")
|
||||||
|
}
|
||||||
|
guard returnAirHumidity > 0 else {
|
||||||
|
throw ValidationError(message: "Return air humidity should be greater than 0.")
|
||||||
|
}
|
||||||
|
guard supplyAirTemperature > 0 else {
|
||||||
|
throw ValidationError(message: "Supply air temperature should be greater than 0.")
|
||||||
|
}
|
||||||
|
guard supplyAirHumidity > 0 else {
|
||||||
|
throw ValidationError(message: "Supply air humidity should be greater than 0.")
|
||||||
|
}
|
||||||
|
guard systemSize > 0 else {
|
||||||
|
throw ValidationError(message: "System size should be greater than 0.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ValidationError: Error {
|
||||||
|
let message: String
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import Dependencies
|
||||||
import PsychrometricClient
|
import PsychrometricClient
|
||||||
|
|
||||||
public enum HVACSystemPerformance {
|
public enum HVACSystemPerformance {
|
||||||
@@ -51,6 +52,7 @@ public enum HVACSystemPerformance {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Add delta-enthalpy.
|
||||||
public struct Capacity: Codable, Equatable, Sendable {
|
public struct Capacity: Codable, Equatable, Sendable {
|
||||||
|
|
||||||
public let total: Double
|
public let total: Double
|
||||||
@@ -70,18 +72,52 @@ public enum HVACSystemPerformance {
|
|||||||
public let cfmPerTon: Double
|
public let cfmPerTon: Double
|
||||||
public let targetTemperatureSplit: Double
|
public let targetTemperatureSplit: Double
|
||||||
public let actualTemperatureSplit: Double
|
public let actualTemperatureSplit: Double
|
||||||
public let condensationRate: Double
|
public let condensationRatePoundsPerHour: Double // lb/hr
|
||||||
|
public let condensationRateGallonsPerHour: Double // gal/hr
|
||||||
|
|
||||||
public init(
|
public init(
|
||||||
cfmPerTon: Double,
|
cfmPerTon: Double,
|
||||||
targetTemperatureSplit: Double,
|
targetTemperatureSplit: Double,
|
||||||
actualTemperatureSplit: Double,
|
actualTemperatureSplit: Double,
|
||||||
condensationRate: Double
|
condensationRatePoundsPerHour: Double
|
||||||
) {
|
) {
|
||||||
self.cfmPerTon = cfmPerTon
|
self.cfmPerTon = cfmPerTon
|
||||||
self.targetTemperatureSplit = targetTemperatureSplit
|
self.targetTemperatureSplit = targetTemperatureSplit
|
||||||
self.actualTemperatureSplit = actualTemperatureSplit
|
self.actualTemperatureSplit = actualTemperatureSplit
|
||||||
self.condensationRate = condensationRate
|
self.condensationRatePoundsPerHour = condensationRatePoundsPerHour
|
||||||
|
self.condensationRateGallonsPerHour = condensationRatePoundsPerHour * 0.12
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
|
||||||
|
public extension HVACSystemPerformance.Response {
|
||||||
|
|
||||||
|
static func mock() async throws -> Self {
|
||||||
|
@Dependency(\.psychrometricClient) var psychrometricClient
|
||||||
|
|
||||||
|
return try await .init(
|
||||||
|
returnAirProperties: psychrometricClient.psychrometricProperties(.dryBulb(75, relativeHumidity: 50%)),
|
||||||
|
supplyAirProperties: psychrometricClient.psychrometricProperties(.dryBulb(55, relativeHumidity: 87%)),
|
||||||
|
capacity: .mock,
|
||||||
|
systemMetrics: .mock
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public extension HVACSystemPerformance.Capacity {
|
||||||
|
static var mock: Self {
|
||||||
|
.init(total: 24000, sensible: 19000, latent: 5000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public extension HVACSystemPerformance.SystemMetrics {
|
||||||
|
static let mock = Self(
|
||||||
|
cfmPerTon: 400,
|
||||||
|
targetTemperatureSplit: 20.8,
|
||||||
|
actualTemperatureSplit: 18.7,
|
||||||
|
condensationRatePoundsPerHour: 16981.3
|
||||||
|
)
|
||||||
|
}
|
||||||
|
#endif
|
||||||
@@ -1,7 +1,5 @@
|
|||||||
import Elementary
|
import Elementary
|
||||||
|
|
||||||
// TODO: Experiment with removing text colors and move them further up the call stack.
|
|
||||||
|
|
||||||
/// A styled header for a form element, which consists of an
|
/// A styled header for a form element, which consists of an
|
||||||
/// svg image and label / name for the form.
|
/// svg image and label / name for the form.
|
||||||
///
|
///
|
||||||
@@ -20,7 +18,7 @@ public struct FormHeader: HTML, Sendable {
|
|||||||
|
|
||||||
public var content: some HTML {
|
public var content: some HTML {
|
||||||
LabeledContent {
|
LabeledContent {
|
||||||
h2(.class("text-2xl font-extrabold text-gray-600 dark:text-slate-200")) { label }
|
h2(.class("text-2xl font-extrabold")) { label }
|
||||||
} label: {
|
} label: {
|
||||||
SVG(svg, color: .blue)
|
SVG(svg, color: .blue)
|
||||||
}
|
}
|
||||||
@@ -52,8 +50,8 @@ public struct Input: HTML, Sendable {
|
|||||||
input(
|
input(
|
||||||
.id(id), .placeholder(placeholder), .name(name ?? id),
|
.id(id), .placeholder(placeholder), .name(name ?? id),
|
||||||
.class("""
|
.class("""
|
||||||
w-full px-4 py-2 border border-gray-300 dark:border-gray-400 rounded-md focus:ring-2
|
w-full px-4 py-2 border border-gray-300 dark:border-gray-400 rounded-md
|
||||||
focus:ring-yellow-800 focus:border-yellow-800 text-blue-600 dark:text-gray-300
|
focus:ring-2 focus:ring-yellow-800 focus:border-yellow-800
|
||||||
""")
|
""")
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -76,7 +74,7 @@ public struct InputLabel<InputLabel: HTML>: HTML {
|
|||||||
public var content: some HTML<HTMLTag.label> {
|
public var content: some HTML<HTMLTag.label> {
|
||||||
label(
|
label(
|
||||||
.for(forInputId),
|
.for(forInputId),
|
||||||
.class("block text-sm font-medium text-blue-500 dark:text-gray-300 mb-2")
|
.class("block text-sm font-medium mb-2")
|
||||||
) {
|
) {
|
||||||
self.inputLabel
|
self.inputLabel
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,10 @@ private let numberFormatter: NumberFormatter = {
|
|||||||
}()
|
}()
|
||||||
|
|
||||||
extension String.StringInterpolation {
|
extension String.StringInterpolation {
|
||||||
mutating func appendInterpolation(double: Double) {
|
mutating func appendInterpolation(double: Double, fractionDigits: Int = 2) {
|
||||||
|
let formatter = NumberFormatter()
|
||||||
|
formatter.numberStyle = .decimal
|
||||||
|
formatter.maximumFractionDigits = fractionDigits
|
||||||
appendInterpolation(numberFormatter.string(from: NSNumber(value: double))!)
|
appendInterpolation(numberFormatter.string(from: NSNumber(value: double))!)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -82,8 +82,9 @@ extension ViewController: DependencyKey {
|
|||||||
switch route {
|
switch route {
|
||||||
case .index:
|
case .index:
|
||||||
return request.respond(HVACSystemPerformanceForm())
|
return request.respond(HVACSystemPerformanceForm())
|
||||||
case .submit:
|
case let .submit(hvacRequest):
|
||||||
return HVACSystemPerformanceResult()
|
let response = try await hvacRequest.respond(logger: request.logger)
|
||||||
|
return HVACSystemPerformanceResult(response: response)
|
||||||
}
|
}
|
||||||
|
|
||||||
case let .moldRisk(route):
|
case let .moldRisk(route):
|
||||||
|
|||||||
@@ -1,10 +1,17 @@
|
|||||||
import Elementary
|
import Elementary
|
||||||
import ElementaryHTMX
|
import ElementaryHTMX
|
||||||
|
import PsychrometricClient
|
||||||
import Routes
|
import Routes
|
||||||
import Styleguide
|
import Styleguide
|
||||||
|
|
||||||
struct HVACSystemPerformanceForm: HTML, Sendable {
|
struct HVACSystemPerformanceForm: HTML, Sendable {
|
||||||
|
|
||||||
|
let response: HVACSystemPerformance.Response?
|
||||||
|
|
||||||
|
init(response: HVACSystemPerformance.Response? = nil) {
|
||||||
|
self.response = response
|
||||||
|
}
|
||||||
|
|
||||||
var content: some HTML {
|
var content: some HTML {
|
||||||
FormHeader(label: "HVAC System Performance", svg: .thermometerSun)
|
FormHeader(label: "HVAC System Performance", svg: .thermometerSun)
|
||||||
form(
|
form(
|
||||||
@@ -23,12 +30,12 @@ struct HVACSystemPerformanceForm: HTML, Sendable {
|
|||||||
.attributes(.type(.number), .min("1"), .step("0.5"), .required)
|
.attributes(.type(.number), .min("1"), .step("0.5"), .required)
|
||||||
}
|
}
|
||||||
LabeledContent(label: "Altitude (ft.)") {
|
LabeledContent(label: "Altitude (ft.)") {
|
||||||
Input(id: "altitude", placeholder: "Altitude (Optional)")
|
Input(id: "altitude", placeholder: "Project altitude (Optional)")
|
||||||
.attributes(.type(.number), .min("1"), .step("0.5"))
|
.attributes(.type(.number), .min("1"), .step("0.5"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
div(.class("grid grid-cols-1 md:grid-cols-2 gap-4")) {
|
div(.class("grid grid-cols-1 md:grid-cols-2 gap-8")) {
|
||||||
div(.class("space-y-4")) {
|
div(.class("space-y-4")) {
|
||||||
h3(.class("text-lg font-medium")) { "Return Air" }
|
h3(.class("text-lg font-medium")) { "Return Air" }
|
||||||
LabeledContent(label: "Dry Bulb (°F)") {
|
LabeledContent(label: "Dry Bulb (°F)") {
|
||||||
@@ -55,8 +62,143 @@ struct HVACSystemPerformanceForm: HTML, Sendable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
div(.id("result")) {}
|
div(.id("result")) {
|
||||||
|
if let response {
|
||||||
|
HVACSystemPerformanceResult(response: response)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct HVACSystemPerformanceResult: HTML, Sendable {}
|
struct HVACSystemPerformanceResult: HTML, Sendable {
|
||||||
|
let response: HVACSystemPerformance.Response
|
||||||
|
|
||||||
|
var content: some HTML {
|
||||||
|
ResultContainer(reset: .hvacSystemPerformance(.index)) {
|
||||||
|
// ResultContainer {
|
||||||
|
// Capacities
|
||||||
|
div(.class("grid grid-cols-1 lg:grid-cols-3 gap-4")) {
|
||||||
|
CapacityContainer(title: "Total Capacity", capacity: response.capacity.total, shr: nil)
|
||||||
|
.attributes(.class("text-blue-600 bg-blue-100 border border-blue-600 p-6"))
|
||||||
|
|
||||||
|
CapacityContainer(title: "Sensible Capacity", capacity: response.capacity.sensible, shr: response.capacity.shr)
|
||||||
|
.attributes(.class("text-green-600 bg-green-100 border border-green-600 p-6"))
|
||||||
|
|
||||||
|
CapacityContainer(title: "Latent Capacity", capacity: response.capacity.latent, shr: nil)
|
||||||
|
.attributes(.class("text-purple-600 bg-purple-100 border border-purple-600 p-6"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// System Metrics
|
||||||
|
SystemMetricsView(metrics: response.systemMetrics, shr: response.capacity.shr)
|
||||||
|
.attributes(.class("mt-8 border rounded-xl shadow-lg"))
|
||||||
|
|
||||||
|
// Psychrometric properties.
|
||||||
|
div(.class("grid grid-cols-1 md:grid-cols-2 gap-6 mt-8")) {
|
||||||
|
PsychrometricPropertiesGrid(title: "Return Air Properties", properties: response.returnAirProperties)
|
||||||
|
PsychrometricPropertiesGrid(title: "Supply Air Properties", properties: response.supplyAirProperties)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// header: {
|
||||||
|
// p { "Fix me." }
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct CapacityContainer: HTML, Sendable {
|
||||||
|
let title: String
|
||||||
|
let capacity: Double
|
||||||
|
let shr: Double?
|
||||||
|
|
||||||
|
var tons: Double { capacity / 12000 }
|
||||||
|
|
||||||
|
var content: some HTML<HTMLTag.div> {
|
||||||
|
div(.class("rounded-xl")) {
|
||||||
|
h4(.class("text-lg font-semibold mb-2")) { title }
|
||||||
|
div(.class("text-3xl font-bold")) {
|
||||||
|
"\(double: tons, fractionDigits: 1)"
|
||||||
|
span(.class("text-xl ps-2")) { "Tons" }
|
||||||
|
}
|
||||||
|
div(.class("text-sm mt-1")) {
|
||||||
|
"\(double: capacity, fractionDigits: 1) BTU/h"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct SystemMetricsView: HTML, Sendable {
|
||||||
|
let metrics: HVACSystemPerformance.SystemMetrics
|
||||||
|
let shr: Double
|
||||||
|
|
||||||
|
var content: some HTML<HTMLTag.div> {
|
||||||
|
div {
|
||||||
|
h4(.class("""
|
||||||
|
text-lg font-semibold flex justify-center py-2 mb-4 text-blue-600 dark:text-slate-300
|
||||||
|
""")) { "System Performance Metrics" }
|
||||||
|
|
||||||
|
// grid grid-cols-1 md:grid-cols-2 gap-6
|
||||||
|
div(.class("flex justify-between items-center p-4")) {
|
||||||
|
div(.class("space-y-3")) {
|
||||||
|
LabeledMetric(label: "Airflow per Ton", value: metrics.cfmPerTon, units: "CFM/ton")
|
||||||
|
.attributes(.class("mb-8"))
|
||||||
|
|
||||||
|
div {
|
||||||
|
LabeledMetric(label: "Temperature Split", value: metrics.actualTemperatureSplit, units: "°F")
|
||||||
|
p(.class("text-xs")) {
|
||||||
|
"Target: \(double: metrics.targetTemperatureSplit) °F"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
div(.class("space-y-3")) {
|
||||||
|
LabeledMetric(label: "Moisture Removal", value: metrics.condensationRateGallonsPerHour, units: "gal/h")
|
||||||
|
.attributes(.class("mb-8"))
|
||||||
|
LabeledMetric(label: "Sensible Heat Ratio", value: shr, units: "%")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct LabeledMetric: HTML, Sendable {
|
||||||
|
let label: String
|
||||||
|
let value: Double
|
||||||
|
let units: String
|
||||||
|
let fractionDigits: Int
|
||||||
|
|
||||||
|
init(
|
||||||
|
label: String,
|
||||||
|
value: Double,
|
||||||
|
units: String,
|
||||||
|
fractionDigits: Int = 1
|
||||||
|
) {
|
||||||
|
self.label = label
|
||||||
|
self.value = value
|
||||||
|
self.units = units
|
||||||
|
self.fractionDigits = fractionDigits
|
||||||
|
}
|
||||||
|
|
||||||
|
var content: some HTML<HTMLTag.div> {
|
||||||
|
div {
|
||||||
|
span(.class("text-sm")) { label }
|
||||||
|
p(.class("text-2xl font-semibold")) {
|
||||||
|
"\(double: value, fractionDigits: fractionDigits)"
|
||||||
|
span(.class("text-base font-normal ps-2")) { units }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct PsychrometricPropertiesGrid: HTML, Sendable {
|
||||||
|
let title: String
|
||||||
|
let properties: PsychrometricProperties
|
||||||
|
|
||||||
|
var content: some HTML<HTMLTag.div> {
|
||||||
|
div(.class("rounded-xl border border-blue-600 dark:border-gray-400 bg-blue-100 dark:bg-slate-700 p-4")) {
|
||||||
|
h4(.class("""
|
||||||
|
text-lg font-semibold flex justify-center py-2 mb-4 text-blue-600 dark:text-slate-300
|
||||||
|
""")) { title }
|
||||||
|
PsychrometricPropertiesView(properties: properties)
|
||||||
|
.attributes(.class("grid grid-cols-1 gap-4"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -130,6 +130,7 @@ private extension MoldRisk.RiskLevel {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Remove and use PsychrometricPropertiesView as the base.
|
||||||
struct PsychrometricPropertiesGrid: HTML {
|
struct PsychrometricPropertiesGrid: HTML {
|
||||||
let properties: PsychrometricProperties
|
let properties: PsychrometricProperties
|
||||||
|
|
||||||
@@ -170,5 +171,4 @@ struct PsychrometricPropertiesGrid: HTML {
|
|||||||
let property = properties[keyPath: keyPath]
|
let property = properties[keyPath: keyPath]
|
||||||
return displayProperty(label, property.rawValue, property.units.rawValue)
|
return displayProperty(label, property.rawValue, property.units.rawValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
39
Sources/ViewController/Views/PsychrometricProperties.swift
Normal file
39
Sources/ViewController/Views/PsychrometricProperties.swift
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import Elementary
|
||||||
|
import PsychrometricClient
|
||||||
|
|
||||||
|
struct PsychrometricPropertiesView: HTML {
|
||||||
|
let properties: PsychrometricProperties
|
||||||
|
|
||||||
|
var content: some HTML<HTMLTag.div> {
|
||||||
|
div(.class("w-full")) {
|
||||||
|
displayProperty("Dew Point", \.dewPoint.rawValue)
|
||||||
|
displayProperty("Wet Bulb", \.wetBulb.rawValue)
|
||||||
|
displayProperty("Enthalpy", \.enthalpy.rawValue)
|
||||||
|
displayProperty("Density", \.density.rawValue)
|
||||||
|
displayProperty("Vapor Pressure", \.vaporPressure.rawValue)
|
||||||
|
displayProperty("Specific Volume", properties.specificVolume.rawValue)
|
||||||
|
displayProperty("Absolute Humidity", \.absoluteHumidity)
|
||||||
|
displayProperty("Humidity Ratio", properties.humidityRatio.value)
|
||||||
|
displayProperty("Degree of Saturation", properties.degreeOfSaturation.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func displayProperty(_ label: String, _ value: Double, _ symbol: String? = nil) -> some HTML {
|
||||||
|
let symbol = "\(symbol == nil ? "" : " \(symbol!)")"
|
||||||
|
|
||||||
|
return div(.class("flex items-center justify-between text-blue-500 dark:text-slate-200")) {
|
||||||
|
span(.class("font-semibold")) { "\(label): " }
|
||||||
|
span(.class("font-light")) {
|
||||||
|
"\(double: value)\(symbol)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func displayProperty<N: NumberWithUnitOfMeasure>(
|
||||||
|
_ label: String,
|
||||||
|
_ keyPath: KeyPath<PsychrometricProperties, N>
|
||||||
|
) -> some HTML where N.Number == Double, N.Units: RawRepresentable, N.Units.RawValue == String {
|
||||||
|
let property = properties[keyPath: keyPath]
|
||||||
|
return displayProperty(label, property.rawValue, property.units.rawValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user