diff --git a/src/bcf/src/bcf/v2/bcfxml.py b/src/bcf/src/bcf/v2/bcfxml.py index 76c094d69e..61a91d0267 100644 --- a/src/bcf/src/bcf/v2/bcfxml.py +++ b/src/bcf/src/bcf/v2/bcfxml.py @@ -223,10 +223,10 @@ class BcfXml: def edit_topic(self, topic): if not topic.creation_date: - topic.creation_date = datetime.utcnow().isoformat() + topic.creation_date = datetime.utcnow().replace(microsecond=0).isoformat() topic.creation_author = self.author else: - topic.modified_date = datetime.utcnow().isoformat() + topic.modified_date = datetime.utcnow().replace(microsecond=0).isoformat() topic.modified_author = self.author self.document = minidom.Document() @@ -345,10 +345,10 @@ class BcfXml: def edit_comment(self, comment, topic): if not comment.date: - comment.date = datetime.utcnow().isoformat() + comment.date = datetime.utcnow().replace(microsecond=0).isoformat() comment.author = self.author else: - comment.modified_date = datetime.utcnow().isoformat() + comment.modified_date = datetime.utcnow().replace(microsecond=0).isoformat() comment.modified_author = self.author self.edit_topic(topic) @@ -595,7 +595,7 @@ class BcfXml: copyfile(header_file.reference, os.path.join(topic_filepath, header_file.filename)) header_file.reference = header_file.filename header_file.is_external = False - header_file.date = datetime.utcnow().isoformat() + header_file.date = datetime.utcnow().replace(microsecond=0).isoformat() if not topic.header: topic.header = bcf.v2.data.Header() topic.header.files.append(header_file) diff --git a/src/ifctester/README.md b/src/ifctester/README.md index ae533658ae..a1f76f098e 100644 --- a/src/ifctester/README.md +++ b/src/ifctester/README.md @@ -1,3 +1,35 @@ # ifctester -Author, test, and see reports from IDS audits on the command line, as a webapp, or as a library. +With **IfcTester**, you can author and read **Information Delivery Specification** - **IDS** - files and validate your IFC models against IDS to see if your model is compliant. After the audit, you can generate reports in console, as a web page, JSON or BCF file. It works from the command line, as a web app, or as a library. + +## How to use it + +``` +# create new IDS +my_ids = ids.Ids(title="My IDS") + +# add specification to it +my_spec = ids.Specification(name="My first specification") +my_spec.applicability.append(ids.Entity(name="IFCWALL")) +property = ids.Property( + name="IsExternal", + value="TRUE", + propertySet="Pset_WallCommon", + measure="IfcBoolean", + uri="https://identifier.buildingsmart.org/uri/.../prop/LoadBearing", + instructions="Walls need to be load bearing.") +my_spec.requirements.append(property) +my_ids.specifications.append(my_spec) + +# Save such IDS to file +result = my_ids.to_xml("SampleIDS.xml") + +# open IFC file: +my_ifc = ifcopenshell.open("MyIfcModel.ifc") + +# validate IFC model against IDS requirements: +my_ids.validate(my_ifc) + +# show results: +reporter.Console(my_ids).report() +``` \ No newline at end of file diff --git a/src/ifctester/ifctester/facet.py b/src/ifctester/ifctester/facet.py index 5ed619250e..cbf8e1440b 100644 --- a/src/ifctester/ifctester/facet.py +++ b/src/ifctester/ifctester/facet.py @@ -882,10 +882,14 @@ class PropertyResult(Result): return "The property set does not contain the required property" elif self.reason["type"] == "MEASURE": return f"The data type \"{str(self.reason['actual'])}\" does not match the requirements" - elif self.reason["type"] == "VALUE" and len(self.reason["actual"]) == 1: - return f"The property value \"{str(self.reason['actual'][0])}\" does not match the requirements" elif self.reason["type"] == "VALUE": - return f"The property values \"{str(self.reason['actual'])}\" do not match the requirements" + if isinstance(self.reason["actual"], list): + if len(self.reason["actual"]) == 1: + return f"The property value \"{str(self.reason['actual'][0])}\" does not match the requirements" + else: + return f"The property values \"{str(self.reason['actual'])}\" do not match the requirements" + else: + return f"The property value \"{str(self.reason['actual'])}\" does not match the requirements" elif self.reason["type"] == "PROHIBITED": return f"The property should not have met the requirement" diff --git a/src/ifctester/ifctester/reporter.py b/src/ifctester/ifctester/reporter.py index 8a341b6f28..3300dfd327 100644 --- a/src/ifctester/ifctester/reporter.py +++ b/src/ifctester/ifctester/reporter.py @@ -204,63 +204,58 @@ class Html(Json): return outfile.write(pystache.render(file.read(), self.results)) -class BcfHandler(logging.StreamHandler): - """Logging handler for creation of BCF report files. +class Bcf(Reporter): + def __init__(self, ids, with_viewpoint=True, filepath=None, group=False): + super().__init__(ids) - :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 + from bcf.v2.bcfxml import BcfXml - Example:: + self.with_viewpoint = with_viewpoint + self.group = group + self.bcf = BcfXml() + self.bcf.new_project() + if not filepath: + self.filepath=os.path.join(sys.path[0], self.ids.info['title'] + '.bcf') + # self.bcf.filepath = sys.path[0] + self.bcf.project.name = self.ids.info.get("title", "Untitled IDS") + self.bcf.author = self.ids.info.get("author", "your@email.com") + self.bcf.creation_date = datetime.datetime.today().replace(microsecond=0).isoformat() + self.bcf.modified_date = datetime.datetime.today().replace(microsecond=0).isoformat() - 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 report(self): + for specification in self.ids.specifications: + self.report_specification(specification, save_project=False) + self.bcf.save_project(self.filepath) - def __init__(self, project_name="IDS Project", author="your@email.com", filepath=None, report_valid=False): + def report_specification(self, specification, save_project=True): + from bcf.v2 import data as bcf + for requirement in specification.requirements: + if self.group: + topic = bcf.Topic() + topic.title = requirement.to_string("requirement") + topic.description = ";\n".join([requirement.failed_reasons[i] + " for " + str(e.get_info()['type']) + ": " + str(e.GlobalId) for i, e in enumerate(requirement.failed_entities)]) + self.bcf.add_topic(topic) + if self.with_viewpoint: + self.add_viewpoint(topic, requirement) + else: + for i, e in enumerate(requirement.failed_entities): + topic = bcf.Topic() + topic.title = requirement.to_string("requirement") + topic.description = requirement.failed_reasons[i] + " for " + str(e.get_info()['type']) + ": " + str(e.GlobalId) + self.bcf.add_topic(topic) + if self.with_viewpoint: + self.add_viewpoint(topic, requirement) + if save_project: + self.bcf.save_project(self.filepath) + + def add_viewpoint(self, topic, requirement): import numpy as np import ifcopenshell.util.placement - from bcf.v2.bcfxml import BcfXml from bcf.v2 import data as bcf - 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"]) + ifc_elem = requirement.failed_entities[0] 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)) @@ -289,18 +284,17 @@ class BcfHandler(logging.StreamHandler): 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) + for e in requirement.failed_entities: + c = bcf.Component() + c.ifc_guid = e.GlobalId + 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) - - 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.bcf" - if not (self.filepath.endswith(".bcf") or self.filepath.endswith(".bcfzip")): - self.filepath = self.filepath + r"\IDS_report.bcf" - self.bcf.save_project(self.filepath) + # add single pixel png file as a snapshot, as viewpoints without snapshots won't open in viewers + png = b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00\x01\x01\x03\x00\x00\x00%\xdbV\xca\x00\x00\x00\x03PLTE\x00\x00\x00\xa7z=\xda\x00\x00\x00\x01tRNS\x00@\xe6\xd8f\x00\x00\x00\nIDAT\x08\xd7c`\x00\x00\x00\x02\x00\x01\xe2!\xbc3\x00\x00\x00\x00IEND\xaeB`\x82" + snapshot_path = os.path.join(self.bcf.filepath, topic.guid, topic.guid+".png") + snapshot = open(snapshot_path, "wb") + snapshot.write(png) + snapshot.close() + viewpoint.snapshot = topic.guid+".png" + self.bcf.add_viewpoint(topic, viewpoint) \ No newline at end of file