# 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 re import lark import ifcopenshell.util import ifcopenshell.util.fm import ifcopenshell.util.element import ifcopenshell.util.classification filter_elements_grammar = lark.Lark("""start: filter_group filter_group: facet_list ("+" facet_list)* facet_list: facet ("," facet)* facet: instance | entity | attribute | type | material | property | classification | location instance: not? globalid globalid: /[0-3][a-zA-Z0-9_$]{21}/ 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 /(?!%*\\]]*/ ("." /[^\\W][^.=<>!%*\\]]*/)* // Embed common.lark for packaging _STRING_INNER: /.*?/ _STRING_ESC_INNER: _STRING_INNER /(?!%*\\]]*/ ("." /[^\\W][^.=<>!%*\\]]*/)* lfunction: and | or inverse_relationship: types | decomposed_by | bounded_by | grouped_by types: "*" decomposed_by: "@" bounded_by: "@@" grouped_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 = [] filter_query = cls.parse_filter_query(filter_rule.children[0].children[0]) 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 filter_value = filter_rule.children[2].children[0] if isinstance(filter_value, lark.Tree): is_regex = True token_type = filter_value.data else: is_regex = False token_type = filter_value.type if token_type == "filter_regex": value = str(filter_value.children[0][1:-1]) elif token_type == "ESCAPED_STRING": value = str(filter_value[1:-1]) elif token_type == "SIGNED_INT": value = int(filter_value) elif token_type == "SIGNED_FLOAT": value = float(filter_value) elif token_type == "BOOLEAN": value = filter_value.lower() == "true" elif token_type == "NULL": value = None for element in elements: element_value = cls.get_element_value(element, filter_query["keys"], is_regex=filter_query["is_regex"]) if element_value is None and value is not None and "not" not in comparison: continue if comparison and cls.filter_element(element, element_value, comparison, value, is_regex=is_regex): results.append(element) elif not comparison and element_value: results.append(element) return results @classmethod def parse_filter_query(cls, filter_query): keys = filter_query is_regex = False if isinstance(keys, str): keys = [keys] elif keys.data == "keys_regex": is_regex = True keys = [k[1:-1].replace('\\"', '"') for k in keys.children] elif keys.data == "keys_quoted": keys = [k[1:-1].replace('\\"', '"') for k in keys.children] elif keys.data == "keys_simple": keys = keys.children return {"keys": keys, "is_regex": is_regex} @classmethod def get_element_value(cls, element, keys, is_regex=False): value = element for key in keys: key = key.strip() if value is None: return if key == "type": value = ifcopenshell.util.element.get_type(value) elif key in ("material", "mat"): value = ifcopenshell.util.element.get_material(value, should_skip_usage=True) elif key in ("materials", "mats"): value = ifcopenshell.util.element.get_materials(value) elif key == "styles": value = ifcopenshell.util.element.get_styles(value) elif key in ("item", "i"): if value.is_a("IfcMaterialLayerSet"): value = value.MaterialLayers elif value.is_a("IfcMaterialProfileSet"): value = value.MaterialProfiles elif value.is_a("IfcMaterialConstituentSet"): value = value.MaterialConstituents elif key == "container": value = ifcopenshell.util.element.get_container(value) elif key == "class": value = value.is_a() elif key == "id": value = value.id() elif isinstance(value, ifcopenshell.entity_instance): if key == "Name" and value.is_a("IfcMaterialLayerSet"): key = "LayerSetName" # This oddity in the IFC spec is annoying so we account for it. attribute = getattr(value, key, None) if attribute is not None: value = attribute else: # Try to extract pset if is_regex: psets = ifcopenshell.util.element.get_psets(value) matching_psets = [] for pset_name, pset in psets.items(): if re.match(key, pset_name): matching_psets.append(pset) result = matching_psets or None else: result = ifcopenshell.util.element.get_pset(value, key) value = result elif isinstance(value, dict): # Such as from the result of a prior get_pset if is_regex: results = [] for prop_name, prop_value in value.items(): if re.match(key, prop_name): if isinstance(prop_value, (list, tuple)): results.extend(prop_value) else: results.append(prop_value) value = results else: value = value.get(key, None) elif isinstance(value, (list, tuple)): # If we use regex if key.isnumeric(): try: value = value[int(key)] except IndexError: return else: results = [] for v in value: subvalue = cls.get_element_value(v, [key], is_regex=is_regex) if isinstance(subvalue, list): results.extend(subvalue) else: results.append(subvalue) value = results return value @classmethod def filter_element(cls, element, element_value, comparison, value, is_regex=False): if comparison.startswith("not"): return not cls.filter_element(element, element_value, comparison[3:], value, is_regex=is_regex) elif comparison == "equal" and isinstance(element_value, list): if is_regex: for element_v in element_value: if re.match(value, element_v): return True return False return value in element_value elif comparison == "equal": if is_regex: return bool(re.match(value, element_value)) return element_value == value elif comparison == "contains" and isinstance(element_value, list): return bool([ev for ev in element_value if value in str(ev)]) 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])]