# IDS - Information Delivery Specification. # Copyright (C) 2021 Artur Tomczak , Thomas Krijnen # # 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 os import re import logging import numpy as np from datetime import date import ifcopenshell.util.element import ifcopenshell.util.placement from bcf.v2.bcfxml import BcfXml from bcf.v2 import data as bcf from xmlschema import XMLSchema from xmlschema import etree_tostring from xmlschema.validators import identities cwd = os.path.dirname(os.path.realpath(__file__)) ids_schema = XMLSchema(os.path.join(cwd, "ids.xsd")) # source: "http://standards.buildingsmart.org/IDS/ids_04.xsd" def error(msg): raise Exception(msg) class ids: """Represents the XML root node and its childNodes.""" def __init__( self, ifcversion=None, description=None, author=None, copyright=None, version=None, creation_date=None, purpose=None, milestone=None, ): """Create an IDS object. :param ifcversion: IFC schema version. If None, then schema independent. Options: '2.3.0.1'|'4.0.2.1'|'4.3.0.0'|None, defaults to None :type ifcversion: str, optional :param description:, defaults to None :type description: str, optional :param author: Email of the IDS author, defaults to None :type author: str, optional :param copyright:, defaults to None :type copyright: str, optional :param version: IDS file version, defaults to None :type version: float, optional :param creation_date: Date in 'yyyy-mm-dd' format, defaults to current date :type creation_date: str, optional :param purpose:, defaults to None :type purpose: str, optional :param milestone:, defaults to None :type milestone: str, optional """ self.specifications = [] self.info = {} if ifcversion: if ifcversion in ["2.3.0.1", "4.0.2.1", "4.3.0.0"]: self.info["ifcversion"] = ifcversion if author: if "@" in author: self.info["author"] = author if description: self.info["description"] = description if copyright: self.info["copyright"] = copyright if version: self.info["version"] = version if creation_date: if re.match(r"\d\d\d\d-\d\d-\d\d", creation_date): self.info["date"] = creation_date # date.fromisoformat(creation_date).isoformat() if "date" not in self.info: self.info["date"] = date.today().isoformat() if purpose: self.info["purpose"] = purpose if milestone: self.info["milestone"] = milestone def asdict(self): """Converts object to a dictionary, adding required attributes. :return: Xmlschema compliant dictionary. :rtype: dict """ ids_dict = { "@xmlns": "http://standards.buildingsmart.org/IDS", "@xmlns:xs": "http://www.w3.org/2001/XMLSchema", "@xmlns:xsi": "http://www.w3.org/2001/XMLSchema-instance", "@xsi:schemaLocation": "http://standards.buildingsmart.org/IDS/ids_04.xsd", "specification": [], "info": self.info, } for spec in self.specifications: ids_dict["specification"].append(spec.asdict()) return ids_dict def to_xml(self, filepath="./", ids_schema=ids_schema): """Save IDS object as .xml file. :param filepath: Path for the new file, defaults to "./" :type filepath: str, optional :param ids_schema: XML Schema for an IDS file, defaults to ids_schema object from buildingSMART :type ids_schema: XMLschema, optional :return: Result of the newly created file validation against the schema. :rtype: bool """ if filepath.endswith("/"): filepath = filepath + "IDS" if not filepath.endswith(".xml"): filepath = filepath + ".xml" ids_dict = self.asdict() ids_xml = ids_schema.encode( ids_dict, namespaces={ "": "http://standards.buildingsmart.org/IDS", "xs": "http://www.w3.org/2001/XMLSchema", "xsi": "http://www.w3.org/2001/XMLSchema-instance", "xsi:schemaLocation": "http://standards.buildingsmart.org/IDS/ids_04.xsd", }, ) # validation='skip', ids_str = etree_tostring( ids_xml, namespaces={ "": "http://standards.buildingsmart.org/IDS", # 'xs': 'http://www.w3.org/2001/XMLSchema', # 'xsi': 'http://www.w3.org/2001/XMLSchema-instance', # 'xsi:schemaLocation': "http://standards.buildingsmart.org/IDS/ids_04.xsd" }, ) with open(filepath, "w") as f: f.write('\n') f.write("\n") f.write(ids_str) f.close() # ids_schema.validate(filepath) return ids_schema.is_valid(filepath) @staticmethod def open(filepath, ids_schema=ids_schema): """Use to open ids.xml files :param filepath: ids file path :type filepath: str :param ids_schema: XML Schema for an IDS file, defaults to ids_schema object from buildingSMART :type ids_schema: XMLschema, optional :return: IDS file as a python object :rtype: ids object """ ids_schema.validate(filepath) ids_content = ids_schema.decode( filepath, strip_namespaces=True, namespaces={"": "http://standards.buildingsmart.org/IDS"} ) ids_file = ids() ids_file.specifications = [specification.parse(s) for s in ids_content["specification"]] return ids_file def validate(self, ifc_file, logger=None): """Use to validate IFC model against IDS specifications. :param ifc_file: path to ifc file :type ifc_file: str :param logger: Logging object with handlers, defaults to None :type logger: logging, optional """ if not isinstance(logger, logging.Logger): logger = logging.getLogger("IDS_Logger") logging.basicConfig(level=logging.INFO, format="%(message)s") logger.setLevel(logging.INFO) if "ifcversion" in self.info.keys(): if self.info["ifcversion"] in ["2.3.0.1", "4.0.2.1", "4.3.0.0"]: if self.info["ifcversion"][0:3] == "2.3": if not ifc_file.schema.startswith("IFC2x3"): logger.error("IFC file is of %s not of %s schema." % (ifc_file.schema, self.info["ifcversion"])) elif self.info["ifcversion"][0:3] == "4.0": if not ifc_file.schema == "IFC4": logger.error("IFC file is of %s not of %s schema." % (ifc_file.schema, self.info["ifcversion"])) elif self.info["ifcversion"][0:3] == "4.3": if not ifc_file.schema.startswith("IFC4x3"): logger.error("IFC file is of %s not of %s schema." % (ifc_file.schema, self.info["ifcversion"])) else: logger.error("IFC version not recognized") # Consider other way around: for elem, for spec so we can see if an element pass all IDSes? for spec in self.specifications: self.ifc_applicable = 0 self.ifc_passed = 0 for elem in ifc_file.by_type("IfcObject"): apply, comply = spec(elem, logger) if apply: self.ifc_applicable += 1 if comply: self.ifc_passed += 1 if self.ifc_applicable == 0: if spec.necessity == "required": logger.error("No applicable elements found. Minimum 1 applicable element required.") else: logger.debug("No applicable elements found. None required.") try: percentage = self.ifc_passed / self.ifc_applicable * 100 except ZeroDivisionError: percentage = 0 logger.debug( "Out of %s IFC elements, %s were applicable and %s of them passed (%s)." % ( len(ifc_file.by_type("IfcProduct")), self.ifc_applicable, self.ifc_passed, str(percentage) + "%", ) ) for h in logger.handlers: h.flush() class specification: """Represents the XML node and its two children and """ def __init__(self, name="Specification", necessity="required"): """Create a specification to be added in ids. :param name:, defaults to "Specification" :type name: str, optional :param necessity: 'required'|'optional', defaults to "required" :type necessity: str, optional """ self.name = name self.applicability = None self.requirements = None self.necessity = necessity def asdict(self): """Converts object to a dictionary, adding required attributes. :return: Xmlschema compliant dictionary. :rtype: dict """ # if older python collections.OrderedDict() spec_dict = { "@name": self.name, "@necessity": self.necessity, "applicability": {}, "requirements": {}, } for x in ["applicability", "requirements"]: for fac in (getattr(self, x)).terms: fclass = type(fac).__name__ if fclass in spec_dict[x]: spec_dict[x][fclass].append(fac.asdict()) else: spec_dict[x][fclass] = [fac.asdict()] return spec_dict @staticmethod def parse(ids_dict): """Parse xml specification to python object. :param ids_dict: :type ids_dict: dict """ def parse_rules(dict): facet_names = list(dict.keys()) facet_properties = [v[0] if isinstance(v, list) else v for v in list(dict.values())] classes = [meta_facet.facets.__getitem__(f) for f in facet_names] facets = [cls(n) for cls, n in zip(classes, facet_properties)] return facets spec = specification() spec.name = ids_dict["@name"] spec.necessity = ids_dict["@necessity"] spec.applicability = boolean_and(parse_rules(ids_dict["applicability"])) spec.requirements = boolean_and(parse_rules(ids_dict["requirements"])) return spec def add_applicability(self, facet): """Applicability specifies what conditions must be meet for an IFC object to be used for validation. Note, that at least one entity facet is required. :param facet: any of entity|classification|property|material :type facet: facet Example:: i = ids.ids() i.specifications.append(ids.specification(name="Test_Specification")) e = ids.entity.create(name="Test_Name", predefinedtype="Test_PredefinedType") i.specifications[0].add_applicability(e) """ if self.applicability: self.applicability = boolean_and(self.applicability.terms + [facet]) else: self.applicability = boolean_and([facet]) def add_requirement(self, facet): """Requirement is validated on all applicable IFC elements. Note, that at least one facet of any type is required. :param facet: any of entity|classification|property|material :type facet: facet """ if self.requirements: self.requirements = boolean_and(self.requirements.terms + [facet]) else: self.requirements = boolean_and([facet]) def __call__(self, inst, logger): """When specification is called on an ifc instance, it validates against applicability and requirements. :param inst: IFC entity element :type inst: IFC entity :param logger: Logging object :type logger: logging :return: results of validation on applicability and requirements :rtype: [bool,bool] """ if self.applicability(inst, logger): valid = self.requirements(inst, logger) if valid: logger.info( { "guid": inst.GlobalId, "result": valid.success, "sentence": str(self) + ".\n" + inst.is_a() + " '" + str(inst.Name) + "' (#" + str(inst.id()) + ") has " + str(valid) + " so is compliant", "ifc_element": inst, } ) return True, True else: # BUG "has does not have" logger.error( { "guid": inst.GlobalId, "result": valid.success, "sentence": str(self) + ".\n" + inst.is_a() + " '" + str(inst.Name) + "' (#" + str(inst.id()) + ") has " + str(valid) + " so is not compliant", "ifc_element": inst, } ) return True, False else: return False, False def __str__(self): """Represent the specification in human readible sentence. :return: sentence :rtype: str """ return "Given an instance with %(applicability)s\nWe expect %(requirements)s" % self.__dict__ class facet_evaluation: """The evaluation of a facet with data from IFC. Converts to bool and has a human readable string format.""" def __init__(self, success, str): self.success = success self.str = str def __bool__(self): return self.success def __str__(self): return self.str class meta_facet(type): """A metaclass for automatically registering facets in a map to be instantiated based on XML tagnames.""" facets = {} def __new__(cls, clsname, bases, attrs): newclass = super(meta_facet, cls).__new__(cls, clsname, bases, attrs) meta_facet.facets[clsname] = newclass return newclass class facet(metaclass=meta_facet): """ The base class for IDS facets. IDS facets are functors constructed from XML nodes that return True or False. A getattr method is provided for conveniently extracting XML child node text content. Use child classes instead: entity, classification, property and material. """ def __init__(self, node=None, location=None): if node: self.node = node if "@location" in self: self.location = self.node["@location"] else: self.location = "any" if location: self.location = location else: self.location = "any" def __getattr__(self, k): if k in self.node: v = self.node[k] # BUG list of dictionaries should not happen if isinstance(v, list): v = v[0] if "simpleValue" in list(v): return v["simpleValue"] elif "restriction" in list(v): return restriction.parse(v["restriction"][0]) # TODO handle more than one restriction: return [restriction(r) for r in v["restriction"]] else: raise Exception("Unknown value declaration.") else: return None def __iter__(self): for k in self.parameters: yield k, getattr(self, k) def __str__(self): di = dict(list(self)) for k, v in di.items(): if isinstance(v, str) and not len(v): di[k] = "not specified" return self.message % di class entity(facet): """The IDS entity facet currently *with* inheritance""" parameters = ["name", "predefinedtype"] @staticmethod def create(name=None, predefinedtype=None): """Create an entity facet that can be added to applicability or requirements of IDS specification. :param name: IFC entity name that is required. e.g. IfcWall, defaults to None :type name: str, optional :param predefinedtype: name of the predefined type, defaults to None :type predefinedtype: str, optional :return: entity object :rtype: entity """ inst = entity() inst.name = name inst.predefinedtype = predefinedtype return inst def asdict(self): """Converts object to a dictionary, adding required attributes. :return: Xmlschema compliant dictionary. :rtype: dict """ fac_dict = {"name": parameter_asdict(self.name)} try: fac_dict["predefinedtype"] = parameter_asdict(self.predefinedtype) except (RecursionError, UnboundLocalError) as e: print(e) return fac_dict def __call__(self, inst, logger): """Validate an ifc instance against that entity facet. :param inst: IFC entity element :type inst: IFC entity :param logger: Logging object :type logger: logging :return: result of the validation as bool and message :rtype: facet_evaluation(bool, str) """ # @nb with inheritance if self.predefinedtype and hasattr(inst, "PredefinedType"): self.message = "an entity name '%(name)s' of predefined type '%(predefinedtype)s'" return facet_evaluation( inst.is_a(self.name) and inst.PredefinedType == self.predefinedtype, self.message % {"name": inst.is_a(), "predefinedtype": inst.PredefinedType}, ) else: self.message = "an entity name '%(name)s'" return facet_evaluation(inst.is_a(self.name), self.message % {"name": inst.is_a()}) class classification(facet): """ The IDS classification facet by traversing the HasAssociations inverse attribute """ parameters = ["system", "value", "location"] message = "%(location)sclassification reference %(value)s from '%(system)s'" @staticmethod def create(location="any", value=None, system=None): """Create a classification facet that can be added to applicability or requirements of IDS specification. :param location: Define where to check for the parameter. One of "any"|"instance"|"type", defaults to "any" :type location: str, optional :param value: Value that is required. Could be alphanumeric or restriction object, defaults to None :type value: restriction|alphanumeric, optional :param system: System that is required. Could be alphanumeric or restriction object, defaults to None :type system: restriction|alphanumeric, optional :return: classification object :rtype: classification """ inst = classification() inst.location = location inst.value = value inst.system = system return inst def asdict(self): """Converts object to a dictionary, adding required attributes. :return: Xmlschema compliant dictionary. :rtype: dict """ fac_dict = { "value": parameter_asdict(self.value), "system": parameter_asdict(self.system), "@location": self.location, # "instructions": "SAMPLE_INSTRUCTIONS", } return fac_dict def __call__(self, inst, logger): """Validate an ifc instance against that classification facet. :param inst: IFC entity element :type inst: IFC entity :param logger: Logging object :type logger: logging :return: result of the validation as bool and message :rtype: facet_evaluation(bool, str) """ instance_classiciations = inst.HasAssociations if ifcopenshell.util.element.get_type(inst): type_classifications = ifcopenshell.util.element.get_type(inst).HasAssociations else: type_classifications = () if self.location == "instance" and instance_classiciations: associations = instance_classiciations elif self.location == "type" and type_classifications: associations = type_classifications elif self.location == "any" and (instance_classiciations or type_classifications): associations = instance_classiciations + type_classifications else: associations = () refs = [] for association in associations: if association.is_a("IfcRelAssociatesClassification"): cref = association.RelatingClassification if hasattr(cref, "ItemReference"): # IFC2x3 refs.append((cref.ReferencedSource.Name, cref.ItemReference)) elif hasattr(cref, "Identification"): # IFC4 refs.append((cref.ReferencedSource.Name, cref.Identification)) self.location_msg = location[self.location] if refs: return facet_evaluation( (self.system, self.value) in refs, self.message % { "system": refs[0][0], "value": "'" + refs[0][1] + "'", "location": self.location_msg, }, # what if not first item of refs? ) else: return facet_evaluation(False, "does not have %sclassification reference" % self.location_msg) class property(facet): """ The IDS property facet implemented using `ifcopenshell.util.element` """ parameters = ["name", "propertyset", "value", "location"] message = "%(location)sproperty '%(name)s' in '%(propertyset)s' with a value %(value)s" @staticmethod def create(location="any", propertyset=None, name=None, value=None): """Create a property facet that can be added to applicability or requirements of IDS specification. :param location: Define where to check for the parameter. One of "any"|"instance"|"type", defaults to "any" :type location: str, optional :param propertyset: Propertyset that is required. Could be alphanumeric or restriction object, defaults to None :type propertyset: restriction|alphanumeric, optional :param name: Name that is required. Could be alphanumeric or restriction object, defaults to None :type name: restriction|alphanumeric, optional :param value: Value that is required. Could be alphanumeric or restriction object, defaults to None :type value: restriction|alphanumeric, optional :return: property object :rtype: property """ inst = property() inst.location = location inst.propertyset = propertyset inst.name = name inst.value = value # cls.attributes = {'@location': location} # 'type', 'instance', 'any' # BUG '@href': 'http://identifier.buildingsmart.org/uri/buildingsmart/ifc-4.3/prop/FireRating', #https://identifier.buildingsmart.org/uri/something # BUG 'instructions': 'Please add the desired rating.', return inst def asdict(self): """Converts object to a dictionary, adding required attributes. :return: Xmlschema compliant dictionary. :rtype: dict """ fac_dict = { "@location": self.location, "propertyset": parameter_asdict(self.propertyset), "name": parameter_asdict(self.name), "value": parameter_asdict(self.value), # "instructions": "SAMPLE_INSTRUCTIONS", # TODO '@href': 'http://identifier.buildingsmart.org/uri/buildingsmart/ifc-4.3/prop/FireRating', #https://identifier.buildingsmart.org/uri/something } return fac_dict def __call__(self, inst, logger): """Validate an ifc instance against that property facet. :param inst: IFC entity element :type inst: IFC entity :param logger: Logging object :type logger: logging :return: result of the validation as bool and message :rtype: facet_evaluation(bool, str) """ self.location = self.node["@location"] if self.propertyset == "attribute": val = {k.lower(): v for k, v in inst.get_info().items()}.get(self.name, None) else: # TODO sometimes AttributeError: 'str' object has no attribute 'wrappedValue' instance_props = ifcopenshell.util.element.get_psets(inst) if ifcopenshell.util.element.get_type(inst): type_props = ifcopenshell.util.element.get_psets(ifcopenshell.util.element.get_type(inst)) else: type_props = {} if self.location == "instance": props = instance_props elif self.location == "type" and type_props: props = type_props elif self.location == "any" and (instance_props or type_props): props = {**instance_props, **type_props} else: props = {} pset = props.get(self.propertyset) val = pset.get(self.name) if pset else None self.location_msg = location[self.location] di = {"name": self.name, "propertyset": self.propertyset, "value": "'%s'" % val, "location": self.location_msg} if val is not None: msg = self.message % di else: if pset: msg = "does not have %(location)sproperty '%(name)s' in a set '%(propertyset)s'" % di else: msg = "does not have %(location)sset '%(propertyset)s'" % di # TODO implement data type comparison # xs:string # xs:decimal # xs:integer # xs:boolean # xs:anyURI # xs:date YYYY-MM-DD # xs:time hh:mm:ss # xs:dateTime YYYY-MM-DDThh:mm:ss # xs:duration PnYnMnDTnHnMnS return facet_evaluation(val == self.value, msg) class material(facet): """The IDS material facet used to traverse the HasAssociations inverse attribute.""" parameters = ["value", "location"] message = "%(location)smaterial '%(value)s'" @staticmethod def create(location="any", value=None): """Create a material facet that can be added to applicability or requirements of IDS specification. :param location: Define where to check for the parameter. One of "any"|"instance"|"type", defaults to "any" :type location: str, optional :param value: Value that is required. Could be alphanumeric or restriction object, defaults to None :type value: restriction|alphanumeric, optional :return: material object :rtype: material """ inst = material() inst.location = location inst.value = value # TODO '@use': 'optional' # TODO '@href': 'https://identifier.buildingsmart.org/uri/something', # TODO 'instructions': 'Please add the desired...', return inst def asdict(self): """Converts object to a dictionary, adding required attributes. :return: Xmlschema compliant dictionary. :rtype: dict """ fac_dict = { "value": parameter_asdict(self.value), "@location": self.location, # TODO "instructions": "SAMPLE_INSTRUCTIONS", # TODO '@href': 'http://identifier.buildingsmart.org/uri/buildingsmart/ifc-4.3/prop/FireRating', #https://identifier.buildingsmart.org/uri/something # TODO '@use': 'optional' } return fac_dict def __call__(self, inst, logger): """Validate an ifc instance against that material facet. :param inst: IFC entity element :type inst: IFC entity :param logger: Logging object :type logger: logging :return: result of the validation as bool and message :rtype: facet_evaluation(bool, str) """ self.location = self.node["@location"] instance_material_rel = [rel for rel in inst.HasAssociations if rel.is_a("IfcRelAssociatesMaterial")] if ifcopenshell.util.element.get_type(inst): type_material_rel = [ rel for rel in ifcopenshell.util.element.get_type(inst).HasAssociations if rel.is_a("IfcRelAssociatesMaterial") ] else: type_material_rel = [] if self.location == "instance": material_relations = list(instance_material_rel) elif self.location == "type" and type_material_rel: material_relations = list(type_material_rel) elif self.location == "any" and (instance_material_rel or type_material_rel): material_relations = instance_material_rel + type_material_rel else: material_relations = [] materials = [] for rel in material_relations: if rel.RelatingMaterial.is_a() == "IfcMaterial": materials.append(rel.RelatingMaterial.Name) elif rel.RelatingMaterial.is_a() == "IfcMaterialMaterialList": # DEPRECATED in IFC4 [materials.append(mat.Name) for mat in rel.RelatingMaterial] elif rel.RelatingMaterial.is_a() == "IfcMaterialConstituentSet": [materials.append(mat.Material.Name) for mat in rel.RelatingMaterial.MaterialConstituents] elif rel.RelatingMaterial.is_a() == "IfcMaterialLayerSet": [materials.append(mat.Name) for mat in rel.RelatingMaterial.MaterialLayers] elif rel.RelatingMaterial.is_a() == "IfcMaterialLayerSetUsage": layers = rel.RelatingMaterial.ForLayerSet.MaterialLayers [materials.append(layer.Material.Name) for layer in layers] elif rel.RelatingMaterial.is_a() == "IfcMaterialProfileSet": [materials.append(mat.Material.Name) for mat in rel.RelatingMaterial.MaterialProfiles] elif rel.RelatingMaterial.is_a() == "IfcMaterialProfileSetUsage": profileSets = rel.RelatingMaterial.ForProfileSet.MaterialProfiles [materials.append(pset.Material.Name) for pset in profileSets] else: raise Exception("IfcRelAssociatesMaterial not implemented") if not materials: materials.append("UNDEFINED") self.location_msg = location[self.location] return facet_evaluation( self.value in materials, self.message % {"value": "'/'".join(materials), "location": self.location_msg}, ) def parameter_asdict(parameter): """Converts parameter to an IDS compliant dictionary, handling both value and restrictions. :return: Xmlschema compliant dictionary. :rtype: dict """ if isinstance(parameter, str): parameter_dict = {"simpleValue": parameter} elif isinstance(parameter, restriction): parameter_dict = {"xs:restriction": [parameter.asdict()]} elif isinstance(parameter, list): restrictions = {"@base": "xs:" + parameter[0].base} for p in parameter: x = p.asdict() restrictions[list(x)[1]] = x[list(x)[1]] parameter_dict = {"xs:restriction": [restrictions]} return parameter_dict class boolean_logic: """Boolean conjunction over a collection of functions""" def __init__(self, terms): self.terms = terms def __call__(self, *args): eval = [t(*args) for t in self.terms] join = [" and ", " or "][self.fold == any] return facet_evaluation(self.fold(eval), join.join(map(str, eval))) def __str__(self): return [" and ", " or "][self.fold == any].join(map(str, self.terms)) class boolean_and(boolean_logic): fold = all class boolean_or(boolean_logic): fold = any class restriction: """ The value restriction from XSD implemented as a list of values and a containment test """ def __init__(self): """Create a restriction that can be used instead of value of a parameter.""" self.type = "" self.options = [] @staticmethod def parse(ids_dict): """Parse xml restriction to python object. :param ids_dict: :type ids_dict: dict """ r = restriction() if ids_dict: # TODO 'base' missing in some IDS?! r.base = ids_dict["@base"][3:] for n in ids_dict: if n == "enumeration": r.type = "enumeration" for x in ids_dict[n]: r.options.append(x["@value"]) elif n[-7:] == "clusive": r.type = "bounds" r.options.append({n: ids_dict[n]["@value"]}) elif n[-5:] == "ength": r.type = "length" if n[3:6] == "min": r.options.append(">=") elif n[3:6] == "max": r.options.append("<=") else: r.options.append("==") r.options[-1] += str(ids_dict[n]["@value"]) elif n == "pattern": r.type = "pattern" r.options.append(ids_dict[n]["@value"]) # TODO add fractionDigits # TODO add totalDigits # TODO add whiteSpace elif n == "@base": pass else: print("Error! Restriction not implemented") return r def asdict(self): """Converts object to a dictionary, adding required attributes. :return: Xmlschema compliant dictionary. :rtype: dict """ rest_dict = {"@base": "xs:" + self.base} if self.type == "enumeration": for option in self.options: if "xs:enumeration" not in rest_dict: rest_dict["xs:enumeration"] = [{"@value": option}] else: rest_dict["xs:enumeration"].append({"@value": option}) elif self.type == "bounds": for option in self.options: if "xs:option" not in rest_dict: rest_dict["xs:" + option] = [{"@value": option}] else: rest_dict["xs:" + option].append({"@value": self.options[option], "@fixed": False}) elif self.type == "pattern": if "xs:pattern" not in rest_dict: rest_dict["xs:pattern"] = [{"@value": self.options}] else: rest_dict["xs:pattern"].append({"@value": self.options}) return rest_dict @staticmethod def create(options, type="pattern", base="string"): """Create restriction instead of simpleValue :param type: One of "enumeration"|"pattern"|"bounds", defaults to "pattern" :type type: str, optional :param options: if enumeration: list of possible values if pattern: xml regular expression string if bounds: dictionary with possible keys: 'minInclusive', 'maxInclusive', 'minExclusive', 'maxExclusive' :type options: list|str|dict :param base: One of "string"|"boolean"|"decimal"|"integer", defaults to "string" :type base: str, optional :raises Exception: If not properly defined restriction. :return: restriction object :rtype: restriction """ rest = restriction() if type in ["enumeration", "pattern", "bounds"]: rest.type = type rest.base = base rest.options = options if ( (type == "enumeration" and isinstance(options, list)) or (type == "bounds" and isinstance(options, dict)) or (type == "pattern" and isinstance(options, str)) ): rest.options = options else: Exception("Options were not properly defined.") return rest else: raise Exception( "Such restriction not implemented. Try: 'enumeration', 'pattern' or 'min/maxInclusive' or 'min/maxExclusive'." ) def __eq__(self, other): """Evaluate the restriction using equality sign. :param other: value to compare with the restriction. :type other: str|float|int :return: True if 'other' match the restriction, False if not. :rtype: bool """ result = False # TODO implement data type comparison if self and (other or other == 0): if self.type == "enumeration" and self.base == "bool": self.options = [x.lower() for x in self.options] result = str(other).lower() in self.options elif self.type == "enumeration": result = other in self.options elif self.type == "bounds": result = True for sign in self.options.keys(): if sign == "minInclusive" and other < self.options[sign]: result = False elif sign == "maxInclusive" and other > self.options[sign]: result = False elif sign == "minExclusive" and other <= self.options[sign]: result = False elif sign == "maxExclusive" and other >= self.options[sign]: result = False elif self.type == "length": for op in self.options: if eval(str(len(other)) + op): # TODO eval not safe? result = True elif self.type == "pattern": translated_pattern = identities.translate_pattern(self.options) regex_pattern = re.compile(translated_pattern) if regex_pattern.fullmatch(other) is not None: result = True # TODO add fractionDigits # TODO add totalDigits # TODO add whiteSpace return result def __repr__(self): """Represent the restriction in human readible sentence. :return: sentence :rtype: str """ msg = "of type '%s', " % (self.base) if self.type == "enumeration": msg = msg + "of value: '%s'" % "' or '".join(self.options) elif self.type == "bounds": msg = msg + "of value %s" % ", and ".join([bounds[x] + str(self.options[x]) for x in self.options]) elif self.type == "length": msg = msg + "with %s letters" % " and ".join(self.options) elif self.type == "pattern": msg = msg + "respecting the pattern '%s'" % self.options # TODO add fractionDigits # TODO add totalDigits # TODO add whiteSpace return msg class SimpleHandler(logging.StreamHandler): """Logging handler listing all cases in python list.""" def __init__(self, report_valid=False): """Logging handler listing all cases in python list. :param report_valid: True if you want to list all the compliant cases as well, defaults to False :type report_valid: bool, optional """ logging.StreamHandler.__init__(self) self.statements = [] if report_valid: self.setLevel(logging.INFO) else: self.setLevel(logging.ERROR) def emit(self, mymsg): """Triggered on each use of logging with the Simple handler enabled. :param log_content: default logger message :type log_content: string|dict """ self.statements.append(mymsg.msg) class BcfHandler(logging.StreamHandler): """Logging handler for creation of BCF report files. :param project_name: defaults to "IDS Project" :type project_name: str, optional :param author: Email of the person creating the BCF report, defaults to "your@email.com" :type author: str, optional :param filepath: Path to save the BCF report, defaults to None :type filepath: str, optional :param report_valid: True if you want to list all the compliant cases as well, defaults to False :type report_valid: bool, optional Example:: bcf_handler = BcfHandler( project_name="Default IDS Project", author="your@email.com", filepath="example.bcf", ) logger = logging.getLogger("IDS_Logger") logging.basicConfig(level=logging.INFO, format="%(message)s") logger.addHandler(bcf_handler) """ def __init__(self, project_name="IDS Project", author="your@email.com", filepath=None, report_valid=False): logging.StreamHandler.__init__(self) if report_valid: self.setLevel(logging.INFO) else: self.setLevel(logging.ERROR) self.bcf = BcfXml() self.bcf.author = author self.bcf.new_project() self.bcf.project.name = project_name self.filepath = filepath self.bcf.edit_project() def emit(self, log_content): """Triggered on each use of logging with the BCF handler enabled. :param log_content: default logger message :type log_content: string|dict """ topic = bcf.Topic() topic.title = log_content.msg["sentence"].split(".\n")[1] topic.description = log_content.msg["sentence"].split(".\n")[0] self.bcf.add_topic(topic) try: # Add viewpoint and link to ifc object viewpoint = bcf.Viewpoint() viewpoint.perspective_camera = bcf.PerspectiveCamera() ifc_elem = log_content.msg["ifc_element"] # ifc_elem = ifc_file.by_guid(log_content.msg["guid"]) target_position = np.array(ifcopenshell.util.placement.get_local_placement(ifc_elem.ObjectPlacement)) target_position = target_position[:, 3][0:3] camera_position = target_position + np.array((5, 5, 5)) viewpoint.perspective_camera.camera_view_point.x = camera_position[0] viewpoint.perspective_camera.camera_view_point.y = camera_position[1] viewpoint.perspective_camera.camera_view_point.z = camera_position[2] camera_direction = camera_position - target_position camera_direction = camera_direction / np.linalg.norm(camera_direction) camera_right = np.cross(np.array([0.0, 0.0, 1.0]), camera_direction) camera_right = camera_right / np.linalg.norm(camera_right) camera_up = np.cross(camera_direction, camera_right) camera_up = camera_up / np.linalg.norm(camera_up) rotation_transform = np.zeros((4, 4)) rotation_transform[0, :3] = camera_right rotation_transform[1, :3] = camera_up rotation_transform[2, :3] = camera_direction rotation_transform[-1, -1] = 1 translation_transform = np.eye(4) translation_transform[:3, -1] = -camera_position look_at_transform = np.matmul(rotation_transform, translation_transform) mat = np.linalg.inv(look_at_transform) viewpoint.perspective_camera.camera_direction.x = mat[0][2] * -1 viewpoint.perspective_camera.camera_direction.y = mat[1][2] * -1 viewpoint.perspective_camera.camera_direction.z = mat[2][2] * -1 viewpoint.perspective_camera.camera_up_vector.x = mat[0][1] viewpoint.perspective_camera.camera_up_vector.y = mat[1][1] viewpoint.perspective_camera.camera_up_vector.z = mat[2][1] viewpoint.components = bcf.Components() c = bcf.Component() c.ifc_guid = log_content.msg["guid"] viewpoint.components.selection.append(c) viewpoint.components.visibility = bcf.ComponentVisibility() viewpoint.components.visibility.default_visibility = True viewpoint.snapshot = None self.bcf.add_viewpoint(topic, viewpoint) except: pass def flush(self): """Saves the BCF report to file. Triggered at the end of the validation process.""" if not self.filepath: self.filepath = os.getcwd() + r"\IDS_report.bcfzip" if not (self.filepath.endswith(".bcf") or self.filepath.endswith(".bcfzip")): self.filepath = self.filepath + r"\IDS_report.bcfzip" self.bcf.save_project(self.filepath) location = {"instance": "an instance ", "type": "a type ", "any": "a "} bounds = { "minInclusive": "larger or equal ", "maxInclusive": "smaller or equal ", "minExclusive": "larger than ", "maxExclusive": "smaller than ", } if __name__ == "__main__": import sys, os import ifcopenshell ids_file = ids.open(sys.argv[1]) ifc_file = ifcopenshell.open(sys.argv[2]) filepath = sys.argv[3] logger = logging.getLogger("IDS_Logger") logging.basicConfig(filename=filepath, level=logging.INFO, format="%(message)s") logging.FileHandler(filepath + r"\report.txt", mode="w") bcf_handler = BcfHandler( project_name="Default IDS Project", author="your@email.com", filepath=filepath + r"\report.bcfzip", ) logger.addHandler(bcf_handler) report = SimpleHandler() logger.addHandler(report) ids_file.validate(ifc_file, logger)