New facet based selector syntax. Proposed to supersede existing selector syntax.

This commit is contained in:
Dion Moult
2023-07-20 19:22:15 +10:00
parent 46c5a5c820
commit 62c120e4bb
2 changed files with 354 additions and 0 deletions
@@ -21,6 +21,7 @@ import lark
import ifcopenshell.util
import ifcopenshell.util.fm
import ifcopenshell.util.element
import ifcopenshell.util.classification
def get_element_value(element, query):
@@ -48,6 +49,259 @@ def get_element_value(element, query):
return Selector.get_element_value(element, filter_query["keys"], filter_query["is_regex"])
def filter_elements(ifc_file, query, elements=None):
l = lark.Lark(
"""start: filter_group
filter_group: facet_list ("+" facet_list)*
facet_list: facet ("," facet)*
facet: entity | attribute | type | material | property | classification | location
entity: not? ifc_class
attribute: attribute_name comparison value
type: "type" comparison value
material: "material" comparison value
property: pset "." prop comparison value
classification: "classification" comparison value
location: "location" comparison value
pset: quoted_string | unquoted_string | regex_string
prop: quoted_string | unquoted_string | regex_string
attribute_name: /[A-Z]\\w+/
ifc_class: /Ifc\\w+/
value: special | quoted_string | unquoted_string | regex_string
unquoted_string: /[^.=\\s]+/
quoted_string: ESCAPED_STRING
regex_string: "/" /[^\\/]+/ "/"
special: null | true | false
comparison: not? equals
not: "!"
equals: "="
null: "NULL"
true: "TRUE"
false: "FALSE"
// Embed common.lark for packaging
DIGIT: "0".."9"
HEXDIGIT: "a".."f"|"A".."F"|DIGIT
INT: DIGIT+
SIGNED_INT: ["+"|"-"] INT
DECIMAL: INT "." INT? | "." INT
_EXP: ("e"|"E") SIGNED_INT
FLOAT: INT _EXP | DECIMAL _EXP?
SIGNED_FLOAT: ["+"|"-"] FLOAT
NUMBER: FLOAT | INT
SIGNED_NUMBER: ["+"|"-"] NUMBER
_STRING_INNER: /.*?/
_STRING_ESC_INNER: _STRING_INNER /(?<!\\\\)(\\\\\\\\)*?/
ESCAPED_STRING : "\\"" _STRING_ESC_INNER "\\""
LCASE_LETTER: "a".."z"
UCASE_LETTER: "A".."Z"
LETTER: UCASE_LETTER | LCASE_LETTER
WORD: LETTER+
CNAME: ("_"|LETTER) ("_"|LETTER|DIGIT)*
WS_INLINE: (" "|/\\t/)+
WS: /[ \\t\\f\\r\\n]/+
CR : /\\r/
LF : /\\n/
NEWLINE: (CR? LF)+
%ignore WS // Disregard spaces in text
"""
)
transformer = FacetTransformer(ifc_file, elements)
transformer.transform(l.parse(query))
return transformer.get_results()
return transformer.elements
class FacetTransformer(lark.Transformer):
def __init__(self, ifc_file, elements):
self.file = ifc_file
self.results = []
self.elements = elements or set()
self.container_parents = {}
self.container_trees = {}
print("INIT TRANSFORMER")
def get_results(self):
results = set()
for r in self.results:
results |= r
return results
def facet_list(self, args):
if self.elements:
self.results.append(self.elements)
self.elements = set()
def entity(self, args):
if args[0].data == "ifc_class":
self.elements |= set(self.file.by_type(args[0].children[0].value))
else:
self.elements -= set(self.file.by_type(args[1].children[0].value))
def attribute(self, args):
name, comparison, value = args
name = name.children[0].value
def filter_function(element):
element_value = getattr(element, name, None)
return self.compare(element_value, comparison, value)
self.elements = set(filter(filter_function, self.elements))
def type(self, args):
comparison, value = args
def filter_function(element):
element_value = getattr(ifcopenshell.util.element.get_type(element), "Name", None)
return self.compare(element_value, comparison, value)
self.elements = set(filter(filter_function, self.elements))
def material(self, args):
comparison, value = args
def filter_function(element):
materials = ifcopenshell.util.element.get_materials(element)
result = False if materials else None
for material in materials:
if self.compare(material.Name, comparison, value):
result = True
if self.compare(getattr(material, "Category", None), comparison, value):
result = True
if result is not None:
return result if comparison == "=" else not result
return self.compare(None, comparison, value)
self.elements = set(filter(filter_function, self.elements))
def property(self, args):
pset, prop, comparison, value = args
def filter_function(element):
if isinstance(pset, str) and isinstance(prop, str):
element_value = ifcopenshell.util.element.get_pset(element, pset, prop)
return self.compare(element_value, comparison, value)
elif isinstance(pset, str) and isinstance(prop, re.Pattern):
element_props = ifcopenshell.util.element.get_pset(element, pset) or {}
for element_prop, element_value in element_props.items():
if prop.match(element_prop):
return self.compare(element_value, comparison, value)
elif isinstance(pset, re.Pattern):
element_psets = ifcopenshell.util.element.get_psets(element)
for element_pset, element_props in element_psets.items():
if not pset.match(element_pset):
continue
if isinstance(prop, str):
element_value = element_props.get(prop, None)
if element_value is not None:
return self.compare(element_value, comparison, value)
elif isinstance(prop, re.Pattern):
for element_prop, element_value in element_props.items():
if prop.match(element_prop):
return self.compare(element_value, comparison, value)
return self.compare(None, comparison, value)
self.elements = set(filter(filter_function, self.elements))
def classification(self, args):
comparison, value = args
def filter_function(element):
references = ifcopenshell.util.classification.get_references(element)
result = False if references else None
for reference in references:
if self.compare(reference.Name, comparison, value):
result = True
if self.compare(
getattr(reference, "Identification", getattr(reference, "ItemReference", None)), comparison, value
):
result = True
if result is not None:
return result if comparison == "=" else not result
return self.compare(None, comparison, value)
self.elements = set(filter(filter_function, self.elements))
def location(self, args):
comparison, value = args
def filter_function(element):
container = ifcopenshell.util.element.get_container(element)
containers = self.get_container_tree(container)
result = False if containers else None
for container in containers:
if self.compare(container.Name, comparison, value):
result = True
if result is not None:
return result if comparison == "=" else not result
return self.compare(None, comparison, value)
self.elements = set(filter(filter_function, self.elements))
def get_container_tree(self, container):
tree = self.container_trees.get(container, None)
if tree:
return tree
tree = []
while container:
if container.is_a("IfcProject"):
break
tree.append(container)
container = ifcopenshell.util.element.get_aggregate(container)
tree_copy = tree.copy()
while tree_copy:
self.container_trees[tree_copy.pop(0)] = tree_copy.copy()
return tree
def comparison(self, args):
return "=" if args[0].data == "equals" else "!="
def pset(self, args):
return self.value(args)
def prop(self, args):
return self.value(args)
def value(self, args):
if args[0].data == "unquoted_string":
return args[0].children[0].value
elif args[0].data == "quoted_string":
return args[0].children[0].value[1:-1].replace('\\"', '"')
elif args[0].data == "regex_string":
return re.compile(args[0].children[0].value)
elif args[0].data == "special":
if args[0].children[0].data == "null":
return None
elif args[0].children[0].data == "true":
return True
elif args[0].children[0].data == "false":
return False
def compare(self, element_value, comparison, value):
if isinstance(value, str):
if isinstance(element_value, int):
value = int(value)
elif isinstance(element_value, float):
value = float(value)
result = element_value == value
elif isinstance(value, re.Pattern):
result = bool(value.match(element_value))
elif value in (None, True, False):
result = element_value is value
return result if comparison == "=" else not result
class Selector:
@classmethod
def parse(cls, ifc_file, query, elements=None):
@@ -74,6 +74,106 @@ class TestGetElementValue(test.bootstrap.IFC4):
assert subject.get_element_value(element, "material.item.Name.1") is None
class TestFilterElements(test.bootstrap.IFC4):
def test_selecting_by_class(self):
element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall")
element.Name = "Foo"
element2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcSlab")
assert subject.filter_elements(self.file, "IfcWall") == {element}
assert subject.filter_elements(self.file, "IfcElement, ! IfcWall") == {element2}
def test_selecting_by_attribute(self):
element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall")
element.Name = "Foo"
element2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall")
element2.Name = "Bar"
ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcSlab")
assert subject.filter_elements(self.file, "IfcWall, Name=Foo") == {element}
element.Name = 'Foo\'s "quoted" name...'
assert subject.filter_elements(self.file, 'IfcWall, Name="Foo\'s \\"quoted\\" name..."') == {element}
assert subject.filter_elements(self.file, "IfcWall, Name=/Fo.*/") == {element}
assert subject.filter_elements(self.file, "IfcWall, Description=NULL") == {element, element2}
def test_selecting_by_type(self):
element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall")
element_type = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWallType")
ifcopenshell.api.run("type.assign_type", self.file, related_object=element, relating_type=element_type)
assert subject.filter_elements(self.file, "IfcWall, type=Foo") == set()
element_type.Name = "Foo"
assert subject.filter_elements(self.file, "IfcWall, type=Foo") == {element}
assert subject.filter_elements(self.file, 'IfcWall, type="Foo"') == {element}
assert subject.filter_elements(self.file, "IfcWall, type=/Fo.*/") == {element}
def test_selecting_by_material(self):
element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall")
element2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall")
assert subject.filter_elements(self.file, "IfcWall, material=NULL") == {element, element2}
material = ifcopenshell.api.run("material.add_material", self.file, name="CON01")
ifcopenshell.api.run("material.assign_material", self.file, product=element, material=material)
assert subject.filter_elements(self.file, "IfcWall, material=CON01") == {element}
assert subject.filter_elements(self.file, "IfcWall, material!=CON01") == {element2}
def test_selecting_by_property(self):
element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall")
element2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall")
pset = ifcopenshell.api.run("pset.add_pset", self.file, product=element, name="Foobar")
ifcopenshell.api.run("pset.edit_pset", self.file, pset=pset, properties={"Foo": "Bar"})
assert subject.filter_elements(self.file, "IfcWall, Foobar.Foo=Bar") == {element}
assert subject.filter_elements(self.file, 'IfcWall, Foobar."Foo"=Bar') == {element}
assert subject.filter_elements(self.file, "IfcWall, Foobar./Fo.*/=Bar") == {element}
assert subject.filter_elements(self.file, "IfcWall, Foobar./Fo.*/!=Bar") == {element2}
ifcopenshell.api.run("pset.edit_pset", self.file, pset=pset, properties={"Bar": False})
assert subject.filter_elements(self.file, "IfcWall, Foobar.Bar=FALSE") == {element}
ifcopenshell.api.run("pset.edit_pset", self.file, pset=pset, properties={"Baz": 123})
assert subject.filter_elements(self.file, "IfcWall, Foobar.Baz=123") == {element}
ifcopenshell.api.run("pset.edit_pset", self.file, pset=pset, properties={"Bay": 123.3})
def test_selecting_by_classification(self):
project = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcProject")
element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall")
element2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall")
assert subject.filter_elements(self.file, "IfcWall, classification=NULL") == {element, element2}
result = ifcopenshell.api.run("classification.add_classification", self.file, classification="Name")
ifcopenshell.api.run(
"classification.add_reference",
self.file,
product=element,
identification="X",
name="Foobar",
classification=result,
)
assert subject.filter_elements(self.file, "IfcWall, classification=NULL") == {element2}
assert subject.filter_elements(self.file, "IfcWall, classification=X") == {element}
assert subject.filter_elements(self.file, "IfcWall, classification=Foobar") == {element}
def test_selecting_by_location(self):
element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall")
element2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall")
space = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcSpace", name="Space")
storey = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcBuildingStorey", name="G")
building = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcBuilding", name="Building")
project = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcProject", name="Project")
ifcopenshell.api.run("spatial.assign_container", self.file, product=element, relating_structure=space)
ifcopenshell.api.run("spatial.assign_container", self.file, product=element2, relating_structure=storey)
ifcopenshell.api.run("aggregate.assign_object", self.file, product=space, relating_object=storey)
ifcopenshell.api.run("aggregate.assign_object", self.file, product=storey, relating_object=building)
ifcopenshell.api.run("aggregate.assign_object", self.file, product=building, relating_object=project)
assert subject.filter_elements(self.file, "IfcWall, location=NULL") == set()
assert subject.filter_elements(self.file, "IfcWall, location=Space") == {element}
assert subject.filter_elements(self.file, "IfcWall, location=G") == {element, element2}
assert subject.filter_elements(self.file, "IfcWall, location=Building") == {element, element2}
def test_selecting_multiple_filter_groups(self):
element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall")
element.Name = "Foo"
element2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcSlab")
element2.Name = "Bar"
assert subject.filter_elements(self.file, "IfcWall + IfcSlab") == {element, element2}
assert subject.filter_elements(self.file, "IfcWall, IfcSlab, Name=Foo") == {element}
assert subject.filter_elements(self.file, "IfcWall, Name=Foo + IfcSlab") == {element, element2}
assert subject.filter_elements(self.file, "IfcWall, Name=Foo + IfcSlab, Name=Bar") == {element, element2}
class TestSelector(test.bootstrap.IFC4):
def test_selecting_by_class(self):
element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall")