feat: Adds attic ventilation calculation.
This commit is contained in:
@@ -1,17 +0,0 @@
|
||||
import Foundation
|
||||
|
||||
private let numberFormatter: NumberFormatter = {
|
||||
let formatter = NumberFormatter()
|
||||
formatter.numberStyle = .decimal
|
||||
formatter.maximumFractionDigits = 2
|
||||
return formatter
|
||||
}()
|
||||
|
||||
extension String.StringInterpolation {
|
||||
mutating func appendInterpolation(double: Double, fractionDigits: Int = 2) {
|
||||
let formatter = NumberFormatter()
|
||||
formatter.numberStyle = .decimal
|
||||
formatter.maximumFractionDigits = fractionDigits
|
||||
appendInterpolation(numberFormatter.string(from: NSNumber(value: double))!)
|
||||
}
|
||||
}
|
||||
@@ -76,6 +76,15 @@ extension ViewController: DependencyKey {
|
||||
}
|
||||
}
|
||||
|
||||
case let .atticVentilation(route):
|
||||
switch route {
|
||||
case .index:
|
||||
return request.respond(AtticVentilationForm(response: nil))
|
||||
case let .submit(request):
|
||||
let response = try await request.respond(logger: logger)
|
||||
return AtticVentilationResponse(response: response)
|
||||
}
|
||||
|
||||
case let .capacitor(route):
|
||||
switch route {
|
||||
case let .index(mode: mode):
|
||||
|
||||
@@ -67,6 +67,7 @@ private struct Header: HTML {
|
||||
navLink(label: "HVAC-System-Performance", route: .hvacSystemPerformance(.index))
|
||||
navLink(label: "Room-Pressure", route: .roomPressure(.index))
|
||||
navLink(label: "Capcitor-Calculator", route: .capacitor(.index))
|
||||
navLink(label: "Attic-Ventilation", route: .atticVentilation(.index))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
182
Sources/ViewController/Views/AtticVentilation.swift
Normal file
182
Sources/ViewController/Views/AtticVentilation.swift
Normal file
@@ -0,0 +1,182 @@
|
||||
import Elementary
|
||||
import ElementaryHTMX
|
||||
import Routes
|
||||
import Styleguide
|
||||
|
||||
struct AtticVentilationForm: HTML, Sendable {
|
||||
let response: AtticVentilation.Response?
|
||||
|
||||
var content: some HTML {
|
||||
FormHeader(label: "Attic Ventilation Calculator", svg: .wind)
|
||||
|
||||
// Form.
|
||||
form(
|
||||
.hx.post(route: .atticVentilation(.index)),
|
||||
.hx.target("#result")
|
||||
) {
|
||||
div(.class("space-y-6")) {
|
||||
div {
|
||||
LabeledContent(label: "Pressure Differential: (Pascals - WRT outside)") {
|
||||
Input(id: "pressureDifferential", placeholder: "Pressure differential")
|
||||
.attributes(.type(.number), .step("0.1"), .autofocus, .required)
|
||||
}
|
||||
p(.class("text-sm text-light mt-2")) { "Positive values indicate higher attic pressure" }
|
||||
}
|
||||
|
||||
div(.class("grid grid-cols-1 md:grid-cols-2 gap-4")) {
|
||||
div {
|
||||
p(.class("font-bold mb-6")) { "Outdoor Conditions" }
|
||||
LabeledContent(label: "Temperature: (°F)") {
|
||||
Input(id: "outdoorTemperature", placeholder: "Outdoor temperature")
|
||||
.attributes(.class("mb-4"), .type(.number), .step("0.1"), .required)
|
||||
}
|
||||
LabeledContent(label: "Dew Point: (°F)") {
|
||||
Input(id: "outdoorDewpoint", placeholder: "Outdoor dewpoint")
|
||||
.attributes(.type(.number), .step("0.1"), .required)
|
||||
}
|
||||
}
|
||||
div {
|
||||
p(.class("font-bold mb-6")) { "Attic Conditions" }
|
||||
LabeledContent(label: "Temperature: (°F)") {
|
||||
Input(id: "atticTemperature", placeholder: "Attic temperature")
|
||||
.attributes(.class("mb-4"), .type(.number), .step("0.1"), .required)
|
||||
}
|
||||
LabeledContent(label: "Dew Point: (°F)") {
|
||||
Input(id: "atticDewpoint", placeholder: "Attic dewpoint")
|
||||
.attributes(.type(.number), .step("0.1"), .required)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LabeledContent(label: "Attic Floor Area: (ft²)") {
|
||||
Input(id: "atticFloorArea", placeholder: "Attic floor area")
|
||||
.attributes(.type(.number), .step("0.1"), .min("0.1"), .required)
|
||||
}
|
||||
|
||||
div(.class("grid grid-cols-1 md:grid-cols-2 gap-4")) {
|
||||
LabeledContent(label: "Existing Intake Area: (ft²)") {
|
||||
Input(id: "existingIntakeArea", placeholder: "Intake area (optional)")
|
||||
.attributes(.class("mb-4"), .type(.number), .step("0.1"), .min("0.1"), .required)
|
||||
}
|
||||
LabeledContent(label: "Existing Exhaust Area: (ft²)") {
|
||||
Input(id: "existingExhaustArea", placeholder: "Exhaust area (optional)")
|
||||
.attributes(.type(.number), .step("0.1"), .min("0.1"), .required)
|
||||
}
|
||||
}
|
||||
|
||||
div {
|
||||
SubmitButton(label: "Calculate Ventilation")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
div(.id("result")) {
|
||||
if let response {
|
||||
AtticVentilationResponse(response: response)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct AtticVentilationResponse: HTML, Sendable {
|
||||
let response: AtticVentilation.Response
|
||||
|
||||
var content: some HTML {
|
||||
ResultContainer(reset: .atticVentilation(.index)) {
|
||||
div(.class("space-y-6")) {
|
||||
statusView()
|
||||
requiredVentilationView()
|
||||
WarningBox(warnings: response.warnings)
|
||||
Note {
|
||||
"""
|
||||
Calculations are based on standard ventilation guidelines and building codes.
|
||||
Local codes may vary. Consider consulting with a qualified professional for specific
|
||||
recommendations.
|
||||
"""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func statusView() -> some HTML {
|
||||
div(
|
||||
.class("""
|
||||
w-full rounded-lg shadow-lg border-2 p-6 \(response.ventilationStatus.borderColor)
|
||||
\(response.ventilationStatus.textColor) \(response.ventilationStatus.backgroundColor)
|
||||
""")
|
||||
) {
|
||||
div(.class("flex text-2xl mb-6")) {
|
||||
SVG(.thermometerSun, color: response.ventilationStatus.textColor)
|
||||
h4(.class("font-extrabold ms-2")) { "Ventilation Status: \(response.ventilationStatus.rawValue.capitalized)" }
|
||||
}
|
||||
div(.class("grid justify-items-stretch grid-cols-1 md:grid-cols-3")) {
|
||||
group("Temperature Differential", "\(double: response.temperatureDifferential, fractionDigits: 1) °F")
|
||||
group("Dewpoint Differential", "\(double: response.dewpointDifferential, fractionDigits: 1) °F")
|
||||
group("Pressure Differential", "\(double: response.pressureDifferential, fractionDigits: 1) °F")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func requiredVentilationView() -> some HTML {
|
||||
div(
|
||||
.class("""
|
||||
w-full rounded-lg border p-6 border-blue-600 text-blue-600 bg-blue-100 dark:bg-blue-300
|
||||
""")
|
||||
) {
|
||||
div(.class("flex text-2xl mb-6")) {
|
||||
h4(.class("font-extrabold ms-2")) { "Required Ventilation" }
|
||||
}
|
||||
div(.class("grid grid-cols-1 md:grid-cols-2 content-center")) {
|
||||
group("Intake", "\(double: response.requiredVentilationArea.intake, fractionDigits: 1) ft²")
|
||||
group("Exhaust", "\(double: response.requiredVentilationArea.exhaust, fractionDigits: 1) ft²")
|
||||
}
|
||||
if response.recommendations.count > 0 {
|
||||
div(.class("mt-8")) {
|
||||
h4(.class("font-bold")) { "Recommendations:" }
|
||||
ul(.class("list-disc mx-10")) {
|
||||
for recommendation in response.recommendations {
|
||||
li { recommendation }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func group(_ label: String, _ value: String) -> some HTML<HTMLTag.div> {
|
||||
div(.class("justify-self-center")) {
|
||||
div(.class("grid grid-cols-1 justify-items-center")) {
|
||||
p(.class("font-medium")) { label }
|
||||
h3(.class("text-3xl font-extrabold")) { value }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private extension AtticVentilation.VentilationStatus {
|
||||
var backgroundColor: String {
|
||||
switch self {
|
||||
case .adequate: return "bg-green-200"
|
||||
case .inadequate: return "bg-amber-200"
|
||||
case .critical: return "bg-red-200"
|
||||
}
|
||||
}
|
||||
|
||||
var borderColor: String {
|
||||
switch self {
|
||||
case .adequate: return "border-green-500"
|
||||
case .inadequate: return "border-amber-500"
|
||||
case .critical: return "border-red-500"
|
||||
}
|
||||
}
|
||||
|
||||
var textColor: String {
|
||||
switch self {
|
||||
case .adequate: return "text-green-500"
|
||||
case .inadequate: return "text-amber-500"
|
||||
case .critical: return "text-red-500"
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -249,8 +249,8 @@ private extension RoomPressure.Mode {
|
||||
|
||||
var label: String {
|
||||
switch self {
|
||||
case let .knownAirflow: return "Known Airflow"
|
||||
case let .measuredPressure: return "Measured Pressure"
|
||||
case .knownAirflow: return "Known Airflow"
|
||||
case .measuredPressure: return "Measured Pressure"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user