///|
pub fn ProductQueryPlan::new() -> ProductQueryPlan {
{
expression: MatchAll,
sort: ById,
descending: false,
offset: 0,
limit: 0,
include_empty_geometry: false,
}
}
///|
pub fn ProductQueryPlan::where_(
self : ProductQueryPlan,
expression : ProductExpression,
) -> ProductQueryPlan {
{ ..self, expression, }
}
///|
pub fn ProductQueryPlan::where_predicate(
self : ProductQueryPlan,
predicate : ProductPredicate,
) -> ProductQueryPlan {
{ ..self, expression: Predicate(predicate) }
}
///|
pub fn ProductQueryPlan::sorted_by(
self : ProductQueryPlan,
sort : ProductSort,
descending? : Bool = false,
) -> ProductQueryPlan {
{ ..self, sort, descending }
}
///|
pub fn ProductQueryPlan::page(
self : ProductQueryPlan,
offset : Int,
limit : Int,
) -> ProductQueryPlan {
{
..self,
offset: if offset < 0 {
0
} else {
offset
},
limit: if limit < 0 {
0
} else {
limit
},
}
}
///|
pub fn ProductQueryPlan::including_empty_geometry(
self : ProductQueryPlan,
value : Bool,
) -> ProductQueryPlan {
{ ..self, include_empty_geometry: value }
}
///|
pub fn ProductQueryPlan::expression(
self : ProductQueryPlan,
) -> ProductExpression {
self.expression
}
///|
pub fn ProductQueryPlan::sort_key(self : ProductQueryPlan) -> ProductSort {
self.sort
}
///|
pub fn ProductQueryPlan::is_unbounded(self : ProductQueryPlan) -> Bool {
self.limit == 0
}
///|
pub fn ProductExpression::all(
expressions : Array[ProductExpression],
) -> ProductExpression {
if expressions.length() == 0 {
MatchAll
} else {
AllOf(expressions)
}
}
///|
pub fn ProductExpression::any(
expressions : Array[ProductExpression],
) -> ProductExpression {
if expressions.length() == 0 {
MatchNone
} else {
AnyOf(expressions)
}
}
///|
pub fn ProductExpression::not(self : ProductExpression) -> ProductExpression {
Not(self)
}
///|
pub fn ProductExpression::and_(
self : ProductExpression,
other : ProductExpression,
) -> ProductExpression {
match (self, other) {
(MatchNone, _) | (_, MatchNone) => MatchNone
(MatchAll, right) => right
(left, MatchAll) => left
(AllOf(left), AllOf(right)) => AllOf(left + right)
(AllOf(left), right) => AllOf(left + [right])
(left, AllOf(right)) => AllOf([left] + right)
(left, right) => AllOf([left, right])
}
}
///|
pub fn ProductExpression::or_(
self : ProductExpression,
other : ProductExpression,
) -> ProductExpression {
match (self, other) {
(MatchAll, _) | (_, MatchAll) => MatchAll
(MatchNone, right) => right
(left, MatchNone) => left
(AnyOf(left), AnyOf(right)) => AnyOf(left + right)
(AnyOf(left), right) => AnyOf(left + [right])
(left, AnyOf(right)) => AnyOf([left] + right)
(left, right) => AnyOf([left, right])
}
}
///|
fn query_contains(text : String, fragment : String) -> Bool {
text.to_lower().contains(fragment.to_lower())
}
///|
fn query_product_properties(
index : ModelIndex,
product : Product,
) -> Array[PropertySet] raise ParseError {
property_sets_for_product(index, product)
}
///|
fn query_product_has_property(
index : ModelIndex,
product : Product,
name : String,
) -> Bool raise ParseError {
property_named(query_product_properties(index, product), name) is Some(_)
}
///|
fn query_product_property_text(
index : ModelIndex,
product : Product,
name : String,
value : String,
) -> Bool raise ParseError {
match property_string(query_product_properties(index, product), name) {
Some(text) => query_contains(text, value)
None => false
}
}
///|
fn query_compare_number(
actual : Double,
comparison : NumberComparison,
expected : Double,
upper : Double?,
) -> Bool {
match comparison {
LessThan => actual < expected
LessOrEqual => actual <= expected
EqualTo => (actual - expected).abs() <= 0.0000001
GreaterOrEqual => actual >= expected
GreaterThan => actual > expected
Between => actual >= expected && actual <= upper.unwrap_or(expected)
}
}
///|
fn query_product_property_number(
index : ModelIndex,
product : Product,
name : String,
comparison : NumberComparison,
expected : Double,
upper : Double?,
) -> Bool raise ParseError {
match property_number(query_product_properties(index, product), name) {
Some(actual) => query_compare_number(actual, comparison, expected, upper)
None => false
}
}
///|
fn query_product_geometry_kind(
index : ModelIndex,
product : Product,
wanted : GeometryKind,
) -> Bool {
geometries_for_product(index, product).any(geometry => geometry.kind == wanted)
}
///|
fn query_product_quantity(
index : ModelIndex,
product : Product,
kind : QuantityKind,
) -> Double raise ParseError {
let row = quantity_for_product(index, product, default_takeoff_options())
row.value(kind)
}
///|
fn query_predicate(
index : ModelIndex,
product : Product,
predicate : ProductPredicate,
include_empty_geometry : Bool,
) -> Bool raise ParseError {
match predicate {
ProductKind(kind) => product.kind == kind
EntityType(name) => product.ifc_type.to_upper() == name.to_upper()
EntityTypePrefix(prefix) =>
product.ifc_type.to_upper().has_prefix(prefix.to_upper())
NameContains(fragment) =>
query_contains(product.name.unwrap_or(""), fragment)
DescriptionContains(fragment) =>
query_contains(product.description.unwrap_or(""), fragment)
GlobalId(value) => query_contains(product.global_id.unwrap_or(""), value)
Tag(value) => query_contains(product.tag.unwrap_or(""), value)
InContainer(id) => product.container == Some(id)
HasProperties => product.property_sets.length() > 0
HasGeometry => {
let geometries = geometries_for_product(index, product)
include_empty_geometry ||
geometries.any(geometry => {
geometry.points.length() > 0 ||
geometry.triangle_count > 0 ||
geometry.depth is Some(_)
})
}
PropertyExists(name) => query_product_has_property(index, product, name)
PropertyText(name, value) =>
query_product_property_text(index, product, name, value)
PropertyNumber(name, comparison, expected, upper) =>
query_product_property_number(
index, product, name, comparison, expected, upper,
)
IdRange(first, last) => product.id >= first && product.id <= last
GeometryKind(kind) => query_product_geometry_kind(index, product, kind)
Quantity(kind, comparison, expected, upper) =>
query_compare_number(
query_product_quantity(index, product, kind),
comparison,
expected,
upper,
)
}
}
///|
fn query_expression(
index : ModelIndex,
product : Product,
expression : ProductExpression,
include_empty_geometry : Bool,
) -> Bool raise ParseError {
match expression {
MatchAll => true
MatchNone => false
Predicate(predicate) =>
query_predicate(index, product, predicate, include_empty_geometry)
Not(inner) =>
!query_expression(index, product, inner, include_empty_geometry)
AllOf(expressions) => {
for expression in expressions {
if !query_expression(index, product, expression, include_empty_geometry) {
return false
}
}
true
}
AnyOf(expressions) => {
for expression in expressions {
if query_expression(index, product, expression, include_empty_geometry) {
return true
}
}
false
}
}
}
///|
fn query_kind_sort(product : Product) -> String {
product_kind_name(product.kind)
}
///|
fn query_product_name_sort(product : Product) -> String {
product.name.unwrap_or("").to_lower()
}
///|
fn query_product_sort_value(
index : ModelIndex,
product : Product,
sort : ProductSort,
) -> String raise ParseError {
match sort {
ById => product.id.to_string()
ByType => product.ifc_type.to_upper()
ByName => query_product_name_sort(product)
ByKind => query_kind_sort(product)
ByQuantity(kind) => query_product_quantity(index, product, kind).to_string()
ByDistance(point) => {
let bounds = model_bounds(index, [product])
box_distance(bounds, { min: point, max: point, empty: false }).to_string()
}
}
}
///|
fn query_product_precedes(
index : ModelIndex,
left : Product,
right : Product,
sort : ProductSort,
descending : Bool,
) -> Bool raise ParseError {
let left_value = query_product_sort_value(index, left, sort)
let right_value = query_product_sort_value(index, right, sort)
if left_value == right_value {
left.id < right.id
} else if descending {
left_value > right_value
} else {
left_value < right_value
}
}
///|
fn query_sort_products(
index : ModelIndex,
products : Array[Product],
plan : ProductQueryPlan,
) -> Array[Product] raise ParseError {
let result = products.copy()
for i in 0.. ProductQueryPage raise ParseError {
let index = model.index()
let matched : Array[Product] = []
let mut examined = 0
for product in model.products() {
examined += 1
if query_expression(
index,
product,
plan.expression,
plan.include_empty_geometry,
) {
matched.push(product)
}
}
let sorted = query_sort_products(index, matched, plan)
let total = sorted.length()
let offset = if plan.offset > total { total } else { plan.offset }
let limit = if plan.limit == 0 { total - offset } else { plan.limit }
let end = if offset + limit > total { total } else { offset + limit }
let products = if end <= offset { [] } else { sorted[offset:end].to_owned() }
{
products,
total_matches: total,
offset,
limit,
has_next: end < total,
examined,
}
}
///|
pub fn query_plan(model : Model) -> ProductQueryPlan {
ignore(model)
ProductQueryPlan::new()
}
///|
pub fn ProductQueryPage::is_empty(self : ProductQueryPage) -> Bool {
self.products.length() == 0
}
///|
pub fn ProductQueryPage::count(self : ProductQueryPage) -> Int {
self.products.length()
}
///|
pub fn ProductQueryPage::ids(self : ProductQueryPage) -> Array[Int] {
self.products.map(product => product.id)
}
///|
pub fn ProductQueryPage::next_offset(self : ProductQueryPage) -> Int? {
if self.has_next {
Some(self.offset + self.limit)
} else {
None
}
}
///|
pub fn ProductQueryPage::headline(self : ProductQueryPage) -> String {
"matches=" +
self.total_matches.to_string() +
" returned=" +
self.products.length().to_string() +
" offset=" +
self.offset.to_string() +
" has_next=" +
self.has_next.to_string()
}
///|
pub fn ProductQueryPage::to_json(self : ProductQueryPage) -> String {
"{\"total_matches\":" +
self.total_matches.to_string() +
",\"offset\":" +
self.offset.to_string() +
",\"limit\":" +
self.limit.to_string() +
",\"has_next\":" +
self.has_next.to_string() +
",\"examined\":" +
self.examined.to_string() +
",\"products\":[" +
self.products.map(product => product_to_json(product)).join(",") +
"]}"
}
///|
fn query_predicate_text(predicate : ProductPredicate) -> String {
match predicate {
ProductKind(kind) => "kind=" + product_kind_name(kind)
EntityType(name) => "type=" + name
EntityTypePrefix(prefix) => "type_prefix=" + prefix
NameContains(value) => "name~" + value
DescriptionContains(value) => "description~" + value
GlobalId(value) => "global_id=" + value
Tag(value) => "tag=" + value
InContainer(id) => "container=#" + id.to_string()
HasProperties => "has_properties"
HasGeometry => "has_geometry"
PropertyExists(name) => "property=" + name
PropertyText(name, value) => "property_text=" + name + "~" + value
PropertyNumber(name, comparison, expected, upper) => {
let upper_text = match upper {
Some(value) => ".." + value.to_string()
None => ""
}
"property_number=" +
name +
" " +
query_number_comparison_text(comparison) +
" " +
expected.to_string() +
upper_text
}
IdRange(first, last) => "id=" + first.to_string() + ".." + last.to_string()
GeometryKind(kind) => "geometry=" + geometry_kind_text(kind)
Quantity(kind, comparison, expected, upper) => {
let upper_text = match upper {
Some(value) => ".." + value.to_string()
None => ""
}
"quantity=" +
quantity_kind_name(kind) +
" " +
query_number_comparison_text(comparison) +
" " +
expected.to_string() +
upper_text
}
}
}
///|
fn query_number_comparison_text(comparison : NumberComparison) -> String {
match comparison {
LessThan => "<"
LessOrEqual => "<="
EqualTo => "="
GreaterOrEqual => ">="
GreaterThan => ">"
Between => "between"
}
}
///|
fn geometry_kind_text(kind : GeometryKind) -> String {
match kind {
Extrusion => "extrusion"
FacetedBrep => "faceted_brep"
TriangulatedMesh => "triangulated_mesh"
CsgSolid => "csg_solid"
Profile => "profile"
Polyline => "polyline"
PointCloud => "point_cloud"
Unknown => "unknown"
}
}
///|
fn query_expression_text(expression : ProductExpression) -> String {
match expression {
MatchAll => "all"
MatchNone => "none"
Predicate(predicate) => query_predicate_text(predicate)
Not(inner) => "not(" + query_expression_text(inner) + ")"
AllOf(values) =>
"all(" +
values.map(value => query_expression_text(value)).join(", ") +
")"
AnyOf(values) =>
"any(" +
values.map(value => query_expression_text(value)).join(", ") +
")"
}
}
///|
pub fn ProductQueryPlan::explain(self : ProductQueryPlan) -> String {
"filter=" +
query_expression_text(self.expression) +
" offset=" +
self.offset.to_string() +
" limit=" +
self.limit.to_string() +
" descending=" +
self.descending.to_string()
}
///|
pub fn ProductExpression::describe(self : ProductExpression) -> String {
query_expression_text(self)
}
///|
pub fn ProductPredicate::describe(self : ProductPredicate) -> String {
query_predicate_text(self)
}
///|
pub fn ProductQueryPlan::execute(
self : ProductQueryPlan,
model : Model,
) -> ProductQueryPage raise ParseError {
execute_query_plan(model, self)
}
///|
pub fn query_kind(
model : Model,
kind : ProductKind,
) -> Array[Product] raise ParseError {
ProductQueryPlan::new().where_predicate(ProductKind(kind)).execute(model).products
}
///|
pub fn query_by_name(
model : Model,
name : String,
) -> Array[Product] raise ParseError {
ProductQueryPlan::new().where_predicate(NameContains(name)).execute(model).products
}
///|
pub fn query_property(
model : Model,
name : String,
value : String,
) -> Array[Product] raise ParseError {
ProductQueryPlan::new()
.where_predicate(PropertyText(name, value))
.execute(model).products
}
///|
pub fn query_geometry(
model : Model,
kind : GeometryKind,
) -> Array[Product] raise ParseError {
ProductQueryPlan::new().where_predicate(GeometryKind(kind)).execute(model).products
}
///|
pub fn query_plan_report(
model : Model,
plan : ProductQueryPlan,
) -> Array[String] raise ParseError {
let page = plan.execute(model)
[plan.explain(), page.headline()]
}