mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-09-18 06:21:40 +00:00
New facet based selector syntax. Proposed to supersede existing selector syntax.
This commit is contained in:
@@ -21,6 +21,7 @@ import lark
|
|||||||
import ifcopenshell.util
|
import ifcopenshell.util
|
||||||
import ifcopenshell.util.fm
|
import ifcopenshell.util.fm
|
||||||
import ifcopenshell.util.element
|
import ifcopenshell.util.element
|
||||||
|
import ifcopenshell.util.classification
|
||||||
|
|
||||||
|
|
||||||
def get_element_value(element, query):
|
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"])
|
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:
|
class Selector:
|
||||||
@classmethod
|
@classmethod
|
||||||
def parse(cls, ifc_file, query, elements=None):
|
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
|
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):
|
class TestSelector(test.bootstrap.IFC4):
|
||||||
def test_selecting_by_class(self):
|
def test_selecting_by_class(self):
|
||||||
element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall")
|
element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall")
|
||||||
|
|||||||
Reference in New Issue
Block a user