# IfcOpenShell - IFC toolkit and geometry engine # Copyright (C) 2021 Dion Moult # # This file is part of IfcOpenShell. # # IfcOpenShell is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # IfcOpenShell is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public License # along with IfcOpenShell. If not, see . import ifcopenshell.util import ifcopenshell.util.fm import ifcopenshell.util.element import lark class Selector: @classmethod def parse(cls, ifc_file, query, elements=None): cls.file = ifc_file cls.elements = elements l = lark.Lark( """start: query (lfunction query)* query: selector | group group: "(" query (lfunction query)* ")" selector: (inverse_relationship)? guid_selector | (inverse_relationship)? class_selector guid_selector: "#" /[0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_$]{22}/ class_selector: "." WORD filter ? filter: "[" filter_key (comparison filter_value)? "]" filter_key: WORD | pset_or_qto filter_value: ESCAPED_STRING | SIGNED_FLOAT | SIGNED_INT | BOOLEAN | NULL pset_or_qto: /[A-Za-z0-9_]+/ "." /[A-Za-z0-9_]+/ lfunction: and | or inverse_relationship: types | decomposed_by | bounded_by types: "*" decomposed_by: "@" bounded_by: "@@" and: "&" or: "|" not: "!" comparison: (not)* (oneof | contains | morethanequalto | lessthanequalto | equal | morethan | lessthan) oneof: "%=" contains: "*=" morethanequalto: ">=" lessthanequalto: "<=" equal: "=" morethan: ">" lessthan: "<" BOOLEAN: "TRUE" | "FALSE" | "true" | "false"| "True" | "False" NULL: "NULL" // 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 /(? 1 and class_selector.children[1].data == "filter": return cls.filter_elements(elements, class_selector.children[1]) return elements @classmethod def filter_elements(cls, elements, filter_rule): results = [] key = filter_rule.children[0].children[0] if not isinstance(key, str): key = key.children[0] + "." + key.children[1] comparison = value = None if len(filter_rule.children) > 1: comparison = filter_rule.children[1].children[0].data if comparison == "not": comparison += filter_rule.children[1].children[1].data token_type = filter_rule.children[2].children[0].type if token_type == "ESCAPED_STRING": value = str(filter_rule.children[2].children[0][1:-1]) elif token_type == "SIGNED_INT": value = int(filter_rule.children[2].children[0]) elif token_type == "SIGNED_FLOAT": value = float(filter_rule.children[2].children[0]) elif token_type == "BOOLEAN": value = filter_rule.children[2].children[0].lower() == 'true' elif token_type == "NULL": value = None for element in elements: element_value = cls.get_element_value(element, key) if element_value is None and value is not None: continue if comparison and cls.filter_element(element, element_value, comparison, value): results.append(element) elif not comparison and element_value: results.append(element) return results @classmethod def get_element_value(cls, element, key): if "." in key and key.split(".")[0] == "type": try: element = ifcopenshell.util.element.get_type(element) if not element: return None except: return key = ".".join(key.split(".")[1:]) elif "." in key and key.split(".")[0] == "material": try: element = ifcopenshell.util.element.get_material(element, should_skip_usage=True) if not element: return None except: return key = ".".join(key.split(".")[1:]) elif "." in key and key.split(".")[0] == "container": try: element = ifcopenshell.util.element.get_container(element) if not element: return None except: return key = ".".join(key.split(".")[1:]) info = element.get_info() if key in info: return info[key] elif "." in key: key_components = key.split(".") pset_name = key_components[0] prop = ".".join(key_components[1:]) psets = ifcopenshell.util.element.get_psets(element) if pset_name in psets and prop in psets[pset_name]: return psets[pset_name][prop] @classmethod def filter_element(cls, element, element_value, comparison, value): if comparison.startswith("not"): return not cls.filter_element(element, element_value, comparison[3:], value) elif comparison == "equal": return element_value == value elif comparison == "contains": return value in str(element_value) elif comparison == "morethan": return element_value > value elif comparison == "lessthan": return element_value < value elif comparison == "morethanequalto": return element_value >= value elif comparison == "lessthanequalto": return element_value <= value elif comparison == "oneof": return element_value in value.split(",") return False @classmethod def get_guid_selector(cls, guid_selector): return [cls.file.by_id(guid_selector.children[0])]