diff --git a/src/bcf/README.md b/src/bcf/README.md new file mode 100644 index 0000000000..85f2f328cf --- /dev/null +++ b/src/bcf/README.md @@ -0,0 +1,7 @@ +# bcf + +A simple Python implementation of BCF. The data model is described in `data.py`. +Manipulation of BCF-XML is available via `bcfxml.py` and manipulation of BCF-API +is available via `bcfapi.py`. + +Currently supports BCF version 2.1. diff --git a/src/bcf/bcf/bcfxml.py b/src/bcf/bcf/bcfxml.py new file mode 100644 index 0000000000..66418a3283 --- /dev/null +++ b/src/bcf/bcf/bcfxml.py @@ -0,0 +1,306 @@ +import os +import tempfile +import shutil +import zipfile +import logging +import bcf.data +from xmlschema import XMLSchema +from contextlib import contextmanager + + +cwd = os.path.dirname(os.path.realpath(__file__)) + + +@contextmanager +def cd(newdir): + prevdir = os.getcwd() + os.chdir(os.path.expanduser(newdir)) + try: + yield + finally: + os.chdir(prevdir) + + +class BcfXml: + def __init__(self): + self.filepath = None + self.logger = logging.getLogger("bcfxml") + self.project = bcf.data.Project() + self.topics = {} + + def get_project(self, filepath=None): + if not filepath: + return self.project + zip_file = zipfile.ZipFile(filepath) + self.filepath = tempfile.mkdtemp() + zip_file.extractall(self.filepath) + data = self._read_xml("project.bcfp", "project.xsd") + self.project.project_id = data["Project"]["@ProjectId"] + self.project.name = data["Project"]["Name"] + return self.project + + def edit_project(self): + pass + + def save_project(self, filepath): + with cd(self.filepath): + zip_file = zipfile.ZipFile(filepath, "w", zipfile.ZIP_DEFLATED) + for root, dirs, files in os.walk("./"): + for file in files: + zip_file.write(os.path.join(root, file)) + zip_file.close() + + def get_version(self): + data = self._read_xml("bcf.version", "version.xsd") + return data["@VersionId"] + + def get_topics(self): + self.topics = {} + topics = [] + subdirs = [] + for (dirpath, dirnames, filenames) in os.walk(self.filepath): + subdirs = dirnames + break + for subdir in subdirs: + self.topics[subdir] = self.get_topic(subdir) + return self.topics + + def get_topic(self, guid): + if guid in self.topics: + return self.topics[guid] + data = self._read_xml(os.path.join(guid, "markup.bcf"), "markup.xsd") + topic = bcf.data.Topic() + self.topics[guid] = topic + + mandatory_keys = { + "guid": "@Guid", + "title": "Title", + "creation_date": "CreationDate", + "creation_author": "CreationAuthor", + } + for key, value in mandatory_keys.items(): + setattr(topic, key, data["Topic"][value]) + + optional_keys = { + "priority": "Priority", + "index": "Index", + "labels": "Labels", + "modified_date": "ModifiedDate", + "modified_author": "ModifiedAuthor", + "due_date": "DueDate", + "assigned_to": "AssignedTo", + "stage": "Stage", + "description": "Description", + "topic_status": "TopicStatus", + "topic_type": "TopicType", + } + for key, value in optional_keys.items(): + if value in data["Topic"]: + setattr(topic, key, data["Topic"][value]) + + if "BimSnippet" in data["Topic"]: + bim_snippet = bcf.data.BimSnippet() + keys = { + "snippet_type": "@SnippetType", + "is_external": "@IsExternal", + "reference": "Reference", + "reference_schema": "ReferenceSchema", + } + for key, value in keys.items(): + if value in data["Topic"]["BimSnippet"]: + setattr(bim_snippet, key, data["Topic"]["BimSnippet"][value]) + topic.bim_snippet = bim_snippet + + if "DocumentReference" in data["Topic"]: + for item in data["Topic"]["DocumentReference"]: + document_reference = bcf.data.DocumentReference() + keys = { + "referenced_document": "ReferencedDocument", + "is_external": "@IsExternal", + "guid": "@Guid", + "description": "Description", + } + for key, value in keys.items(): + if value in item: + setattr(document_reference, key, item[value]) + topic.document_references.append(document_reference) + + if "RelatedTopic" in data["Topic"]: + for item in data["Topic"]["RelatedTopic"]: + related_topic = bcf.data.RelatedTopic() + related_topic.guid = item["@Guid"] + topic.related_topics.append(related_topic) + return topic + + def get_header(self, guid): + data = self._read_xml(os.path.join(guid, "markup.bcf"), "markup.xsd") + + def get_comments(self, guid): + comments = [] + data = self._read_xml(os.path.join(guid, "markup.bcf"), "markup.xsd") + if "Comment" not in data: + return comments + for item in data["Comment"]: + comment = bcf.data.Comment() + mandatory_keys = {"guid": "@Guid", "date": "Date", "author": "Author", "comment": "Comment"} + for key, value in mandatory_keys.items(): + setattr(comment, key, item[value]) + optional_keys = {"modified_date": "ModifiedDate", "modified_author": "ModifiedAuthor"} + for key, value in optional_keys.items(): + if value in item: + setattr(comment, key, item[value]) + if "Viewpoint" in item: + viewpoint = bcf.data.Viewpoint() + viewpoint.guid = item["Viewpoint"]["@Guid"] + comment.viewpoint = viewpoint + comments.append(comment) + return comments + + def get_viewpoints(self, guid): + viewpoints = [] + data = self._read_xml(os.path.join(guid, "markup.bcf"), "markup.xsd") + if "Viewpoints" not in data: + return viewpoints + for item in data["Viewpoints"]: + viewpoints.append(self.get_viewpoint(item, guid)) + return viewpoints + + def get_viewpoint(self, data, topic_guid): + viewpoint = bcf.data.Viewpoint() + viewpoint.guid = data["@Guid"] + optional_keys = {"viewpoint": "Viewpoint", "snapshot": "Snapshot", "index": "Index"} + for key, value in optional_keys.items(): + if value in data: + setattr(viewpoint, key, data[value]) + visinfo = self._read_xml(os.path.join(topic_guid, viewpoint.viewpoint), "visinfo.xsd") + viewpoint.components = self.get_viewpoint_components(visinfo) + viewpoint.orthogonal_camera = self.get_viewpoint_orthogonal_camera(visinfo) + viewpoint.perspective_camera = self.get_viewpoint_perspective_camera(visinfo) + viewpoint.lines = self.get_viewpoint_lines(visinfo) + viewpoint.clipping_planes = self.get_viewpoint_clipping_planes(visinfo) + viewpoint.bitmaps = self.get_viewpoint_bitmaps(visinfo) + return viewpoint + + def get_viewpoint_components(self, visinfo): + if "Components" not in visinfo: + return None + components = bcf.data.Components() + data = visinfo["Components"] + if "ViewSetupHints" in data: + view_setup_hints = bcf.data.ViewSetupHints() + optional_keys = { + "spaces_visible": "@SpacesVisible", + "space_boundaries_visible": "@SpaceBoundariesVisible", + "openings_visible": "@OpeningsVisible", + } + for key, value in optional_keys.items(): + if value in data["ViewSetupHints"]: + setattr(view_setup_hints, key, data["ViewSetupHints"][value]) + components.view_setup_hints = view_setup_hints + if "Selection" in data and "Component" in data["Selection"]: + for item in data["Selection"]["Component"]: + components.selection.append(self.get_component(item)) + if "Visibility" in data: + component_visibility = bcf.data.ComponentVisibility() + if "@DefaultVisibility" in data["Visibility"]: + component_visibility.default_visibility = data["Visibility"]["@DefaultVisibility"] + if "Exceptions" in data["Visibility"] and "Component" in data["Visibility"]["Exceptions"]: + for item in data["Visibility"]["Exceptions"]["Component"]: + component_visibility.exceptions.append(self.get_component(item)) + components.visibility = component_visibility + if "Coloring" in data and "Color" in data["Coloring"]: + for item in data["Coloring"]["Color"]: + color = bcf.data.Color() + color.color = item["@Color"] + for item2 in item["Component"]: + color.components.append(self.get_component(item2)) + components.coloring.append(color) + return components + + def get_viewpoint_orthogonal_camera(self, visinfo): + if "OrthogonalCamera" not in visinfo: + return None + camera = bcf.data.OrthogonalCamera() + data = visinfo["OrthogonalCamera"] + self.set_vector(camera.camera_view_point, data["CameraViewPoint"]) + self.set_vector(camera.camera_direction, data["CameraDirection"]) + self.set_vector(camera.camera_up_vector, data["CameraUpVector"]) + camera.view_to_world_scale = data["ViewToWorldScale"] + + def get_viewpoint_perspective_camera(self, visinfo): + if "PerspectiveCamera" not in visinfo: + return None + camera = bcf.data.PerspectiveCamera() + data = visinfo["PerspectiveCamera"] + self.set_vector(camera.camera_view_point, data["CameraViewPoint"]) + self.set_vector(camera.camera_direction, data["CameraDirection"]) + self.set_vector(camera.camera_up_vector, data["CameraUpVector"]) + camera.field_of_view = data["FieldOfView"] + + def get_viewpoint_lines(self, visinfo): + if "Lines" not in visinfo: + return [] + lines = [] + for item in visinfo["Lines"]["Line"]: + line = bcf.data.Line() + self.set_vector(line.start_point, item["StartPoint"]) + self.set_vector(line.end_point, item["EndPoint"]) + lines.append(line) + return lines + + def get_viewpoint_clipping_planes(self, visinfo): + if "ClippingPlanes" not in visinfo: + return [] + planes = [] + for item in visinfo["ClippingPlanes"]["ClippingPlane"]: + plane = bcf.data.ClippingPlane() + self.set_vector(plane.location, item["Location"]) + self.set_vector(plane.direction, item["Direction"]) + planes.append(plane) + return planes + + def get_viewpoint_bitmaps(self, visinfo): + if "Bitmap" not in visinfo: + return [] + bitmaps = [] + for item in visinfo["Bitmap"]: + bitmap = bcf.data.Bitmap() + bitmap.reference = item["Reference"] + bitmap.bitmap_type = item["Bitmap"].lower() + self.set_vector(bitmap.location, item["Location"]) + self.set_vector(bitmap.normal, item["Normal"]) + self.set_vector(bitmap.up, item["Up"]) + bitmap.height = item["Height"] + bitmaps.append(bitmap) + return bitmaps + + def set_vector(self, to_obj, from_xml): + to_obj.x = from_xml["X"] + to_obj.y = from_xml["Y"] + to_obj.z = from_xml["Z"] + + def get_component(self, data): + component = bcf.data.Component() + optional_keys = { + "originating_system": "OriginatingSystem", + "authoring_tool_id": "AuthoringToolId", + "ifc_guid": "@IfcGuid", + } + for key, value in optional_keys.items(): + if value in data: + setattr(component, key, data[value]) + return component + + def close_project(self): + shutil.rmtree(self.filepath) + + def _read_xml(self, filename, xsd): + schema = XMLSchema(os.path.join(cwd, "xsd", xsd)) + filepath = os.path.join(self.filepath, filename) + (data, errors) = schema.to_dict(filepath, validation="lax") + for error in errors: + self.logger.error(error) + return data + + def __del__(self): + self.close_project() diff --git a/src/bcf/bcf/data.py b/src/bcf/bcf/data.py new file mode 100644 index 0000000000..7bd72b7c22 --- /dev/null +++ b/src/bcf/bcf/data.py @@ -0,0 +1,159 @@ +class Project: + def __init__(self): + self.project_id = "" + self.name = "" + + +class BimSnippet: + def __init__(self): + self.snippet_type = None + self.is_external = False + self.reference = None + self.reference_schema = None + + +class DocumentReference: + def __init__(self): + self.referenced_document = None + self.description = None + self.guid = None + self.is_external = False + + +class RelatedTopic: + def __init__(self): + self.guid = None + + +class Topic: + def __init__(self): + self.reference_links = [] + self.title = "" + self.priority = None + self.index = None # Deprecated, stored, but ignored + self.labels = [] + self.creation_date = None + self.creation_author = None + self.modified_date = None + self.modified_author = None + self.due_date = None + self.assigned_to = None + self.stage = None + self.description = None + self.bim_snippet = None + self.document_references = [] + self.related_topics = [] + self.topic_status = None + self.topic_type = None + self.guid = None + + +class Comment: + def __init__(self): + self.guid = None + self.date = None + self.author = None + self.comment = None + self.viewpoint = None + self.modified_date = None + self.modified_author = None + self.topic_guid = None # Part of BCF-API + + +class ViewSetupHints: + def __init__(self): + self.spaces_visible = False + self.space_boundaries_visible = False + self.openings_visible = False + + +class Component: + def __init__(self): + self.originating_system = None + self.authoring_tool_id = None + self.ifc_guid = None + + +class ComponentVisibility: + def __init__(self): + self.exceptions = [] + self.default_visibility = False + + +class Color: + def __init__(self): + self.color = None + self.components = [] + + +class Components: + def __init__(self): + self.view_setup_hints = None + self.selection = [] + self.visibility = None + self.coloring = [] + + +class Point: + def __init__(self): + self.x = 0 + self.y = 0 + self.z = 0 + + +class Direction(Point): + pass + + +class OrthogonalCamera: + def __init__(self): + self.camera_view_point = Point() + self.camera_direction = Direction() + self.camera_up_vector = Direction() + self.view_to_world_scale = 1.0 + + +class PerspectiveCamera: + def __init__(self): + self.camera_view_point = Point() + self.camera_direction = Direction() + self.camera_up_vector = Direction() + self.field_of_view = 60.0 + + +class Line: + def __init__(self): + self.start_point = Point() + self.end_point = Point() + + +class ClippingPlane: + def __init__(self): + self.location = Point() + self.direction = Direction() + + +class Bitmap: + def __init__(self): + self.reference = "" # Only in BCF-XML + self.bitmap_data = None # Only in BCF-API + self.bitmap_type = "png" # Enum of png or jpg + self.location = Point() + self.normal = Direction() + self.up = Direction() + self.height = 1.0 + + +class Viewpoint: + def __init__(self): + self.guid = None + self.viewpoint = None + self.snapshot = None + self.index = None + + self.components = None # It's not a list, despite the plural name + self.orthogonal_camera = None + self.perspective_camera = None + self.lines = [] + self.clipping_planes = [] + self.bitmaps = [] diff --git a/src/bcf/bcf/xsd/markup.xsd b/src/bcf/bcf/xsd/markup.xsd new file mode 100644 index 0000000000..037464ee8f --- /dev/null +++ b/src/bcf/bcf/xsd/markup.xsd @@ -0,0 +1,154 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/bcf/bcf/xsd/project.xsd b/src/bcf/bcf/xsd/project.xsd new file mode 100644 index 0000000000..8203889b6c --- /dev/null +++ b/src/bcf/bcf/xsd/project.xsd @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/src/bcf/bcf/xsd/version.xsd b/src/bcf/bcf/xsd/version.xsd new file mode 100644 index 0000000000..55cbd2be90 --- /dev/null +++ b/src/bcf/bcf/xsd/version.xsd @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/src/bcf/bcf/xsd/visinfo.xsd b/src/bcf/bcf/xsd/visinfo.xsd new file mode 100644 index 0000000000..8bec85b445 --- /dev/null +++ b/src/bcf/bcf/xsd/visinfo.xsd @@ -0,0 +1,191 @@ + + + + + + VisualizationInfo documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + view's visible size in meters + + + + + + + + + + + + + It is currently limited to a value between 45 and 60 degrees. + This limitation will be dropped in the next release and viewers + should be expect values outside this range in current implementations. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +