add BCF reporter to IDS (#2552)

* grammar

* reason not always a list

* extend readme with instruction

* remove microseconds from date

* add BCF reporter
This commit is contained in:
ArturTomczak
2022-10-30 23:16:08 +01:00
committed by GitHub
parent 0338825175
commit 31600033bf
4 changed files with 99 additions and 69 deletions
+5 -5
View File
@@ -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)
+33 -1
View File
@@ -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()
```
+7 -3
View File
@@ -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"
+54 -60
View File
@@ -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)