///|
fn schedule_node_products(root : SpatialNode) -> Array[Int] {
root.all_product_ids()
}
///|
fn schedule_count_kind(products : Array[Product], kind : ProductKind) -> Int {
products.fold(init=0, (result, product) => {
if product.kind == kind {
result + 1
} else {
result
}
})
}
///|
fn schedule_product_by_id(products : Array[Product], id : Int) -> Product? {
for product in products {
if product.id == id {
return Some(product)
}
}
None
}
///|
fn schedule_row_for_node(
model : Model,
index : ModelIndex,
node : SpatialNode,
) -> SpatialScheduleRow raise ParseError {
let products = model.products()
let product_ids = schedule_node_products(node)
let selected = product_ids.fold(init=Array::new(), (result, id) => {
match schedule_product_by_id(products, id) {
Some(product) => result.push(product)
None => ()
}
result
})
let rows = selected.map(product => {
quantity_for_product(index, product, default_takeoff_options())
})
let bounds = rows.fold(init=BoundingBox::empty(), (result, row) => {
merge_bounds(result, row.bounds)
})
{
id: node.id,
name: node.name.unwrap_or(""),
ifc_type: node.ifc_type,
path: node.path_to(node.id).unwrap_or([node.id]),
product_count: selected.length(),
door_count: schedule_count_kind(selected, Door),
window_count: schedule_count_kind(selected, Window),
wall_count: schedule_count_kind(selected, Wall),
area: rows.fold(init=0.0, (result, row) => result + row.value(Area)),
volume: rows.fold(init=0.0, (result, row) => result + row.value(Volume)),
bounds,
}
}
///|
fn schedule_nodes(root : SpatialNode) -> Array[SpatialNode] {
flatten_spatial(root).filter(node => {
let name = node.ifc_type.to_upper()
name == "IFCSPACE" || name == "IFCBUILDINGSTOREY" || name == "IFCBUILDING"
})
}
///|
/// Produce room/storey rows for schedules, dashboards and downstream reports.
pub fn spatial_schedule(model : Model) -> SpatialSchedule raise ParseError {
let index = model.index()
let rows : Array[SpatialScheduleRow] = []
match spatial_roots(index) {
roots =>
for root in roots {
for node in schedule_nodes(root) {
rows.push(schedule_row_for_node(model, index, node))
}
}
}
{
rows,
total_spaces: rows
.filter(row => row.ifc_type.to_upper() == "IFCSPACE")
.length(),
total_products: rows.fold(init=0, (result, row) => {
result + row.product_count
}),
total_area: rows.fold(init=0.0, (result, row) => result + row.area),
total_volume: rows.fold(init=0.0, (result, row) => result + row.volume),
}
}
///|
pub fn SpatialSchedule::row_count(self : SpatialSchedule) -> Int {
self.rows.length()
}
///|
pub fn SpatialSchedule::spaces(
self : SpatialSchedule,
) -> Array[SpatialScheduleRow] {
self.rows.filter(row => row.ifc_type.to_upper() == "IFCSPACE")
}
///|
pub fn SpatialSchedule::storeys(
self : SpatialSchedule,
) -> Array[SpatialScheduleRow] {
self.rows.filter(row => row.ifc_type.to_upper() == "IFCBUILDINGSTOREY")
}
///|
pub fn SpatialSchedule::find(
self : SpatialSchedule,
id : Int,
) -> SpatialScheduleRow? {
for row in self.rows {
if row.id == id {
return Some(row)
}
}
None
}
///|
pub fn SpatialSchedule::named(
self : SpatialSchedule,
fragment : String,
) -> Array[SpatialScheduleRow] {
let wanted = fragment.to_lower()
self.rows.filter(row => row.name.to_lower().contains(wanted))
}
///|
pub fn SpatialSchedule::total_doors(self : SpatialSchedule) -> Int {
self.rows.fold(init=0, (result, row) => result + row.door_count)
}
///|
pub fn SpatialSchedule::total_windows(self : SpatialSchedule) -> Int {
self.rows.fold(init=0, (result, row) => result + row.window_count)
}
///|
pub fn SpatialSchedule::total_walls(self : SpatialSchedule) -> Int {
self.rows.fold(init=0, (result, row) => result + row.wall_count)
}
///|
pub fn SpatialSchedule::average_space_area(self : SpatialSchedule) -> Double {
let spaces = self.spaces()
if spaces.length() == 0 {
0.0
} else {
spaces.fold(init=0.0, (result, row) => result + row.area) /
spaces.length().to_double()
}
}
///|
pub fn SpatialSchedule::headline(self : SpatialSchedule) -> String {
"rows=" +
self.rows.length().to_string() +
" spaces=" +
self.total_spaces.to_string() +
" products=" +
self.total_products.to_string() +
" area=" +
self.total_area.to_string() +
" volume=" +
self.total_volume.to_string()
}
///|
pub fn SpatialScheduleRow::headline(self : SpatialScheduleRow) -> String {
self.ifc_type +
" #" +
self.id.to_string() +
" " +
self.name +
" products=" +
self.product_count.to_string() +
" area=" +
self.area.to_string() +
" volume=" +
self.volume.to_string()
}
///|
pub fn SpatialScheduleRow::opening_count(self : SpatialScheduleRow) -> Int {
self.door_count + self.window_count
}
///|
pub fn SpatialScheduleRow::path_text(self : SpatialScheduleRow) -> String {
self.path.map(id => "#" + id.to_string()).join("/")
}
///|
pub fn SpatialSchedule::to_csv(self : SpatialSchedule) -> String {
let builder = StringBuilder()
builder <+ "id,type,name,path,products,doors,windows,walls,area,volume\n"
for row in self.rows {
builder <+
"\{row.id},\{row.ifc_type},\{row.name},\{row.path_text()},\{row.product_count},\{row.door_count},\{row.window_count},\{row.wall_count},\{row.area},\{row.volume}\n"
}
builder.to_string()
}
///|
fn schedule_row_json(row : SpatialScheduleRow) -> String {
"{\"id\":" +
row.id.to_string() +
",\"type\":\"" +
row.ifc_type +
"\",\"name\":\"" +
row.name +
"\",\"products\":" +
row.product_count.to_string() +
",\"doors\":" +
row.door_count.to_string() +
",\"windows\":" +
row.window_count.to_string() +
",\"walls\":" +
row.wall_count.to_string() +
",\"area\":" +
row.area.to_string() +
",\"volume\":" +
row.volume.to_string() +
"}"
}
///|
pub fn SpatialSchedule::to_json(self : SpatialSchedule) -> String {
"{\"total_spaces\":" +
self.total_spaces.to_string() +
",\"total_products\":" +
self.total_products.to_string() +
",\"total_area\":" +
self.total_area.to_string() +
",\"total_volume\":" +
self.total_volume.to_string() +
",\"rows\":[" +
self.rows.map(row => schedule_row_json(row)).join(",") +
"]}"
}
///|
pub fn SpatialSchedule::to_markdown(self : SpatialSchedule) -> String {
let builder = StringBuilder()
builder <+ "# Spatial schedule\n\n"
builder <+ "- Spaces: \{self.total_spaces}\n"
builder <+ "- Products: \{self.total_products}\n"
builder <+ "- Doors: \{self.total_doors()}\n"
builder <+ "- Windows: \{self.total_windows()}\n"
builder <+ "- Area: \{self.total_area}\n"
builder <+ "- Volume: \{self.total_volume}\n\n"
builder <+
"| ID | Type | Name | Products | Doors | Windows | Area | Volume |\n"
builder <+ "| ---: | --- | --- | ---: | ---: | ---: | ---: | ---: |\n"
for row in self.rows {
builder <+
"| \{row.id} | \{row.ifc_type} | \{row.name} | \{row.product_count} | \{row.door_count} | \{row.window_count} | \{row.area} | \{row.volume} |\n"
}
builder.to_string()
}
///|
pub fn schedule_area_by_name(
schedule : SpatialSchedule,
name : String,
) -> Double {
schedule.named(name).fold(init=0.0, (result, row) => result + row.area)
}
///|
pub fn schedule_product_count_by_type(
schedule : SpatialSchedule,
ifc_type : String,
) -> Int {
let wanted = ifc_type.to_upper()
schedule.rows
.filter(row => row.ifc_type.to_upper() == wanted)
.fold(init=0, (result, row) => result + row.product_count)
}