///|
fn catalog_type_counts(index : ModelIndex) -> Map[String, Int] {
let counts : Map[String, Int] = Map([])
for entity in index.all() {
let name = entity.name.to_upper()
counts.update_or_default(name, 1, count => count + 1)
}
counts
}
///|
/// Build the indexes that are normally needed by an application session.
///
/// The catalog deliberately keeps the original `Model` alongside its indexes.
/// This makes it possible to move from an optimized query back to a lossless
/// entity without reparsing the source text.
pub fn ModelCatalog::new(model : Model) -> ModelCatalog raise ParseError {
let index = model.index()
let products = model.products()
let property_sets = model.property_sets()
let roots = spatial_roots(index)
let metrics = analyze_model(model.to_step(), model)
{
model,
index,
products,
property_sets,
roots,
metrics,
type_counts: catalog_type_counts(index),
}
}
///|
/// Construct a reusable catalog from an IFC STEP source.
pub fn catalog(source : String) -> ModelCatalog raise ParseError {
ModelCatalog::new(parse(source))
}
///|
pub fn ModelCatalog::entity(self : ModelCatalog, id : Int) -> Entity? {
self.index.entity(id)
}
///|
pub fn ModelCatalog::has_entity(self : ModelCatalog, id : Int) -> Bool {
self.index.has(id)
}
///|
pub fn ModelCatalog::type_count(self : ModelCatalog, name : String) -> Int {
self.type_counts.get(name.to_upper()).unwrap_or(0)
}
///|
pub fn ModelCatalog::schema(self : ModelCatalog) -> String {
self.model.schema()
}
///|
pub fn ModelCatalog::summary(self : ModelCatalog) -> ModelSummary {
self.model.summary()
}
///|
pub fn ModelCatalog::products_of_kind(
self : ModelCatalog,
kind : ProductKind,
) -> Array[Product] {
self.products.filter(product => product.kind == kind)
}
///|
pub fn ModelCatalog::products_named(
self : ModelCatalog,
fragment : String,
) -> Array[Product] {
let folded = fragment.to_lower()
self.products.filter(product => {
match product.name {
Some(name) => name.to_lower().contains(folded)
None => false
}
})
}
///|
pub fn ModelCatalog::property_sets_for(
self : ModelCatalog,
product : Product,
) -> Array[PropertySet] raise ParseError {
property_sets_for_product(self.index, product)
}
///|
pub fn ModelCatalog::property(
self : ModelCatalog,
product : Product,
name : String,
) -> Property? raise ParseError {
property_named(self.property_sets_for(product), name)
}
///|
pub fn ModelCatalog::spatial_root(self : ModelCatalog) -> SpatialNode? {
self.roots.get(0)
}
///|
pub fn ModelCatalog::type_histogram(self : ModelCatalog) -> Map[String, Int] {
self.type_counts
}
///|
pub fn CatalogQuery::new() -> CatalogQuery {
{
kind: None,
ifc_type: None,
name_contains: None,
property_contains: None,
container: None,
sort_key: EntityId,
direction: Ascending,
offset: 0,
limit: 100,
}
}
///|
pub fn CatalogQuery::of_kind(
self : CatalogQuery,
kind : ProductKind,
) -> CatalogQuery {
{ ..self, kind: Some(kind) }
}
///|
pub fn CatalogQuery::of_type(
self : CatalogQuery,
ifc_type : String,
) -> CatalogQuery {
{ ..self, ifc_type: Some(ifc_type.to_upper()) }
}
///|
pub fn CatalogQuery::named(
self : CatalogQuery,
fragment : String,
) -> CatalogQuery {
{ ..self, name_contains: Some(fragment.to_lower()) }
}
///|
pub fn CatalogQuery::with_property(
self : CatalogQuery,
fragment : String,
) -> CatalogQuery {
{ ..self, property_contains: Some(fragment.to_lower()) }
}
///|
pub fn CatalogQuery::in_container(
self : CatalogQuery,
container : Int,
) -> CatalogQuery {
{ ..self, container: Some(container) }
}
///|
pub fn CatalogQuery::sorted_by(
self : CatalogQuery,
sort_key : CatalogSortKey,
direction : SortDirection,
) -> CatalogQuery {
{ ..self, sort_key, direction }
}
///|
pub fn CatalogQuery::page(
self : CatalogQuery,
offset : Int,
limit : Int,
) -> CatalogQuery {
{
..self,
offset: if offset < 0 {
0
} else {
offset
},
limit: if limit < 0 {
0
} else {
limit
},
}
}
///|
fn query_name(product : Product) -> String {
product.name.unwrap_or("").to_lower()
}
///|
fn query_kind_name(product : Product) -> String {
match product.kind {
Project => "project"
Site => "site"
Building => "building"
Storey => "storey"
Space => "space"
Wall => "wall"
Door => "door"
Window => "window"
Beam => "beam"
Column => "column"
Slab => "slab"
Roof => "roof"
Stair => "stair"
Ramp => "ramp"
Opening => "opening"
Generic => "generic"
}
}
///|
fn product_property_matches(
index : ModelIndex,
product : Product,
fragment : String,
) -> Bool raise ParseError {
let needle = fragment.to_lower()
for property_set in property_sets_for_product(index, product) {
if property_set.name.unwrap_or("").to_lower().contains(needle) {
return true
}
for property in property_set.properties {
if property.name.to_lower().contains(needle) ||
property.value.to_step().to_lower().contains(needle) {
return true
}
}
}
false
}
///|
fn product_matches_query(
index : ModelIndex,
product : Product,
query : CatalogQuery,
) -> Bool raise ParseError {
let kind_ok = match query.kind {
Some(kind) => product.kind == kind
None => true
}
let type_ok = match query.ifc_type {
Some(name) => product.ifc_type.to_upper() == name
None => true
}
let name_ok = match query.name_contains {
Some(fragment) => query_name(product).contains(fragment)
None => true
}
let property_ok = match query.property_contains {
Some(fragment) => product_property_matches(index, product, fragment)
None => true
}
let container_ok = match query.container {
Some(id) => product.container == Some(id)
None => true
}
kind_ok && type_ok && name_ok && property_ok && container_ok
}
///|
fn product_sort_value(product : Product, key : CatalogSortKey) -> String {
match key {
EntityId => product.id.to_string()
EntityType => product.ifc_type.to_upper()
ProductName => query_name(product)
ProductKind => query_kind_name(product)
PropertyValue => product.tag.unwrap_or("").to_lower()
}
}
///|
fn product_precedes(
left : Product,
right : Product,
key : CatalogSortKey,
direction : SortDirection,
) -> Bool {
let comparison = String::compare(
product_sort_value(left, key),
product_sort_value(right, key),
)
match direction {
Ascending => comparison < 0
Descending => comparison > 0
}
}
///|
fn stable_product_sort(
products : Array[Product],
key : CatalogSortKey,
direction : SortDirection,
) -> Array[Product] {
let result = products.copy()
let length = result.length()
for i in 0.. CatalogPage raise ParseError {
let matched = self.products.filter(product => {
product_matches_query(self.index, product, query)
})
let sorted = stable_product_sort(matched, query.sort_key, query.direction)
let total = sorted.length()
let offset = if query.offset > total { total } else { query.offset }
let requested_end = offset + query.limit
let end = if requested_end > total { total } else { requested_end }
let page = if query.limit == 0 || offset == end {
[]
} else {
sorted[offset:end].to_owned()
}
{
products: page,
total_matches: total,
offset,
limit: query.limit,
has_next: end < total,
}
}
///|
pub fn ModelCatalog::query_all(
self : ModelCatalog,
query : CatalogQuery,
) -> Array[Product] raise ParseError {
let page = self.query({ ..query, offset: 0, limit: self.products.length() })
page.products
}
///|
pub fn catalog_query(
model : Model,
query : CatalogQuery,
) -> Array[Product] raise ParseError {
ModelCatalog::new(model).query_all(query)
}