feat: Completes hvac-system-performance views and api call.
This commit is contained in:
@@ -8,7 +8,10 @@ private let numberFormatter: NumberFormatter = {
|
||||
}()
|
||||
|
||||
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))!)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,8 +82,9 @@ extension ViewController: DependencyKey {
|
||||
switch route {
|
||||
case .index:
|
||||
return request.respond(HVACSystemPerformanceForm())
|
||||
case .submit:
|
||||
return HVACSystemPerformanceResult()
|
||||
case let .submit(hvacRequest):
|
||||
let response = try await hvacRequest.respond(logger: request.logger)
|
||||
return HVACSystemPerformanceResult(response: response)
|
||||
}
|
||||
|
||||
case let .moldRisk(route):
|
||||
|
||||
@@ -1,10 +1,17 @@
|
||||
import Elementary
|
||||
import ElementaryHTMX
|
||||
import PsychrometricClient
|
||||
import Routes
|
||||
import Styleguide
|
||||
|
||||
struct HVACSystemPerformanceForm: HTML, Sendable {
|
||||
|
||||
let response: HVACSystemPerformance.Response?
|
||||
|
||||
init(response: HVACSystemPerformance.Response? = nil) {
|
||||
self.response = response
|
||||
}
|
||||
|
||||
var content: some HTML {
|
||||
FormHeader(label: "HVAC System Performance", svg: .thermometerSun)
|
||||
form(
|
||||
@@ -23,12 +30,12 @@ struct HVACSystemPerformanceForm: HTML, Sendable {
|
||||
.attributes(.type(.number), .min("1"), .step("0.5"), .required)
|
||||
}
|
||||
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"))
|
||||
}
|
||||
}
|
||||
|
||||
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")) {
|
||||
h3(.class("text-lg font-medium")) { "Return Air" }
|
||||
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 {
|
||||
let properties: PsychrometricProperties
|
||||
|
||||
@@ -170,5 +171,4 @@ struct PsychrometricPropertiesGrid: HTML {
|
||||
let property = properties[keyPath: keyPath]
|
||||
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