feat: Initial filter pressure drop views, calculations need implemented.
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -40,6 +40,11 @@ extension ApiController: DependencyKey {
|
|||||||
logger.debug("Calculating dehumidifier size: \(request)")
|
logger.debug("Calculating dehumidifier size: \(request)")
|
||||||
return try await request.respond(logger)
|
return try await request.respond(logger)
|
||||||
|
|
||||||
|
case let .calculateFilterPressureDrop(request):
|
||||||
|
logger.debug("Calculating filter pressure drop: \(request)")
|
||||||
|
// FIX:
|
||||||
|
fatalError()
|
||||||
|
|
||||||
case let .calculateHVACSystemPerformance(request):
|
case let .calculateHVACSystemPerformance(request):
|
||||||
logger.debug("Calculating hvac system performance: \(request)")
|
logger.debug("Calculating hvac system performance: \(request)")
|
||||||
return try await request.respond(logger: logger)
|
return try await request.respond(logger: logger)
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
public enum ClimateZone: String, Codable, Equatable, Sendable {
|
public enum ClimateZone: String, CaseIterable, Codable, Equatable, Sendable {
|
||||||
case dry
|
// NOTE: Keep in this order.
|
||||||
|
|
||||||
case hotHumid
|
case hotHumid
|
||||||
case marine
|
|
||||||
case moist
|
case moist
|
||||||
|
case dry
|
||||||
|
case marine
|
||||||
|
|
||||||
public var zoneIdentifiers: [String] {
|
public var zoneIdentifiers: [String] {
|
||||||
switch self {
|
switch self {
|
||||||
@@ -26,6 +28,6 @@ public enum ClimateZone: String, Codable, Equatable, Sendable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public var label: String {
|
public var label: String {
|
||||||
return "\(rawValue.capitalized) (\(zoneIdentifiers.joined(separator: ",")))"
|
return "\(self == .hotHumid ? "Hot Humid" : rawValue.capitalized) (\(zoneIdentifiers.joined(separator: ", ")))"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,20 +4,25 @@ public enum FilterPressureDrop {
|
|||||||
Calculate filter pressure drop and sizing based on system requirements.
|
Calculate filter pressure drop and sizing based on system requirements.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
public enum Mode: String, CaseIterable, Codable, Equatable, Sendable {
|
||||||
|
case basic
|
||||||
|
case fanLaw
|
||||||
|
}
|
||||||
|
|
||||||
public enum Request: Codable, Equatable, Sendable {
|
public enum Request: Codable, Equatable, Sendable {
|
||||||
case basic(Basic)
|
case basic(Basic)
|
||||||
case fanLaw(FanLaw)
|
case fanLaw(FanLaw)
|
||||||
|
|
||||||
public struct Basic: Codable, Equatable, Sendable {
|
public struct Basic: Codable, Equatable, Sendable {
|
||||||
|
|
||||||
let systemSize: HVACSystemSize
|
let systemSize: Double
|
||||||
let climateZone: ClimateZone
|
let climateZone: ClimateZone
|
||||||
let filterType: FilterType
|
let filterType: FilterType
|
||||||
let filterWidth: Double
|
let filterWidth: Double
|
||||||
let filterHeight: Double
|
let filterHeight: Double
|
||||||
|
|
||||||
public init(
|
public init(
|
||||||
systemSize: HVACSystemSize,
|
systemSize: Double,
|
||||||
climateZone: ClimateZone,
|
climateZone: ClimateZone,
|
||||||
filterType: FilterPressureDrop.FilterType,
|
filterType: FilterPressureDrop.FilterType,
|
||||||
filterWidth: Double,
|
filterWidth: Double,
|
||||||
@@ -58,22 +63,30 @@ public enum FilterPressureDrop {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum Result: Codable, Equatable, Sendable {
|
public enum Response: Codable, Equatable, Sendable {
|
||||||
case basic(Basic)
|
case basic(Basic)
|
||||||
case fanLaw(FanLaw)
|
case fanLaw(FanLaw)
|
||||||
|
|
||||||
public struct Basic: Codable, Equatable, Sendable {
|
public struct Basic: Codable, Equatable, Sendable {
|
||||||
|
|
||||||
let filterArea: Double
|
public let filterArea: Double
|
||||||
let feetPerMinute: Double
|
public let feetPerMinute: Double
|
||||||
let initialPressureDrop: Double
|
public let initialPressureDrop: Double
|
||||||
let maxPressureDrop: Double
|
public let maxPressureDrop: Double
|
||||||
|
public let warnings: [String]
|
||||||
|
|
||||||
public init(filterArea: Double, feetPerMinute: Double, initialPressureDrop: Double, maxPressureDrop: Double) {
|
public init(
|
||||||
|
filterArea: Double,
|
||||||
|
feetPerMinute: Double,
|
||||||
|
initialPressureDrop: Double,
|
||||||
|
maxPressureDrop: Double,
|
||||||
|
warnings: [String]
|
||||||
|
) {
|
||||||
self.filterArea = filterArea
|
self.filterArea = filterArea
|
||||||
self.feetPerMinute = feetPerMinute
|
self.feetPerMinute = feetPerMinute
|
||||||
self.initialPressureDrop = initialPressureDrop
|
self.initialPressureDrop = initialPressureDrop
|
||||||
self.maxPressureDrop = maxPressureDrop
|
self.maxPressureDrop = maxPressureDrop
|
||||||
|
self.warnings = warnings
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,7 +111,7 @@ public enum FilterPressureDrop {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum FilterType: String, Codable, Equatable, Sendable {
|
public enum FilterType: String, CaseIterable, Codable, Equatable, Sendable {
|
||||||
case fiberglass
|
case fiberglass
|
||||||
case pleatedBasic
|
case pleatedBasic
|
||||||
case pleatedBetter
|
case pleatedBetter
|
||||||
@@ -114,3 +127,34 @@ public enum FilterPressureDrop {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
public extension FilterPressureDrop.Response {
|
||||||
|
|
||||||
|
static func mock(mode: FilterPressureDrop.Mode) -> Self {
|
||||||
|
switch mode {
|
||||||
|
case .basic:
|
||||||
|
return .basic(.init(
|
||||||
|
filterArea: 3.47,
|
||||||
|
feetPerMinute: 230,
|
||||||
|
initialPressureDrop: 0.2,
|
||||||
|
maxPressureDrop: 0.15,
|
||||||
|
warnings: [
|
||||||
|
"""
|
||||||
|
Intial pressure drop is more than 50% of maximum allowable.
|
||||||
|
Consider using a larger filter or different type with lower intial pressure drop.
|
||||||
|
"""
|
||||||
|
]
|
||||||
|
))
|
||||||
|
case .fanLaw:
|
||||||
|
return .fanLaw(.init(
|
||||||
|
predictedPressureDrop: 0.127,
|
||||||
|
velocityRatio: 1.13,
|
||||||
|
pressureRatio: 1.27,
|
||||||
|
faceVelocity: 259
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
public enum HVACSystemSize: Double, Codable, Equatable, Sendable {
|
public enum HVACSystemSize: Double, CaseIterable, Codable, Equatable, Sendable {
|
||||||
case one = 1
|
case one = 1
|
||||||
case oneAndAHalf = 1.5
|
case oneAndAHalf = 1.5
|
||||||
case two = 2
|
case two = 2
|
||||||
|
|||||||
@@ -13,6 +13,10 @@ public enum SiteRoute: Equatable, Sendable {
|
|||||||
Route(.case(Self.api)) {
|
Route(.case(Self.api)) {
|
||||||
Api.router
|
Api.router
|
||||||
}
|
}
|
||||||
|
Route(.case(Self.health)) {
|
||||||
|
Path { "health" }
|
||||||
|
Method.get
|
||||||
|
}
|
||||||
Route(.case(Self.view)) {
|
Route(.case(Self.view)) {
|
||||||
View.router
|
View.router
|
||||||
}
|
}
|
||||||
@@ -26,6 +30,7 @@ public extension SiteRoute {
|
|||||||
case calculateAtticVentilation(AtticVentilation.Request)
|
case calculateAtticVentilation(AtticVentilation.Request)
|
||||||
case calculateCapacitor(Capacitor.Request)
|
case calculateCapacitor(Capacitor.Request)
|
||||||
case calculateDehumidifierSize(DehumidifierSize.Request)
|
case calculateDehumidifierSize(DehumidifierSize.Request)
|
||||||
|
case calculateFilterPressureDrop(FilterPressureDrop.Request)
|
||||||
case calculateHVACSystemPerformance(HVACSystemPerformance.Request)
|
case calculateHVACSystemPerformance(HVACSystemPerformance.Request)
|
||||||
case calculateMoldRisk(MoldRisk.Request)
|
case calculateMoldRisk(MoldRisk.Request)
|
||||||
case calculateRoomPressure(RoomPressure.Request)
|
case calculateRoomPressure(RoomPressure.Request)
|
||||||
@@ -53,6 +58,16 @@ public extension SiteRoute {
|
|||||||
Method.post
|
Method.post
|
||||||
Body(.json(DehumidifierSize.Request.self))
|
Body(.json(DehumidifierSize.Request.self))
|
||||||
}
|
}
|
||||||
|
Route(.case(Self.calculateFilterPressureDrop)) {
|
||||||
|
Path { "api"; "v1"; "calculateFilterPressureDrop" }
|
||||||
|
Method.post
|
||||||
|
OneOf {
|
||||||
|
Body(.json(FilterPressureDrop.Request.Basic.self))
|
||||||
|
.map(.case(FilterPressureDrop.Request.basic))
|
||||||
|
Body(.json(FilterPressureDrop.Request.FanLaw.self))
|
||||||
|
.map(.case(FilterPressureDrop.Request.fanLaw))
|
||||||
|
}
|
||||||
|
}
|
||||||
Route(.case(Self.calculateHVACSystemPerformance)) {
|
Route(.case(Self.calculateHVACSystemPerformance)) {
|
||||||
Path { "api"; "v1"; "calculateHVACSystemPerformance" }
|
Path { "api"; "v1"; "calculateHVACSystemPerformance" }
|
||||||
Method.post
|
Method.post
|
||||||
@@ -84,6 +99,7 @@ public extension SiteRoute {
|
|||||||
case atticVentilation(AtticVentilation)
|
case atticVentilation(AtticVentilation)
|
||||||
case capacitor(Capacitor)
|
case capacitor(Capacitor)
|
||||||
case dehumidifierSize(DehumidifierSize)
|
case dehumidifierSize(DehumidifierSize)
|
||||||
|
case filterPressureDrop(FilterPressureDrop)
|
||||||
case hvacSystemPerformance(HVACSystemPerformance)
|
case hvacSystemPerformance(HVACSystemPerformance)
|
||||||
case moldRisk(MoldRisk)
|
case moldRisk(MoldRisk)
|
||||||
case roomPressure(RoomPressure)
|
case roomPressure(RoomPressure)
|
||||||
@@ -101,6 +117,9 @@ public extension SiteRoute {
|
|||||||
Route(.case(Self.dehumidifierSize)) {
|
Route(.case(Self.dehumidifierSize)) {
|
||||||
DehumidifierSize.router
|
DehumidifierSize.router
|
||||||
}
|
}
|
||||||
|
Route(.case(Self.filterPressureDrop)) {
|
||||||
|
FilterPressureDrop.router
|
||||||
|
}
|
||||||
Route(.case(Self.hvacSystemPerformance)) {
|
Route(.case(Self.hvacSystemPerformance)) {
|
||||||
HVACSystemPerformance.router
|
HVACSystemPerformance.router
|
||||||
}
|
}
|
||||||
@@ -213,6 +232,53 @@ public extension SiteRoute {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public enum FilterPressureDrop: Equatable, Sendable {
|
||||||
|
case index(mode: Routes.FilterPressureDrop.Mode? = nil)
|
||||||
|
case submit(Routes.FilterPressureDrop.Request)
|
||||||
|
|
||||||
|
public static let index = Self.index()
|
||||||
|
|
||||||
|
static let rootPath = "filter-pressure-drop"
|
||||||
|
|
||||||
|
public static let router = OneOf {
|
||||||
|
Route(.case(Self.index)) {
|
||||||
|
Path { rootPath }
|
||||||
|
Method.get
|
||||||
|
Query {
|
||||||
|
Optionally { Field("mode") { Routes.FilterPressureDrop.Mode.parser() } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Route(.case(Self.submit)) {
|
||||||
|
Path { rootPath }
|
||||||
|
Method.post
|
||||||
|
Body {
|
||||||
|
OneOf {
|
||||||
|
FormData {
|
||||||
|
Field("systemSize") { Double.parser() }
|
||||||
|
Field("climateZone") { ClimateZone.parser() }
|
||||||
|
Field("filterType") { Routes.FilterPressureDrop.FilterType.parser() }
|
||||||
|
Field("filterWidth") { Double.parser() }
|
||||||
|
Field("filterHeight") { Double.parser() }
|
||||||
|
}
|
||||||
|
.map(.memberwise(Routes.FilterPressureDrop.Request.Basic.init))
|
||||||
|
.map(.case(Routes.FilterPressureDrop.Request.basic))
|
||||||
|
|
||||||
|
FormData {
|
||||||
|
Field("filterWidth") { Double.parser() }
|
||||||
|
Field("filterHeight") { Double.parser() }
|
||||||
|
Field("filterDepth") { Double.parser() }
|
||||||
|
Field("ratedAirflow") { Double.parser() }
|
||||||
|
Field("ratedPressureDrop") { Double.parser() }
|
||||||
|
Field("designAirflow") { Double.parser() }
|
||||||
|
}
|
||||||
|
.map(.memberwise(Routes.FilterPressureDrop.Request.FanLaw.init))
|
||||||
|
.map(.case(Routes.FilterPressureDrop.Request.fanLaw))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public enum HVACSystemPerformance: Equatable, Sendable {
|
public enum HVACSystemPerformance: Equatable, Sendable {
|
||||||
case index
|
case index
|
||||||
case submit(Routes.HVACSystemPerformance.Request)
|
case submit(Routes.HVACSystemPerformance.Request)
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import Elementary
|
import Elementary
|
||||||
|
import Routes
|
||||||
|
|
||||||
public struct PrimaryButton: HTML, Sendable {
|
public struct PrimaryButton: HTML, Sendable {
|
||||||
let label: String
|
let label: String
|
||||||
@@ -75,3 +76,70 @@ public struct ResetButton: HTML, Sendable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public struct Toggle: HTML {
|
||||||
|
|
||||||
|
let isOn: Bool
|
||||||
|
|
||||||
|
// Left hand side / 'on' label.
|
||||||
|
let onLabel: String
|
||||||
|
// Applied to the rhs when the toggle is consider 'off'.
|
||||||
|
let onAttributes: [HTMLAttribute<HTMLTag.button>]
|
||||||
|
|
||||||
|
// Right hand side / 'off' label.
|
||||||
|
let offLabel: String
|
||||||
|
// Applied to the left hand side when the toggle is consider 'on'.
|
||||||
|
let offAttributes: [HTMLAttribute<HTMLTag.button>]
|
||||||
|
|
||||||
|
public init(
|
||||||
|
isOn: Bool,
|
||||||
|
onLabel: String,
|
||||||
|
onAttributes: [HTMLAttribute<HTMLTag.button>],
|
||||||
|
offLabel: String,
|
||||||
|
offAttributes: [HTMLAttribute<HTMLTag.button>]
|
||||||
|
) {
|
||||||
|
self.isOn = isOn
|
||||||
|
self.onLabel = onLabel
|
||||||
|
self.onAttributes = onAttributes
|
||||||
|
self.offLabel = offLabel
|
||||||
|
self.offAttributes = offAttributes
|
||||||
|
}
|
||||||
|
|
||||||
|
public var content: some HTML<HTMLTag.div> {
|
||||||
|
div(.class("flex items-center gap-x-0")) {
|
||||||
|
switch isOn {
|
||||||
|
case true:
|
||||||
|
SecondaryButton(label: onLabel)
|
||||||
|
.attributes(.class("rounded-s-lg"), .disabled)
|
||||||
|
|
||||||
|
PrimaryButton(label: offLabel)
|
||||||
|
.attributes(contentsOf: offAttributes + [.class("rounded-e-lg")])
|
||||||
|
|
||||||
|
case false:
|
||||||
|
PrimaryButton(label: onLabel)
|
||||||
|
.attributes(contentsOf: onAttributes + [.class("rounded-s-lg")])
|
||||||
|
|
||||||
|
SecondaryButton(label: offLabel)
|
||||||
|
.attributes(.class("rounded-e-lg"), .disabled)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public extension Array where Element == HTMLAttribute<HTMLTag.button> {
|
||||||
|
|
||||||
|
static func hxDefaults(
|
||||||
|
_ attributes: HTMLAttribute<HTMLTag.button>...
|
||||||
|
) -> Self {
|
||||||
|
[
|
||||||
|
.hx.target("#content"),
|
||||||
|
.hx.pushURL(true)
|
||||||
|
] + attributes
|
||||||
|
}
|
||||||
|
|
||||||
|
static func hxDefaults(
|
||||||
|
get route: SiteRoute.View
|
||||||
|
) -> Self {
|
||||||
|
.hxDefaults(.hx.get(route: route))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ 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 rounded-md
|
w-full px-4 py-2 border rounded-md min-h-11
|
||||||
focus:ring-2 focus:ring-yellow-800 focus:border-yellow-800
|
focus:ring-2 focus:ring-yellow-800 focus:border-yellow-800
|
||||||
placeholder-shown:!border-gray-400
|
placeholder-shown:!border-gray-400
|
||||||
invalid:border-red-500 out-of-range:border-red-500
|
invalid:border-red-500 out-of-range:border-red-500
|
||||||
@@ -84,3 +84,57 @@ public struct InputLabel<InputLabel: HTML>: HTML {
|
|||||||
}
|
}
|
||||||
|
|
||||||
extension InputLabel: Sendable where InputLabel: Sendable {}
|
extension InputLabel: Sendable where InputLabel: Sendable {}
|
||||||
|
|
||||||
|
public struct Select<T>: HTML {
|
||||||
|
|
||||||
|
let id: String
|
||||||
|
let name: String?
|
||||||
|
let values: [T]
|
||||||
|
let label: @Sendable (T) -> String
|
||||||
|
let value: @Sendable (T) -> String
|
||||||
|
|
||||||
|
public init(
|
||||||
|
id: String,
|
||||||
|
name: String? = nil,
|
||||||
|
values: [T],
|
||||||
|
label: @escaping @Sendable (T) -> String,
|
||||||
|
value: @escaping @Sendable (T) -> String
|
||||||
|
) {
|
||||||
|
self.id = id
|
||||||
|
self.name = name
|
||||||
|
self.values = values
|
||||||
|
self.label = label
|
||||||
|
self.value = value
|
||||||
|
}
|
||||||
|
|
||||||
|
public var content: some HTML<HTMLTag.select> {
|
||||||
|
select(
|
||||||
|
.id(id),
|
||||||
|
.name(name ?? id),
|
||||||
|
.class("w-full rounded-md border px-4 py-2 min-h-11")
|
||||||
|
) {
|
||||||
|
for value in values {
|
||||||
|
option(.value(self.value(value))) { label(value) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Select: Sendable where T: Sendable {}
|
||||||
|
|
||||||
|
public extension Select where T: CaseIterable, T: RawRepresentable, T.RawValue: CustomStringConvertible {
|
||||||
|
init(
|
||||||
|
for type: T.Type,
|
||||||
|
id: String,
|
||||||
|
name: String? = nil,
|
||||||
|
label: @escaping @Sendable (T) -> String
|
||||||
|
) {
|
||||||
|
self.init(
|
||||||
|
id: id,
|
||||||
|
name: name,
|
||||||
|
values: Array(T.allCases),
|
||||||
|
label: label,
|
||||||
|
value: { $0.rawValue.description }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -10,11 +10,16 @@ public struct Note: HTML, Sendable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public var content: some HTML {
|
public var content: some HTML {
|
||||||
div(.class("mt-8 p-4 bg-gray-100 dark:bg-gray-700 rounded-md shadow-md border border-blue-500")) {
|
div(
|
||||||
p(.class("text-sm text-blue-500")) {
|
.class(
|
||||||
span(.class("font-extrabold pe-2")) { label }
|
"""
|
||||||
text
|
mt-8 p-4 bg-gray-100 dark:bg-gray-700 rounded-md shadow-md
|
||||||
}
|
border border-blue-500 text-blue-500 text-sm
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
p(.class("font-extrabold mb-3")) { label }
|
||||||
|
p(.class("px-6")) { text }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
34
Sources/Styleguide/Row.swift
Normal file
34
Sources/Styleguide/Row.swift
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import Elementary
|
||||||
|
|
||||||
|
public struct Row<Body: HTML>: HTML {
|
||||||
|
|
||||||
|
let body: Body
|
||||||
|
|
||||||
|
public init(
|
||||||
|
@HTMLBuilder body: () -> Body
|
||||||
|
) {
|
||||||
|
self.body = body()
|
||||||
|
}
|
||||||
|
|
||||||
|
public var content: some HTML<HTMLTag.div> {
|
||||||
|
div(.class("flex justify-between")) {
|
||||||
|
body
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public extension Row
|
||||||
|
where Body == _HTMLTuple2<HTMLElement<HTMLTag.span, HTMLText>, HTMLElement<HTMLTag.span, HTMLText>>
|
||||||
|
{
|
||||||
|
init(
|
||||||
|
label: String,
|
||||||
|
value: String
|
||||||
|
) {
|
||||||
|
self.init {
|
||||||
|
span(.class("font-bold")) { label }
|
||||||
|
span { value }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Row: Sendable where Body: Sendable {}
|
||||||
25
Sources/Styleguide/VerticalGroup.swift
Normal file
25
Sources/Styleguide/VerticalGroup.swift
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import Elementary
|
||||||
|
|
||||||
|
public struct VerticalGroup: HTML, Sendable {
|
||||||
|
let label: String
|
||||||
|
let value: String
|
||||||
|
let valueLabel: String?
|
||||||
|
|
||||||
|
public init(label: String, value: String, valueLabel: String? = nil) {
|
||||||
|
self.label = label
|
||||||
|
self.value = value
|
||||||
|
self.valueLabel = valueLabel
|
||||||
|
}
|
||||||
|
|
||||||
|
public var content: some HTML<HTMLTag.div> {
|
||||||
|
div(.class("grid grid-cols-1 justify-items-center")) {
|
||||||
|
p(.class("font-medium")) { label }
|
||||||
|
h3(.class("text-3xl font-extrabold")) {
|
||||||
|
value
|
||||||
|
if let valueLabel {
|
||||||
|
span(.class("text-lg ms-2")) { valueLabel }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -43,6 +43,12 @@ struct HomePage: HTML, Sendable {
|
|||||||
svg: .zap,
|
svg: .zap,
|
||||||
route: .capacitor(.index)
|
route: .capacitor(.index)
|
||||||
)
|
)
|
||||||
|
group(
|
||||||
|
label: "Filter Pressure Drop",
|
||||||
|
description: FilterPressureDrop.description,
|
||||||
|
svg: .funnel,
|
||||||
|
route: .filterPressureDrop(.index)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,8 +59,7 @@ struct HomePage: HTML, Sendable {
|
|||||||
route: SiteRoute.View
|
route: SiteRoute.View
|
||||||
) -> some HTML {
|
) -> some HTML {
|
||||||
button(
|
button(
|
||||||
.hx.get(route: route),
|
.hx.get(route: route), .hx.target("#content"), .hx.pushURL(true),
|
||||||
.hx.target("#content"),
|
|
||||||
.class("""
|
.class("""
|
||||||
w-full rounded-xl shadow-lg border border-blue-600 justify-items-start
|
w-full rounded-xl shadow-lg border border-blue-600 justify-items-start
|
||||||
hover:bg-blue-600 hover:border-yellow-300 hover:text-yellow-300 transition-colors
|
hover:bg-blue-600 hover:border-yellow-300 hover:text-yellow-300 transition-colors
|
||||||
|
|||||||
@@ -100,6 +100,16 @@ extension ViewController: DependencyKey {
|
|||||||
return DehumidifierSizeResult(response: response)
|
return DehumidifierSizeResult(response: response)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case let .filterPressureDrop(route):
|
||||||
|
switch route {
|
||||||
|
case let .index(mode: mode):
|
||||||
|
// FIX: remove response.
|
||||||
|
return request.respond(FilterPressureDropForm(mode: mode, response: .mock(mode: mode ?? .basic)))
|
||||||
|
case .submit:
|
||||||
|
// FIX: implement.
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
|
||||||
case let .hvacSystemPerformance(route):
|
case let .hvacSystemPerformance(route):
|
||||||
switch route {
|
switch route {
|
||||||
case .index:
|
case .index:
|
||||||
|
|||||||
@@ -143,6 +143,7 @@ struct AtticVentilationResponse: HTML, Sendable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Use Styleguid.VerticalGroup
|
||||||
private func group(_ label: String, _ value: String) -> some HTML<HTMLTag.div> {
|
private func group(_ label: String, _ value: String) -> some HTML<HTMLTag.div> {
|
||||||
div(.class("justify-self-center")) {
|
div(.class("justify-self-center")) {
|
||||||
div(.class("grid grid-cols-1 justify-items-center")) {
|
div(.class("grid grid-cols-1 justify-items-center")) {
|
||||||
|
|||||||
@@ -17,42 +17,16 @@ struct CapacitorForm: HTML, Sendable {
|
|||||||
div(.class("flex flex-wrap justify-between")) {
|
div(.class("flex flex-wrap justify-between")) {
|
||||||
FormHeader(label: "Capacitor Calculator - \(mode.rawValue.capitalized) Capacitor", svg: .zap)
|
FormHeader(label: "Capacitor Calculator - \(mode.rawValue.capitalized) Capacitor", svg: .zap)
|
||||||
// Mode toggle / buttons.
|
// Mode toggle / buttons.
|
||||||
div(.class("flex items-center gap-x-0")) {
|
|
||||||
switch mode {
|
// Mode toggle / buttons.
|
||||||
case .test:
|
Toggle(
|
||||||
SecondaryButton(label: "Test Capacitor")
|
isOn: mode == .test,
|
||||||
.attributes(.class("rounded-s-lg"))
|
onLabel: "Test Capacitor",
|
||||||
.attributes(.disabled, when: mode == .test)
|
onAttributes: .hxDefaults(get: .capacitor(.index(mode: .test))),
|
||||||
.attributes(
|
offLabel: "Size Capacitor",
|
||||||
.hx.get(route: .capacitor(.index(mode: .test))),
|
offAttributes: .hxDefaults(get: .capacitor(.index(mode: .size)))
|
||||||
.hx.target("#content"),
|
|
||||||
when: mode == .size
|
|
||||||
)
|
)
|
||||||
PrimaryButton(label: "Size Capacitor")
|
.attributes(.class("mb-6"))
|
||||||
.attributes(.class("rounded-e-lg"))
|
|
||||||
.attributes(
|
|
||||||
.hx.get(route: .capacitor(.index(mode: .size))),
|
|
||||||
.hx.target("#content"),
|
|
||||||
when: mode == .test
|
|
||||||
)
|
|
||||||
case .size:
|
|
||||||
PrimaryButton(label: "Test Capacitor")
|
|
||||||
.attributes(.class("rounded-s-lg"))
|
|
||||||
.attributes(
|
|
||||||
.hx.get(route: .capacitor(.index(mode: .test))),
|
|
||||||
.hx.target("#content"),
|
|
||||||
when: mode == .size
|
|
||||||
)
|
|
||||||
SecondaryButton(label: "Size Capacitor")
|
|
||||||
.attributes(.class("rounded-e-lg"))
|
|
||||||
.attributes(.disabled, when: mode == .size)
|
|
||||||
.attributes(
|
|
||||||
.hx.get(route: .capacitor(.index(mode: .size))),
|
|
||||||
.hx.target("#content"),
|
|
||||||
when: mode == .test
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Form(mode: mode).attributes(.class("mt-6"))
|
Form(mode: mode).attributes(.class("mt-6"))
|
||||||
|
|||||||
249
Sources/ViewController/Views/FilterPressureDrop.swift
Normal file
249
Sources/ViewController/Views/FilterPressureDrop.swift
Normal file
@@ -0,0 +1,249 @@
|
|||||||
|
import Elementary
|
||||||
|
import ElementaryHTMX
|
||||||
|
import Routes
|
||||||
|
import Styleguide
|
||||||
|
|
||||||
|
struct FilterPressureDropForm: HTML, Sendable {
|
||||||
|
let mode: FilterPressureDrop.Mode
|
||||||
|
let response: FilterPressureDrop.Response?
|
||||||
|
|
||||||
|
init(mode: FilterPressureDrop.Mode? = nil, response: FilterPressureDrop.Response? = nil) {
|
||||||
|
self.mode = mode ?? .basic
|
||||||
|
self.response = response
|
||||||
|
}
|
||||||
|
|
||||||
|
var content: some HTML {
|
||||||
|
div(.class("flex flex-wrap justify-between")) {
|
||||||
|
FormHeader(label: "Filter Pressure Drop - \(mode.label)", svg: .funnel)
|
||||||
|
Toggle(
|
||||||
|
isOn: mode == .basic,
|
||||||
|
onLabel: FilterPressureDrop.Mode.basic.label,
|
||||||
|
onAttributes: .hxDefaults(get: .filterPressureDrop(.index(mode: .basic))),
|
||||||
|
offLabel: FilterPressureDrop.Mode.fanLaw.label,
|
||||||
|
offAttributes: .hxDefaults(get: .filterPressureDrop(.index(mode: .fanLaw)))
|
||||||
|
)
|
||||||
|
.attributes(.class("mb-6"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIX: Remove when done.
|
||||||
|
WarningBox("This is under construction and does not work currently.")
|
||||||
|
|
||||||
|
form(
|
||||||
|
.hx.post(route: .filterPressureDrop(.index)),
|
||||||
|
.hx.target("#result")
|
||||||
|
) {
|
||||||
|
div(.class("space-y-6")) {
|
||||||
|
switch mode {
|
||||||
|
case .basic:
|
||||||
|
BasicFormFields()
|
||||||
|
case .fanLaw:
|
||||||
|
FanLawFormFields()
|
||||||
|
}
|
||||||
|
div {
|
||||||
|
SubmitButton(label: "Calculate Pressure Drop")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
div(.id("result")) {
|
||||||
|
if let response {
|
||||||
|
FilterPressureDropResponse(response: response)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct BasicFormFields: HTML, Sendable {
|
||||||
|
|
||||||
|
var content: some HTML {
|
||||||
|
div {
|
||||||
|
div {
|
||||||
|
h4(.class("text-lg font-bold mb-4")) { "System Parameters" }
|
||||||
|
div(.class("grid grid-cols-1 lg:grid-cols-3 gap-x-4 space-y-6")) {
|
||||||
|
LabeledContent(label: "System Size: (Tons)") {
|
||||||
|
Input(id: "systemSize", placeholder: "Tons")
|
||||||
|
.attributes(.type(.number), .step("0.5"), .min("1.0"), .autofocus, .required)
|
||||||
|
}
|
||||||
|
div {
|
||||||
|
InputLabel(for: "climateZone") { "Climate Zone" }
|
||||||
|
Select(for: ClimateZone.self, id: "climateZone") {
|
||||||
|
$0.label
|
||||||
|
}
|
||||||
|
}
|
||||||
|
div {
|
||||||
|
InputLabel(for: "filterType") { "Filter Type" }
|
||||||
|
Select(for: FilterPressureDrop.FilterType.self, id: "filterType") {
|
||||||
|
$0.label
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
div(.class("mt-6 lg:mt-0")) {
|
||||||
|
h4(.class("text-lg font-bold mb-4")) { "Filter Parameters" }
|
||||||
|
|
||||||
|
div(.class("grid grid-cols-1 lg:grid-cols-2 gap-x-4 space-y-6")) {
|
||||||
|
LabeledContent(label: "Width: (in)") {
|
||||||
|
Input(id: "filterWidth", placeholder: "Width")
|
||||||
|
.attributes(.type(.number), .step("0.1"), .min("0.1"), .required)
|
||||||
|
}
|
||||||
|
LabeledContent(label: "Height: (in)") {
|
||||||
|
Input(id: "filterHeight", placeholder: "Height")
|
||||||
|
.attributes(.type(.number), .step("0.1"), .min("0.1"), .required)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct FanLawFormFields: HTML, Sendable {
|
||||||
|
|
||||||
|
var content: some HTML {
|
||||||
|
div {
|
||||||
|
div(.class("mt-6 lg:mt-0")) {
|
||||||
|
h4(.class("text-lg font-bold mb-4")) { "Filter Parameters" }
|
||||||
|
div(.class("grid grid-cols-1 lg:grid-cols-3 gap-x-4 space-y-6")) {
|
||||||
|
LabeledContent(label: "Width: (in)") {
|
||||||
|
Input(id: "filterWidth", placeholder: "Width")
|
||||||
|
.attributes(.type(.number), .step("0.1"), .min("0.1"), .required, .autofocus)
|
||||||
|
}
|
||||||
|
LabeledContent(label: "Height: (in)") {
|
||||||
|
Input(id: "filterHeight", placeholder: "Height")
|
||||||
|
.attributes(.type(.number), .step("0.1"), .min("0.1"), .required)
|
||||||
|
}
|
||||||
|
LabeledContent(label: "Depth: (in)") {
|
||||||
|
Input(id: "filterDepth", placeholder: "Depth")
|
||||||
|
.attributes(.type(.number), .step("1.0"), .min("1.0"), .required)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
div(.class("mt-6 lg:mt-0")) {
|
||||||
|
h4(.class("text-lg font-bold mb-4")) { "Airflow Parameters" }
|
||||||
|
div(.class("grid grid-cols-1 lg:grid-cols-3 gap-x-4 space-y-6")) {
|
||||||
|
LabeledContent(label: "Rated Airflow: (CFM)") {
|
||||||
|
Input(id: "ratedAirflow", placeholder: "Rated or measured Airflow")
|
||||||
|
.attributes(.type(.number), .step("0.1"), .min("0.1"), .required)
|
||||||
|
}
|
||||||
|
LabeledContent(label: "Filter Pressure Drop: (in. w.c.)") {
|
||||||
|
Input(id: "ratedPressure", placeholder: "Rated or measured pressure drop")
|
||||||
|
.attributes(.type(.number), .step("0.1"), .min("0.1"), .required)
|
||||||
|
}
|
||||||
|
LabeledContent(label: "Design Airflow: (CFM)") {
|
||||||
|
Input(id: "designAirflow", placeholder: "Design or target airflow")
|
||||||
|
.attributes(.type(.number), .step("1.0"), .min("1.0"), .required)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct FilterPressureDropResponse: HTML, Sendable {
|
||||||
|
let response: FilterPressureDrop.Response
|
||||||
|
|
||||||
|
var content: some HTML {
|
||||||
|
ResultContainer(reset: .filterPressureDrop(.index(mode: response.resetMode))) {
|
||||||
|
switch response {
|
||||||
|
case let .basic(response):
|
||||||
|
BasicView(response: response)
|
||||||
|
case let .fanLaw(response):
|
||||||
|
FanLawView(response: response)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct BasicView: HTML {
|
||||||
|
let response: FilterPressureDrop.Response.Basic
|
||||||
|
|
||||||
|
var content: some HTML {
|
||||||
|
div(
|
||||||
|
.class("""
|
||||||
|
grid grid-cols-1 lg:grid-cols-2 gap-6 rounded-lg shadow-lg
|
||||||
|
border border-blue-600 text-blue-600 bg-blue-100 p-6
|
||||||
|
""")
|
||||||
|
) {
|
||||||
|
VerticalGroup(
|
||||||
|
label: "Filter Face Area",
|
||||||
|
value: "\(double: response.filterArea, fractionDigits: 2)",
|
||||||
|
valueLabel: "ft²"
|
||||||
|
)
|
||||||
|
VerticalGroup(
|
||||||
|
label: "Filter Face Velocity",
|
||||||
|
value: "\(double: response.feetPerMinute, fractionDigits: 1)",
|
||||||
|
valueLabel: "FPM"
|
||||||
|
)
|
||||||
|
VerticalGroup(
|
||||||
|
label: "Initial Pressure Drop",
|
||||||
|
value: "\(double: response.initialPressureDrop, fractionDigits: 2)\"",
|
||||||
|
valueLabel: "w.c."
|
||||||
|
)
|
||||||
|
VerticalGroup(
|
||||||
|
label: "Maximum Allowable Pressure Drop",
|
||||||
|
value: "\(double: response.maxPressureDrop, fractionDigits: 2)\"",
|
||||||
|
valueLabel: "w.c."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
WarningBox(warnings: response.warnings)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct FanLawView: HTML {
|
||||||
|
let response: FilterPressureDrop.Response.FanLaw
|
||||||
|
|
||||||
|
var content: some HTML {
|
||||||
|
div(
|
||||||
|
.class("""
|
||||||
|
grid grid-cols-1 lg:grid-cols-2 gap-6 rounded-lg shadow-lg
|
||||||
|
border border-blue-600 text-blue-600 bg-blue-100 p-6
|
||||||
|
""")
|
||||||
|
) {
|
||||||
|
VerticalGroup(
|
||||||
|
label: "Filter Face Velocity",
|
||||||
|
value: "\(double: response.faceVelocity, fractionDigits: 1)",
|
||||||
|
valueLabel: "FPM"
|
||||||
|
)
|
||||||
|
VerticalGroup(
|
||||||
|
label: "Predicted Pressure Drop",
|
||||||
|
value: "\(double: response.predictedPressureDrop, fractionDigits: 2)\"",
|
||||||
|
valueLabel: "w.c."
|
||||||
|
)
|
||||||
|
VerticalGroup(
|
||||||
|
label: "Velocity Ratio",
|
||||||
|
value: "\(double: response.velocityRatio, fractionDigits: 2)"
|
||||||
|
)
|
||||||
|
VerticalGroup(
|
||||||
|
label: "Pressure Ratio",
|
||||||
|
value: "\(double: response.pressureRatio, fractionDigits: 2)"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Note {
|
||||||
|
"""
|
||||||
|
Predictions are based on fan laws where pressure drop varies with the square of the airflow ratio.
|
||||||
|
Results assume similar air properties and filter loading conditions.
|
||||||
|
"""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension FilterPressureDrop.Mode {
|
||||||
|
|
||||||
|
var label: String {
|
||||||
|
switch self {
|
||||||
|
case .basic: return rawValue.capitalized
|
||||||
|
case .fanLaw: return "Fan Law"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension FilterPressureDrop.Response {
|
||||||
|
|
||||||
|
var resetMode: FilterPressureDrop.Mode {
|
||||||
|
switch self {
|
||||||
|
case .basic: return .basic
|
||||||
|
case .fanLaw: return .fanLaw
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,44 +16,14 @@ struct RoomPressureForm: HTML, Sendable {
|
|||||||
div(.class("relative")) {
|
div(.class("relative")) {
|
||||||
div(.class("flex flex-wrap justify-between")) {
|
div(.class("flex flex-wrap justify-between")) {
|
||||||
FormHeader(label: "Room Pressure Calculator - \(mode.label)", svg: .leftRightArrow)
|
FormHeader(label: "Room Pressure Calculator - \(mode.label)", svg: .leftRightArrow)
|
||||||
|
Toggle(
|
||||||
// Mode toggle / buttons.
|
isOn: mode == .knownAirflow,
|
||||||
div(.class("flex items-center gap-x-0")) {
|
onLabel: "Known Airflow",
|
||||||
switch mode {
|
onAttributes: .hxDefaults(get: .roomPressure(.index(mode: .knownAirflow))),
|
||||||
case .knownAirflow:
|
offLabel: "Measured Pressure",
|
||||||
SecondaryButton(label: "Known Airflow")
|
offAttributes: .hxDefaults(get: .roomPressure(.index(mode: .measuredPressure)))
|
||||||
.attributes(.class("rounded-s-lg"))
|
|
||||||
.attributes(.disabled, when: mode == .knownAirflow)
|
|
||||||
.attributes(
|
|
||||||
.hx.get(route: .roomPressure(.index(mode: .knownAirflow))),
|
|
||||||
.hx.target("#content"),
|
|
||||||
when: mode == .measuredPressure
|
|
||||||
)
|
)
|
||||||
PrimaryButton(label: "Measured Pressure")
|
.attributes(.class("mb-6"))
|
||||||
.attributes(.class("rounded-e-lg"))
|
|
||||||
.attributes(
|
|
||||||
.hx.get(route: .roomPressure(.index(mode: .measuredPressure))),
|
|
||||||
.hx.target("#content"),
|
|
||||||
when: mode == .knownAirflow
|
|
||||||
)
|
|
||||||
case .measuredPressure:
|
|
||||||
PrimaryButton(label: "Known Airflow")
|
|
||||||
.attributes(.class("rounded-s-lg"))
|
|
||||||
.attributes(
|
|
||||||
.hx.get(route: .roomPressure(.index(mode: .knownAirflow))),
|
|
||||||
.hx.target("#content"),
|
|
||||||
when: mode == .measuredPressure
|
|
||||||
)
|
|
||||||
SecondaryButton(label: "Measured Pressure")
|
|
||||||
.attributes(.class("rounded-e-lg"))
|
|
||||||
.attributes(.disabled, when: mode == .measuredPressure)
|
|
||||||
.attributes(
|
|
||||||
.hx.get(route: .roomPressure(.index(mode: .measuredPressure))),
|
|
||||||
.hx.target("#content"),
|
|
||||||
when: mode == .knownAirflow
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Form(mode: mode)
|
Form(mode: mode)
|
||||||
@@ -156,6 +126,7 @@ struct RoomPressureForm: HTML, Sendable {
|
|||||||
|
|
||||||
var content: some HTML {
|
var content: some HTML {
|
||||||
InputLabel(for: "preferredGrilleHeight") { "Preferred Grille Height" }
|
InputLabel(for: "preferredGrilleHeight") { "Preferred Grille Height" }
|
||||||
|
// TODO: Use Styleguide.Select
|
||||||
select(
|
select(
|
||||||
.id("preferredGrilleHeight"),
|
.id("preferredGrilleHeight"),
|
||||||
.name("preferredGrilleHeight"),
|
.name("preferredGrilleHeight"),
|
||||||
|
|||||||
Reference in New Issue
Block a user