├── apt.txt ├── img └── ifcmapconversion-relationship.png ├── utils ├── __pycache__ │ ├── IfcGraphViz.cpython-310.pyc │ ├── IfcGraphViz.cpython-39.pyc │ └── JupyterIFCRenderer.cpython-39.pyc ├── _JupyterIFCRenderer.py ├── IfcGraphViz.py └── JupyterIFCRenderer.py ├── helpful docs └── User-Guide-for-Geo-referencing-in-IFC-v2.0.pdf ├── postBuild ├── README.MD ├── environment.yml ├── models └── ifc4 geolocation.ifc └── 1. geolocation.ipynb /apt.txt: -------------------------------------------------------------------------------- 1 | libgl1-mesa-glx -------------------------------------------------------------------------------- /img/ifcmapconversion-relationship.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vulevukusej/Jupyter-IfcOpenShell/HEAD/img/ifcmapconversion-relationship.png -------------------------------------------------------------------------------- /utils/__pycache__/IfcGraphViz.cpython-310.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vulevukusej/Jupyter-IfcOpenShell/HEAD/utils/__pycache__/IfcGraphViz.cpython-310.pyc -------------------------------------------------------------------------------- /utils/__pycache__/IfcGraphViz.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vulevukusej/Jupyter-IfcOpenShell/HEAD/utils/__pycache__/IfcGraphViz.cpython-39.pyc -------------------------------------------------------------------------------- /utils/__pycache__/JupyterIFCRenderer.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vulevukusej/Jupyter-IfcOpenShell/HEAD/utils/__pycache__/JupyterIFCRenderer.cpython-39.pyc -------------------------------------------------------------------------------- /helpful docs/User-Guide-for-Geo-referencing-in-IFC-v2.0.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vulevukusej/Jupyter-IfcOpenShell/HEAD/helpful docs/User-Guide-for-Geo-referencing-in-IFC-v2.0.pdf -------------------------------------------------------------------------------- /postBuild: -------------------------------------------------------------------------------- 1 | jupyter labextension install jupyter-threejs 2 | jupyter labextension install jupyter-datawidgets 3 | jupyter labextension install ipycanvas 4 | jupyter serverextension enable nbgitpuller --sys-prefix -------------------------------------------------------------------------------- /README.MD: -------------------------------------------------------------------------------- 1 | Welcome to this repository, dedicated to providing interactive use-cases for IfcOpenShell. 2 | 3 | I'll be adding more examples over time, so check back periodically. Otherwise, if you have an idea please submit an issue ;) 4 | -------------------------------------------------------------------------------- /environment.yml: -------------------------------------------------------------------------------- 1 | name: base 2 | channels: 3 | - conda-forge 4 | dependencies: 5 | - jupyterlab=3.1 6 | - jupyter-offlinenotebook=0.2 7 | - nodejs=12 8 | - nbconvert=6 9 | # RTC 10 | - jupyterlab-link-share=0.2 11 | # Python Kernel 12 | - ipykernel=5.1 13 | - xeus-python=0.9 14 | - ipywidgets=7.6 15 | - widgetsnbextension=3.5 16 | - ipyleaflet=0.13 17 | - pythonocc-core 18 | - pythreejs 19 | - ifcopenshell 20 | - python-graphviz 21 | - shapely 22 | - ipycanvas 23 | - lxml 24 | - sidecar 25 | - ipycytoscape 26 | - networkx 27 | - nbgitpuller 28 | - altair=4.1 29 | - bqplot=0.12.20 30 | - dask=2020.12 31 | - matplotlib=3.1 32 | - pandas=1 33 | - python=3.7 34 | - scikit-image=0.15 35 | - scikit-learn=0.21 36 | - seaborn=0.11 37 | - tensorflow=1.13 38 | - sympy=1.4 39 | - pyyaml 40 | - traittypes==0.2.1 41 | - invoke=1.2 42 | - rdflib 43 | - sparqlwrapper 44 | - python-graphviz 45 | - pydotplus 46 | -------------------------------------------------------------------------------- /utils/_JupyterIFCRenderer.py: -------------------------------------------------------------------------------- 1 | import os 2 | import random 3 | from OCC.Display.WebGl.jupyter_renderer import JupyterRenderer, format_color, NORMAL 4 | import OCC.Core, OCC.Core.gp 5 | import ifcopenshell, ifcopenshell.geom 6 | 7 | 8 | class JupyterIFCRenderer(JupyterRenderer): 9 | colors_dict = { "IfcWall": (50,50,50)} 10 | 11 | 12 | def __init__(self, 13 | model, 14 | display_ents = ["IfcProduct", "IfcWall"], 15 | hide_ents = ["IfcOpening", "IfcFurnitureElement"], 16 | size=(640, 480), 17 | compute_normals_mode=NORMAL.CLIENT_SIDE, 18 | default_shape_color=format_color(166, 166, 166), # light grey 19 | default_edge_color=format_color(32, 32, 32), # dark grey 20 | default_vertex_color=format_color(8, 8, 8), # darker grey 21 | pick_color=format_color(232, 176, 36), # orange 22 | background_color='white'): 23 | """ Creates a jupyter renderer for IFCFiles. 24 | model: the ifcopenshell model to be displayed 25 | size: a tuple (width, height). Must be a square, or shapes will look like deformed 26 | compute_normals_mode: optional, set to SERVER_SIDE by default. This flag lets you choose the 27 | way normals are computed. If SERVER_SIDE is selected (default value), then normals 28 | will be computed by the Tesselator, packed as a python tuple, and send as a json structure 29 | to the client. If, on the other hand, CLIENT_SIDE is chose, then the computer only compute vertex 30 | indices, and let the normals be computed by the client (the web js machine embedded in the webrowser). 31 | * SERVER_SIDE: higher server load, loading time increased, lower client load. Poor performance client will 32 | choose this option (mobile terminals for instance) 33 | * CLIENT_SIDE: lower server load, loading time decreased, higher client load. Higher performance clients will 34 | choose this option (laptops, desktop machines). 35 | * default_shape_color 36 | * default_e1dge_color: 37 | * default_pick_color: 38 | * background_color: 39 | """ 40 | print("fresh init") 41 | settings = ifcopenshell.geom.settings() 42 | settings.set(settings.USE_PYTHON_OPENCASCADE, True) 43 | super().__init__(size=size,compute_normals_mode=compute_normals_mode) 44 | self.register_select_callback(self.ifc_element_click) 45 | 46 | # to_display = list(map(m.by_type for p in display_ents)) 47 | to_display = [] 48 | # print(display_ents) 49 | for ent in display_ents: 50 | to_display.extend(model.by_type(ent)) 51 | self.shapedict = {} 52 | # print(to_display) 53 | for product in to_display: 54 | if (product.Representation is not None and 55 | product.is_a() not in hide_ents) : # some IfcProducts don't have any 3d representation 56 | 57 | 58 | pdct_shape = ifcopenshell.geom.create_shape(settings, inst=product) 59 | r,g,b,alpha = pdct_shape.styles[0] # the shape color 60 | 61 | color = format_color(int(abs(random.random())*255), int(abs(random.random())*255), int(abs(random.random())*255)) 62 | # color = format_color(int(r),int(g),int(b)) 63 | 64 | # self.shapedict[pdct_shape.geometry]=product 65 | # any renderer (threejs, x3dom, jupyter, qt5 based etc.) 66 | self.DisplayShape(pdct_shape.geometry, shape_color = color, transparency=False, opacity=0.5) 67 | 68 | 69 | 70 | def ifc_element_click(self, value): 71 | # ("element click") 72 | # self.html.value(self, value) 73 | 74 | self.html.value += f"Nested Relative: The Element id:{self.shapedict}
{value}" 75 | # self.html.value += f"
{str(self.shapedict[self._current_shape_selection])}
" 76 | 77 | def setColorSelected(self, color): 78 | # for key, val in self.shapedict.items(): 79 | # if val == element: 80 | # TODO : multiple selection 81 | # for shp in self._current_shape_selection: 82 | shp = self._current_shape_selection 83 | # print(self._current_shape_selection.colors) 84 | print(self._current_mesh_selection.material.color) 85 | self._current_mesh_selection.material.color = color 86 | # self.DisplayShape(shp, shape_color = format_color(*color), transparency=False, opacity=0.5) 87 | 88 | 89 | def __repr__(self): 90 | self.Display() 91 | return "" 92 | 93 | 94 | 95 | -------------------------------------------------------------------------------- /models/ifc4 geolocation.ifc: -------------------------------------------------------------------------------- 1 | ISO-10303-21; 2 | HEADER; 3 | FILE_DESCRIPTION(('ViewDefinition [CoordinationView]'),'2;1'); 4 | FILE_NAME('geolocation.blend.ifc','2020-05-12T12:09:46+10:00',(),(),'IfcOpenShell 0.6.0b0','BlenderBIM 0.0.200413','Moult'); 5 | FILE_SCHEMA(('IFC4')); 6 | ENDSEC; 7 | DATA; 8 | #1=IFCSIUNIT(*,.LENGTHUNIT.,$,.METRE.); 9 | #2=IFCSIUNIT(*,.AREAUNIT.,$,.SQUARE_METRE.); 10 | #3=IFCSIUNIT(*,.VOLUMEUNIT.,$,.CUBIC_METRE.); 11 | #4=IFCUNITASSIGNMENT((#1,#2,#3)); 12 | #5=IFCACTORROLE(.ARCHITECT.,$,'Draws the pretty pictures'); 13 | #6=IFCPOSTALADDRESS(.OFFICE.,'Headquarters',$,'Cupboard under the stairs',('221B Baker Street'),$,'MyTown','Middle-Earth','42','Narnia'); 14 | #7=IFCTELECOMADDRESS(.OFFICE.,'Headquarters',$,('0123456789'),$,$,('dion@thinkmoult.com'),'https://thinkmoult.com',('irc://irc.freenode.net##architect')); 15 | #8=IFCPERSON('Moult','Moult','Dion',('Sebastian','Isan','Tan'),('Mr'),('UE'),(#5),(#6,#7)); 16 | #9=IFCACTORROLE(.USERDEFINED.,'CONTRIBUTOR',$); 17 | #10=IFCTELECOMADDRESS(.USERDEFINED.,'The main webpage of the software collection.','WEBPAGE',$,$,$,$,'https://ifcopenshell.org',$); 18 | #11=IFCTELECOMADDRESS(.USERDEFINED.,'The BlenderBIM webpage of the software collection.','WEBPAGE',$,$,$,$,'https://blenderbim.org',$); 19 | #12=IFCTELECOMADDRESS(.USERDEFINED.,'The source code repository of the software collection.','REPOSITORY',$,$,$,$,'https://github.com/IfcOpenShell/IfcOpenShell.git',$); 20 | #13=IFCORGANIZATION($,'IfcOpenShell','IfcOpenShell is an open source (LGPL) software library that helps users and software developers to work with the IFC file format.',(#9),(#10,#11,#12)); 21 | #14=IFCCARTESIANPOINT((0.,0.,0.)); 22 | #15=IFCDIRECTION((0.,0.,1.)); 23 | #16=IFCDIRECTION((1.,0.,0.)); 24 | #17=IFCAXIS2PLACEMENT3D(#14,#15,#16); 25 | #18=IFCPERSONANDORGANIZATION(#8,#13,$); 26 | #19=IFCAPPLICATION(#13,'0.0.200413','BlenderBIM','BlenderBIM'); 27 | #20=IFCOWNERHISTORY(#18,#19,.READWRITE.,.NOTDEFINED.,1589249386,#18,#19,1589249386); 28 | #21=IFCGEOMETRICREPRESENTATIONCONTEXT($,'Model',3,1.E-05,#17,$); 29 | #22=IFCGEOMETRICREPRESENTATIONSUBCONTEXT('Body','Model',*,*,*,*,#21,$,.MODEL_VIEW.,$); 30 | #23=IFCPROJECT('0z0$6As99Fg8k_xxjb4gTX',$,'My Project',$,$,$,$,(#21),#4); 31 | #24=IFCOBJECTIVE('Beauty','The built form should be beautiful',.HARD.,$,$,$,$,$,$,.DESIGNINTENT.,$); 32 | #25=IFCOBJECTIVE('Safety','No facilities exist to generate killer artificial intelligence',.HARD.,$,$,$,$,$,$,.HEALTHANDSAFETY.,$); 33 | #26=IFCSIUNIT(*,.LENGTHUNIT.,$,.METRE.); 34 | #27=IFCPROJECTEDCRS('EPSG:7856','','','EPSG:5111','','',#26); 35 | #28=IFCMAPCONVERSION(#21,#27,334871.85,6252295.02,12.,2.59808,-1.5,1.); 36 | #29=IFCCARTESIANPOINT((-1.,0.,-1.)); 37 | #30=IFCCARTESIANPOINT((1.,0.,1.)); 38 | #31=IFCCARTESIANPOINT((-1.,0.,1.)); 39 | #32=IFCCARTESIANPOINT((1.,0.,-1.)); 40 | #33=IFCCARTESIANPOINT((0.,1.,-1.)); 41 | #34=IFCCARTESIANPOINT((0.333333313465118,-1.,1.)); 42 | #35=IFCCARTESIANPOINT((-0.333333313465118,-1.,1.)); 43 | #36=IFCCARTESIANPOINT((-0.333333313465118,-1.,-1.)); 44 | #37=IFCCARTESIANPOINT((0.333333313465118,-1.,-1.)); 45 | #38=IFCCARTESIANPOINT((0.,1.,1.)); 46 | #39=IFCCARTESIANPOINT((0.333333313465118,0.,-1.)); 47 | #40=IFCCARTESIANPOINT((-0.333333313465118,0.,-1.)); 48 | #41=IFCCARTESIANPOINT((-0.333333313465118,0.,1.)); 49 | #42=IFCCARTESIANPOINT((0.333333313465118,0.,1.)); 50 | #43=IFCPOLYLOOP((#40,#41,#31,#29)); 51 | #44=IFCFACEOUTERBOUND(#43,.T.); 52 | #45=IFCFACE((#44)); 53 | #46=IFCPOLYLOOP((#39,#32,#30,#42)); 54 | #47=IFCFACEOUTERBOUND(#46,.T.); 55 | #48=IFCFACE((#47)); 56 | #49=IFCPOLYLOOP((#29,#31,#38,#33)); 57 | #50=IFCFACEOUTERBOUND(#49,.T.); 58 | #51=IFCFACE((#50)); 59 | #52=IFCPOLYLOOP((#33,#38,#30,#32)); 60 | #53=IFCFACEOUTERBOUND(#52,.T.); 61 | #54=IFCFACE((#53)); 62 | #55=IFCPOLYLOOP((#39,#42,#34,#37)); 63 | #56=IFCFACEOUTERBOUND(#55,.T.); 64 | #57=IFCFACE((#56)); 65 | #58=IFCPOLYLOOP((#42,#30,#38,#31,#41,#35,#34)); 66 | #59=IFCFACEOUTERBOUND(#58,.T.); 67 | #60=IFCFACE((#59)); 68 | #61=IFCPOLYLOOP((#40,#36,#35,#41)); 69 | #62=IFCFACEOUTERBOUND(#61,.T.); 70 | #63=IFCFACE((#62)); 71 | #64=IFCPOLYLOOP((#39,#37,#36,#40,#29,#33,#32)); 72 | #65=IFCFACEOUTERBOUND(#64,.T.); 73 | #66=IFCFACE((#65)); 74 | #67=IFCPOLYLOOP((#37,#34,#35,#36)); 75 | #68=IFCFACEOUTERBOUND(#67,.T.); 76 | #69=IFCFACE((#68)); 77 | #70=IFCCLOSEDSHELL((#45,#48,#51,#54,#57,#60,#63,#66,#69)); 78 | #71=IFCFACETEDBREP(#70); 79 | #72=IFCSHAPEREPRESENTATION(#22,'Body','Brep',(#71)); 80 | #73=IFCREPRESENTATIONMAP(#17,#72); 81 | #74=IFCCOLOURRGB($,0.800000011920929,0.800000011920929,0.800000011920929); 82 | #75=IFCCOLOURRGB($,0.800000011920929,0.800000011920929,0.800000011920929); 83 | #76=IFCSURFACESTYLERENDERING(#74,-0.,#75,$,$,$,$,$,.NOTDEFINED.); 84 | #77=IFCSURFACESTYLE('Material',.BOTH.,(#76)); 85 | #78=IFCSTYLEDITEM($,(#77),'Material'); 86 | #79=IFCSTYLEDREPRESENTATION(#22,$,$,(#78)); 87 | #80=IFCMATERIAL('Material',$,$); 88 | #81=IFCMATERIALDEFINITIONREPRESENTATION('Material',$,(#79),#80); 89 | #82=IFCLOCALPLACEMENT($,#17); 90 | #83=IFCSITE('2Naya6vn181f_Nk1xMGM7f',#20,'My Site',$,$,#82,$,$,$,$,$,$,$,$); 91 | #84=IFCLOCALPLACEMENT(#82,#17); 92 | #85=IFCBUILDING('3FllR6gD94WuJroKZKsPAi',#20,'My Building',$,$,#84,$,$,$,$,$,$); 93 | #86=IFCLOCALPLACEMENT(#84,#17); 94 | #87=IFCBUILDINGSTOREY('05Sow4VP9EL9wRKG9jYu2v',#20,'Ground Floor',$,$,#86,$,$,$,$); 95 | #88=IFCRELAGGREGATES('0XzNn$s_P0php3tNg$s_uY',#20,$,$,#85,(#87)); 96 | #89=IFCRELAGGREGATES('2qe5dYAKv88xk$E7tgxcZP',#20,$,$,#83,(#85)); 97 | #90=IFCRELAGGREGATES('396Tik8RT9jxX7h7TbB34l',#20,$,$,#23,(#83)); 98 | #91=IFCCARTESIANPOINT((0.,0.,0.)); 99 | #92=IFCDIRECTION((0.,0.,1.)); 100 | #93=IFCDIRECTION((1.,0.,0.)); 101 | #94=IFCAXIS2PLACEMENT3D(#91,#92,#93); 102 | #95=IFCLOCALPLACEMENT(#86,#94); 103 | #96=IFCDIRECTION((1.,0.,0.)); 104 | #97=IFCDIRECTION((0.,1.,0.)); 105 | #98=IFCCARTESIANPOINT((0.,0.,0.)); 106 | #99=IFCDIRECTION((0.,0.,1.)); 107 | #100=IFCCARTESIANTRANSFORMATIONOPERATOR3D(#96,#97,#98,1.,#99); 108 | #101=IFCMAPPEDITEM(#73,#100); 109 | #102=IFCSHAPEREPRESENTATION(#22,'Body','MappedRepresentation',(#101)); 110 | #103=IFCPRODUCTDEFINITIONSHAPE($,$,(#102)); 111 | #104=IFCWALL('2NJ_23i9r0G8LCrcGX2cKW',#20,'Cube',$,$,#95,#103,$,.MOVABLE.); 112 | #105=IFCRELCONTAINEDINSPATIALSTRUCTURE('0cvb6qaDLAvxNHVTrO2Zi4',#20,$,$,(#104),#87); 113 | #106=IFCRELASSOCIATESMATERIAL('1ek21YIpT0EBJYIkVxAYc7',#20,$,$,(#104),#80); 114 | ENDSEC; 115 | END-ISO-10303-21; -------------------------------------------------------------------------------- /utils/IfcGraphViz.py: -------------------------------------------------------------------------------- 1 | import ifcopenshell 2 | import uuid 3 | from graphviz import Digraph 4 | 5 | ## If you are reading this, and are interested, please helpme to reimplement this in networkx! 6 | ## TODO Jakob 7 | 8 | class IfcGraphViz(): 9 | node_attr = dict( 10 | shape='record', 11 | align='left', 12 | fontsize='8', 13 | fontname='Arial', 14 | ranksep='0.1', 15 | height='0.2', 16 | width='1' 17 | ) 18 | edge_attr = dict( 19 | fontsize='8', 20 | fontname='Arial', 21 | ) 22 | 23 | 24 | nodes = None 25 | edges = None 26 | graph = None 27 | 28 | 29 | def plot_graph(self, model, node, forward=10, graph=None, direction="LR"): 30 | if not self.graph: 31 | self.graph = Digraph(node_attr=self.node_attr, edge_attr=self.edge_attr) 32 | self.graph.attr(rankdir=direction, fontname="Arial", fontsize="8") 33 | self.nodes = [] 34 | self.edges = [] 35 | 36 | info_str = """""" 37 | if forward >= 0: 38 | for attr, val in node.get_info().items(): 39 | if type(val) == ifcopenshell.entity_instance: 40 | 41 | self.graph = self.plot_graph(model, val, forward=forward-1, graph=self.graph) 42 | self.graph.edge(str(node.id()), str(val.id()), label=attr) 43 | elif type(val) == tuple: 44 | if len(val) == 0: 45 | self.graph = self.plot_graph(model, val[0], forward=forward-1, graph=self.graph) 46 | self.graph.edge(str(node.id()), str(val[0].id()), label=attr) 47 | 48 | 49 | else: 50 | if attr == "id": 51 | info_str += str(f"#{val} \\n") 52 | elif attr == "type": 53 | info_str += str(f"({val}) \\n\\n") 54 | else: 55 | info_str+= str(f"{attr} : {val} \\l") 56 | 57 | #print(info_str) 58 | node = self.graph.node(name=str(node.id()), label=info_str) 59 | 60 | 61 | 62 | return self.graph 63 | 64 | 65 | 66 | def plot_reverse_graph(self, model, node, reverse=3, graph=None, direction="LR"): 67 | 68 | ## TODO: backwards 69 | if not graph: 70 | self.graph = Digraph(node_attr=self.node_attr, edge_attr=self.edge_attr) 71 | self.graph.attr(rankdir=direction, fontname="Arial", fontsize="8") 72 | info_str = f"#{node.id()}" 73 | #self.graph = self.plot_graph(model, node, forward=1, graph=self.graph) 74 | if reverse >= 0 and node: 75 | inverse_rels = model.get_inverse(node) 76 | for inverse in inverse_rels: 77 | #print (f"{type(inverse)}: {inverse}") 78 | if type(inverse) == ifcopenshell.entity_instance: 79 | 80 | for attr, value in inverse.get_info().items(): 81 | if type(value) == ifcopenshell.entity_instance: 82 | if value.id() == inverse.id(): 83 | #self.graph = self.plot_graph(model, inverse, forward=1, graph=self.graph) 84 | 85 | self.graph.node(name=str(value.id()), label=info_str) 86 | self.graph.edge(str(reverse.id()), str(value.id()), label=attr ) 87 | if type(value) == tuple: 88 | for item in value: 89 | if type(item) == ifcopenshell.entity_instance: 90 | if item.id() == node.id(): 91 | #self.graph = self.plot_graph(model, node, forward=1, graph=self.graph) 92 | self.graph.node(name=str(item.id()), label=info_str) 93 | self.graph.edge(str(inverse.id()), str(item.id()), label=attr) 94 | 95 | 96 | #print(info_str) 97 | node = self.graph.node(name=str(node.id()), label=info_str) 98 | return self.graph 99 | 100 | 101 | 102 | 103 | 104 | def plot_relation_graph(self, m, node, relationships=["IfcRelContainedInSpatialStructure", "IfcRelVoidsElement", "IfcRelFillsElement", "IfcRelAggregates"], 105 | depth=1): 106 | dot = Digraph(node_attr=self.node_attr, edge_attr=self.edge_attr) 107 | dot.attr(rankdir='TD', fontname="Arial", fontsize="8") 108 | # relations = m.by_type("IfcRelationship") 109 | temp_relations = m.get_inverse(node) 110 | 111 | relations = [] 112 | for rel in temp_relations: 113 | #print(rel) 114 | if rel.is_a().startswith("IfcRel"): 115 | #print(f"added {rel}") 116 | relations.append(rel) 117 | 118 | for relation in relations: 119 | 120 | if relation.is_a() in relationships or len(relationships)==0: 121 | relatedNodeId = None 122 | 123 | # dot.node(relation.GlobalId) 124 | for key, value in relation.get_info().items(): 125 | if key.startswith("Relating") and value.id == node.id: 126 | # print("Relating")) 127 | dot.node( getattr(value, "GlobalId", f'#{value.id}'), f'name:{getattr(value, "Name", value.id )} ‚\n {value.is_a()}') 128 | relatedNodeId = getattr(value, "GlobalId", f"#{value.id}") 129 | 130 | 131 | if key.startswith("Related"): 132 | # print (type(value)) 133 | if type(value) == ifcopenshell.entity_instance: 134 | 135 | dot.node( getattr(value, "GlobalId", "No Id"), getattr(value, "Name", "NaN")) 136 | elif type (value) == tuple: 137 | for related in value: 138 | # print (value) 139 | dot.node(getattr(related, "GlobalId", "No GlobalId"), f'name:{getattr(related, "Name", "no name")}\n {related.is_a()} ') 140 | 141 | for key, value in relation.get_info().items(): 142 | 143 | 144 | if key.startswith("Related"): 145 | # print (type(value)) 146 | if type(value) == ifcopenshell.entity_instance and relatedNodeId: 147 | dot.edge(relatedNodeId, getattr(value, "GlobalId", "none")) 148 | elif relatedNodeId: 149 | for related in value: 150 | 151 | dot.edge(relatedNodeId, getattr(related, "GlobalId", "none")) 152 | 153 | 154 | # print (f'\t\t{key} : \t\t\t{value}') 155 | return dot -------------------------------------------------------------------------------- /utils/JupyterIFCRenderer.py: -------------------------------------------------------------------------------- 1 | import os 2 | import random 3 | from OCC.Display.WebGl.jupyter_renderer import JupyterRenderer, format_color, NORMAL, BoundingBox 4 | from ipywidgets import interact, interactive, fixed, interact_manual, IntSlider, Layout, FloatSlider 5 | import ipywidgets as widgets 6 | 7 | import OCC.Core, OCC.Core.gp 8 | import ifcopenshell, ifcopenshell.geom 9 | from OCC.Core.BRepPrimAPI import BRepPrimAPI_MakeBox, BRepPrimAPI_MakeSphere 10 | from pythreejs import Plane 11 | 12 | 13 | class JupyterIFCRenderer(JupyterRenderer): 14 | colors_dict = { "IfcWall": (50,50,50)} 15 | 16 | 17 | def __init__(self, 18 | model, 19 | display_ents = ["IfcProduct"], 20 | hide_ents = ["IfcOpeningElement", "IfcSpace"], 21 | size=(640, 480), 22 | compute_normals_mode=NORMAL.CLIENT_SIDE, 23 | default_shape_color=format_color(166, 166, 166), # light grey 24 | default_edge_color=format_color(32, 32, 32), # dark grey 25 | default_vertex_color=format_color(8, 8, 8), # darker grey 26 | pick_color=format_color(232, 176, 36), # orange 27 | background_color='white'): 28 | """ Creates a jupyter renderer for IFCFiles. 29 | model: the ifcopenshell model to be displayed 30 | size: a tuple (width, height). Must be a square, or shapes will look like deformed 31 | compute_normals_mode: optional, set to SERVER_SIDE by default. This flag lets you choose the 32 | way normals are computed. If SERVER_SIDE is selected (default value), then normals 33 | will be computed by the Tesselator, packed as a python tuple, and send as a json structure 34 | to the client. If, on the other hand, CLIENT_SIDE is chose, then the computer only compute vertex 35 | indices, and let the normals be computed by the client (the web js machine embedded in the webrowser). 36 | * SERVER_SIDE: higher server load, loading time increased, lower client load. Poor performance client will 37 | choose this option (mobile terminals for instance) 38 | * CLIENT_SIDE: lower server load, loading time decreased, higher client load. Higher performance clients will 39 | choose this option (laptops, desktop machines). 40 | * default_shape_color 41 | * default_e1dge_color: 42 | * default_pick_color: 43 | * background_color: 44 | """ 45 | 46 | settings = ifcopenshell.geom.settings() 47 | settings.set(settings.USE_PYTHON_OPENCASCADE, True) 48 | super().__init__(size=size,compute_normals_mode=compute_normals_mode) 49 | self.register_select_callback(self.ifc_element_click) 50 | 51 | 52 | to_display = [] 53 | for ent in display_ents: 54 | to_display.extend(model.by_type(ent, True)) 55 | # print(display_ents) 56 | self.shapedict = {} 57 | self.elementdict = {} 58 | self._meshdict = {} 59 | self.colorcache = {} 60 | self.highlight_color = "#EE2222" 61 | schema = model.wrapped_data.schema 62 | # print(to_display) 63 | 64 | for product in to_display: 65 | # some IfcProducts don't have any 3d representation 66 | if (product.Representation is not None ) : 67 | 68 | pdct_shape = ifcopenshell.geom.create_shape(settings, inst=product) 69 | r,g,b,alpha = pdct_shape.styles[0] # the shape color 70 | # Styles come as floats 0 <= style <= 1 71 | if r == g == b == -1: 72 | r = b = g = 0.7 73 | color = format_color(int(abs(r)*255),int(abs(g*255)),int(abs(b)*255)) 74 | 75 | self.shapedict[pdct_shape.geometry]=product 76 | self.elementdict[product] = pdct_shape.geometry 77 | self.colorcache[pdct_shape.geometry] = color 78 | 79 | # any renderer (threejs, x3dom, jupyter, qt5 based etc.) 80 | self.DisplayShape(pdct_shape.geometry, shape_color = color, transparency=False, opacity=0.5) 81 | # 82 | for ent in hide_ents: 83 | to_hide = model.by_type(ent) 84 | for p in to_hide: 85 | self.setVisible(p, False) 86 | 87 | for meshid, shape in self._shapes.items(): 88 | product = self.shapedict[shape] 89 | #self._meshdict[product] = meshid 90 | self._meshdict[product] = list(filter(lambda mesh: mesh.name == meshid, self._displayed_pickable_objects.children))[0] 91 | 92 | if self._shapes: 93 | self._bb = BoundingBox([self._shapes.values()]) 94 | else: # if nothing registered yet, create a fake bb 95 | self._bb = BoundingBox([[BRepPrimAPI_MakeSphere(5.).Shape()]]) 96 | 97 | self._sectionXSlider = FloatSlider(name = "section plane X", layout=Layout(width='200px'), min=self._bb.ymin-1, max=self._bb.ymax+1, value=self._bb.ymax+1,step=0.1) 98 | self._sectionXSlider.observe(self._sectionPlaneX, "value") 99 | self._controls.append(self._sectionXSlider) 100 | 101 | self._sectionZSlider = FloatSlider(name = "section plane Z", layout=Layout(width='200px'), min=self._bb.zmin-1, max=self._bb.zmax+1, value=self._bb.zmax+1,step=0.1) 102 | self._sectionZSlider.observe(self._sectionPlaneZ, "value") 103 | self._controls.append(self._sectionZSlider) 104 | 105 | def ifc_element_click(self, value): 106 | # ("element click") 107 | # self.html.value(self, value) 108 | #print("click") 109 | product = self.shapedict[value] 110 | self.html.value += f"{product.is_a()}
" 111 | self.html.value += "" 112 | 113 | for key, value in product.get_info().items(): 114 | 115 | self.html.value += f"" 116 | 117 | self.html.value += "
{str(key)}: {str(value)}
" 118 | 119 | 120 | def setColorSelected(self, color): 121 | # for key, val in self.shapedict.items(): 122 | # if val == element: 123 | # TODO : multiple selection 124 | # for shp in self._current_shape_selection: 125 | shp = self._current_shape_selection 126 | # print(self._current_shape_selection.colors) 127 | print(self._current_mesh_selection.material.color) 128 | self._current_mesh_selection.material.color = color 129 | # self.DisplayShape(shp, shape_color = format_color(*color), transparency=False, opacity=0.5) 130 | 131 | 132 | def setHighlighColor(self, color): 133 | self.hl = color 134 | 135 | def highlightShape(self, element): 136 | shape = list(self.shapedict.keys())[list(self.shapedict.values()).index(element)] 137 | color=format_color(166, 166, 166) 138 | # self.DisplayShape(shape, shape_color = color, transparency=False, opacity=0.5) 139 | 140 | 141 | # def changeColor(self, element, color): 142 | # for self.elementdict.values 143 | 144 | def resetHighlight(self): 145 | for p, c in self.colorcache.items(): 146 | self.DisplayShape(p, shape_color = c) 147 | 148 | def setColorProduct(self, product, color): 149 | mesh = self._meshdict.get(product, None) 150 | if mesh: 151 | mesh.material.color = color 152 | 153 | # for shp in self._displayed_pickable_objects.children: 154 | # if self.elementdict[product] 155 | # print (f"Shape: {shp} \t\t\t\t {product}" ) 156 | # if self._shapedict 157 | 158 | 159 | def setAllTransparent(self): 160 | for shp in self._displayed_pickable_objects.children: 161 | shp.material.opacity = 0.0001 162 | shp.material.transparent = True 163 | shp.material.alpha = 0.1 164 | 165 | def setTransparentTrue(self, product): 166 | mesh = self._meshdict.get(product, None) 167 | if mesh: 168 | #print(mesh) 169 | mesh.material.opacity = 0.1 170 | mesh.transparent = True 171 | mesh.material.alpha = 0.1 172 | 173 | def setTransparentFalse(self, product): 174 | mesh = self._meshdict.get(product, None) 175 | if mesh: 176 | #print(mesh) 177 | mesh.material.opacity = 1 178 | mesh.transparent = False 179 | mesh.material.alpha = 1 180 | 181 | 182 | 183 | def resetTransparency (self, product): 184 | mesh = self._meshdict.get(product, None) 185 | if mesh: 186 | mesh.material.opacity = 1 187 | mesh.transparent = False 188 | mesh.material.alpha = 1 189 | 190 | def setColor(self, product, color): 191 | mesh = self._meshdict.get(product, None) 192 | if mesh: 193 | #print(mesh) 194 | mesh.material.color = color 195 | 196 | 197 | def setVisible(self, product, visible): 198 | mesh = self._meshdict.get(product, None) 199 | if mesh: 200 | #print(mesh) 201 | mesh.visible = visible 202 | 203 | 204 | def highlight(self, product): 205 | mesh = self._meshdict.get(product, None) 206 | if mesh: 207 | #print(mesh) 208 | mesh.material.color = self.highlight_color 209 | 210 | def __repr__(self): 211 | self.Display() 212 | return "" 213 | 214 | def _sectionPlaneX(self, x): 215 | self._renderer.localClippingEnabled = True; 216 | self._renderer.clippingPlanes = [Plane((0,-1,0), x['new'])] 217 | 218 | def _sectionPlaneZ(self, z): 219 | self._renderer.localClippingEnabled = True; 220 | self._renderer.clippingPlanes = [Plane((0,0,-1), z['new'])] 221 | 222 | 223 | def getSelectedProduct(self): 224 | shp = self._current_shape_selection 225 | product = self.shapedict.get(shp, None) 226 | return product 227 | 228 | 229 | def setDefaultColors(self): 230 | for p,m in self._meshdict.items(): 231 | if p.is_a() in self.COLOR_MAPPINGS.keys(): 232 | print(f"{p.Name}, {p.is_a()} \t\t\t {self.DEFAULT_COLORS.get(self.COLOR_MAPPINGS.get(p.is_a()))}") 233 | m.material.color = self.DEFAULT_COLORS.get(self.COLOR_MAPPINGS.get(p.is_a()),"#333333") 234 | else: 235 | m.material.color = "#AAAAAA" 236 | 237 | 238 | DEFAULT_COLORS = { 239 | 'dark-electric-blue': '#527589', 240 | 'little-boy-blue' : '#64a1ec', 241 | 'black-coral' : '#525e71', 242 | 'space-cadet' : '#272d3f', 243 | 'brown-sugar' : '#be7752', 244 | 'pale-silver' : '#c0bab1', 245 | 'imperial-red' : '#e63946', 246 | 'honeydew' : '#f1faee', 247 | 'powder-blue' : '#a8dadc', 248 | 'celadon-blue' : '#457b9d', 249 | 'prussian-blue' : '#1d3557', 250 | 'Khaki Web' : '#C6AC8F' 251 | } 252 | COLOR_MAPPINGS = { 253 | 'IfcWall' : 'pale-silver', 254 | 'IfcWallStandardCase' : 'pale-silver', 255 | 'IfcWindow' : 'celdon-blue', 256 | 'IfcFloor' : 'brown-sugar', 257 | 'IfcSlab' : 'Khaki Web', 258 | 'IfcDoor' : 'honeydew' 259 | 260 | } 261 | -------------------------------------------------------------------------------- /1. geolocation.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "## IFC Coordinate Reference Systems\n", 8 | "\n", 9 | "The following notebook attempts to provide a visual enhancement of the original blog-post written by **Dion Moult**: (https://thinkmoult.com/ifc-coordinate-reference-systems-and-revit.html). I've also added some commentary of my own, as I've found this subject often requires a re-read or two to fully grasp the comments. As you read, feel free to add your own comments!\n", 10 | "\n", 11 | "In addition, users may find the accompanying code useful if they want to learn more about using *IfcOpenShell* (http://ifcopenshell.org/).\n", 12 | "\n", 13 | "Special thanks to **Jakob Beetz** for his great work on the original ifcopenshell-notebooks, which can be found here: https://github.com/jakob-beetz/ifcopenshell-notebooks" 14 | ] 15 | }, 16 | { 17 | "cell_type": "code", 18 | "execution_count": 3, 19 | "metadata": {}, 20 | "outputs": [], 21 | "source": [ 22 | "#First, lets import ifcopenshell and load our IFC file:\n", 23 | "import ifcopenshell\n", 24 | "file = ifcopenshell.open(\"models/ifc4 geolocation.ifc\") #Thanks Dion for the file ;)" 25 | ] 26 | }, 27 | { 28 | "cell_type": "markdown", 29 | "metadata": {}, 30 | "source": [ 31 | "#### Coordinate systems defined by **IfcProject**:\n", 32 | "\n", 33 | "Let’s talk about what we want the results to be. According to the IFC specification, IfcProject provides the following information:\n", 34 | "\n", 35 | "- the project coordinate system\n", 36 | "- the coordinate space dimensions\n", 37 | "- the precision used within the geometric representations\n", 38 | "- (optionally) the indication of the true north\n", 39 | "- (optionally) the map conversion between the project coordinate system and the geospatial coordinate reference system.\n", 40 | "\n", 41 | "This information is provided using the **RepresentationContexts** relationship of the IfcProject. This relationship will contain one or more **IfcGeometricRepresentationContext** elements. Each will typically have a **CoordinateSpaceDimension** of 3, to show a 3D model, and the **Precision** attribute shows the model precision.\n", 42 | "\n", 43 | "Let's see how this is defined in the sample file:" 44 | ] 45 | }, 46 | { 47 | "cell_type": "code", 48 | "execution_count": 4, 49 | "metadata": {}, 50 | "outputs": [ 51 | { 52 | "data": { 53 | "text/plain": [ 54 | "{'id': 23,\n", 55 | " 'type': 'IfcProject',\n", 56 | " 'GlobalId': '0z0$6As99Fg8k_xxjb4gTX',\n", 57 | " 'OwnerHistory': None,\n", 58 | " 'Name': 'My Project',\n", 59 | " 'Description': None,\n", 60 | " 'ObjectType': None,\n", 61 | " 'LongName': None,\n", 62 | " 'Phase': None,\n", 63 | " 'RepresentationContexts': (#21=IfcGeometricRepresentationContext($,'Model',3,1.E-05,#17,$),),\n", 64 | " 'UnitsInContext': #4=IfcUnitAssignment((#1,#2,#3))}" 65 | ] 66 | }, 67 | "execution_count": 4, 68 | "metadata": {}, 69 | "output_type": "execute_result" 70 | } 71 | ], 72 | "source": [ 73 | "project = file.by_type(\"IfcProject\")[0]\n", 74 | "\n", 75 | "# Let's look at the project attributes first:\n", 76 | "project.get_info()" 77 | ] 78 | }, 79 | { 80 | "cell_type": "code", 81 | "execution_count": 5, 82 | "metadata": {}, 83 | "outputs": [ 84 | { 85 | "data": { 86 | "text/plain": [ 87 | "{'id': 21,\n", 88 | " 'type': 'IfcGeometricRepresentationContext',\n", 89 | " 'ContextIdentifier': None,\n", 90 | " 'ContextType': 'Model',\n", 91 | " 'CoordinateSpaceDimension': 3,\n", 92 | " 'Precision': 1e-05,\n", 93 | " 'WorldCoordinateSystem': #17=IfcAxis2Placement3D(#14,#15,#16),\n", 94 | " 'TrueNorth': None}" 95 | ] 96 | }, 97 | "execution_count": 5, 98 | "metadata": {}, 99 | "output_type": "execute_result" 100 | } 101 | ], 102 | "source": [ 103 | "# And now specifically the RepresentationContext:\n", 104 | "project.RepresentationContexts[0].get_info()" 105 | ] 106 | }, 107 | { 108 | "cell_type": "markdown", 109 | "metadata": {}, 110 | "source": [ 111 | "#### The WorldCoordinateSystem attribute\n", 112 | "If we dive deeper into **WorldCoordinateSystem** we can find out how the coordinate system for the virtual world is defined:\n", 113 | "\n", 114 | "Usually, this will be set to (0, 0, 0), and represents the origin of the virtual world. In other words, any element in a project usually inherits the local relative placement of its parent, all the way up to **IfcSite**, but somewhere, it needs to end in an absolute coordinate. This **WorldCoordinateSystem** is the final absolute coordinate that is not relative to anything else. It can therefore be used to offset everything in your project, should you want to. The **IfcMapConversion** described below will then be used to convert our virtual world into the real world." 115 | ] 116 | }, 117 | { 118 | "cell_type": "code", 119 | "execution_count": 6, 120 | "metadata": {}, 121 | "outputs": [ 122 | { 123 | "data": { 124 | "image/svg+xml": "\n\n\n\n\n\n\n\n\n14\n\n#14 \n(IfcCartesianPoint) \n\n\n\n17\n\n#17 \n(IfcAxis2Placement3D) \n\n\n\n17->14\n\n\nLocation\n\n\n\n15\n\n#15 \n(IfcDirection) \n\n\n\n17->15\n\n\nAxis\n\n\n\n16\n\n#16 \n(IfcDirection) \n\n\n\n17->16\n\n\nRefDirection\n\n\n\n21\n\n#21 \n(IfcGeometricRepresentationContext) \nContextIdentifier : None \nContextType : Model \nCoordinateSpaceDimension : 3 \nPrecision : 1e-05 \nTrueNorth : None \n\n\n\n21->17\n\n\nWorldCoordinateSystem\n\n\n\n", 125 | "text/plain": [ 126 | "" 127 | ] 128 | }, 129 | "execution_count": 6, 130 | "metadata": {}, 131 | "output_type": "execute_result" 132 | } 133 | ], 134 | "source": [ 135 | "from utils import IfcGraphViz\n", 136 | "graph = IfcGraphViz.IfcGraphViz().plot_graph(file, project.RepresentationContexts[0])\n", 137 | "graph" 138 | ] 139 | }, 140 | { 141 | "cell_type": "code", 142 | "execution_count": 7, 143 | "metadata": {}, 144 | "outputs": [ 145 | { 146 | "data": { 147 | "text/plain": [ 148 | "{'id': 17,\n", 149 | " 'type': 'IfcAxis2Placement3D',\n", 150 | " 'Location': #14=IfcCartesianPoint((0.,0.,0.)),\n", 151 | " 'Axis': #15=IfcDirection((0.,0.,1.)),\n", 152 | " 'RefDirection': #16=IfcDirection((1.,0.,0.))}" 153 | ] 154 | }, 155 | "execution_count": 7, 156 | "metadata": {}, 157 | "output_type": "execute_result" 158 | } 159 | ], 160 | "source": [ 161 | "# As we can see below, the coordinates (0,0,0) are used for the origin. See Location.\n", 162 | "project.RepresentationContexts[0].WorldCoordinateSystem.get_info()" 163 | ] 164 | }, 165 | { 166 | "cell_type": "markdown", 167 | "metadata": {}, 168 | "source": [ 169 | "#### Project Coordinate System\n", 170 | "The actual project coordinate system is defined by the **HasCoordinateOperation** relationship. This holds an *IfcMapConversion* element, with all sorts of useful attributes. Let’s go through them below:\n", 171 | "- **SourceCRS**: refers back to the **IfcGeometricRepresentationContext** of the IfcProject to establish the inverse relationship\n", 172 | "- **TargetCRS**: refers to the CRS used in the project. This will hold an **IfcCoordinateReferenceSystem**, or its subtype **IfcProjectedCRS** (We'll look at this in more detail below).\n", 173 | "- **Eastings**: your IfcProject’s world **IfcGeometricRepresentationContext**’s 0,0,0 origin will correlate to this number. In Sydney, if your building is the Sydney Opera House, this’ll be something like 334902.775. If you have specified a **MapUnit** in the **ProjectedCRS** you should use that unit (e.g. meters). Otherwise, you should use the project units (e.g. millimeters).\n", 174 | "- **Northings**: same as Eastings, but for the Y axis. For the Sydney Opera House, it’ll be something like 6252274.139.\n", 175 | "- **OrthogonalHeight**: continuing our example, this’ll be the AHD of our world origin. Wikipedia says it is 4m in elevation, so I guess it’ll be something like 4. In this case, we keep the same units as Eastings and Northings, so that we can apply a uniform scale afterwords.\n", 176 | "- **XAxisAbscissa**: specifies the local X axis vector along the easting to determine rotation of the local coordinates. If there is no rotation, this will be 1.\n", 177 | "- **XAxisOrdinate**: specifies the local X axis vector along the northing to determine rotation of the local coordinates. If there is no rotation, this will be 0.\n", 178 | "- **Scale**: Our local (source) coordinate system is usually in millimeters, and the target coordinate system (MGA56) is in meters, so the scale conversion will be something around 0.001. Keep in mind that it is unlikely to be exactly 0.001. This is because the scale isn’t primarily about units, it is the scaling factor of the Helmert transformation, which takes into account curvature of the Earth and local site topograpy. Your surveyor can calculate the actual value.\n", 179 | "\n", 180 | "Let's see what this looks like in our sample file. First, let's find **HasCoordinateOperation** - This is an inverse attribute of **IfcGeometricRepresentationContext**:" 181 | ] 182 | }, 183 | { 184 | "cell_type": "code", 185 | "execution_count": 8, 186 | "metadata": {}, 187 | "outputs": [ 188 | { 189 | "name": "stdout", 190 | "output_type": "stream", 191 | "text": [ 192 | "HasCoordinateOperation: (#28=IfcMapConversion(#21,#27,334871.85,6252295.02,12.,2.59808,-1.5,1.),)\n", 193 | "HasSubContexts: (#22=IfcGeometricRepresentationSubContext('Body','Model',*,*,*,*,#21,$,.MODEL_VIEW.,$),)\n" 194 | ] 195 | } 196 | ], 197 | "source": [ 198 | "representation_context = project.RepresentationContexts[0]\n", 199 | "for key in dir(representation_context):\n", 200 | " if (\n", 201 | " not key[0].isalpha()\n", 202 | " or key[0] != key[0].upper()\n", 203 | " or key in representation_context.get_info()\n", 204 | " or not getattr(representation_context, key)\n", 205 | " ):\n", 206 | " continue\n", 207 | " print(f\"{key}: \",getattr(representation_context, key))\n", 208 | " \n", 209 | " # Our inverse attributes are: " 210 | ] 211 | }, 212 | { 213 | "cell_type": "markdown", 214 | "metadata": {}, 215 | "source": [ 216 | "We know know that IfcMapConversion has an ID of **#28**, so let's go ahead and visualize this:" 217 | ] 218 | }, 219 | { 220 | "cell_type": "code", 221 | "execution_count": 9, 222 | "metadata": {}, 223 | "outputs": [ 224 | { 225 | "data": { 226 | "image/svg+xml": "\n\n\n\n\n\n\n\n\n14\n\n#14 \n(IfcCartesianPoint) \n\n\n\n17\n\n#17 \n(IfcAxis2Placement3D) \n\n\n\n17->14\n\n\nLocation\n\n\n\n15\n\n#15 \n(IfcDirection) \n\n\n\n17->15\n\n\nAxis\n\n\n\n16\n\n#16 \n(IfcDirection) \n\n\n\n17->16\n\n\nRefDirection\n\n\n\n21\n\n#21 \n(IfcGeometricRepresentationContext) \nContextIdentifier : None \nContextType : Model \nCoordinateSpaceDimension : 3 \nPrecision : 1e-05 \nTrueNorth : None \n\n\n\n21->17\n\n\nWorldCoordinateSystem\n\n\n\n28\n\n#28 \n(IfcMapConversion) \nEastings : 334871.85 \nNorthings : 6252295.02 \nOrthogonalHeight : 12.0 \nXAxisAbscissa : 2.59808 \nXAxisOrdinate : -1.5 \nScale : 1.0 \n\n\n\n28->21\n\n\nSourceCRS\n\n\n\n27\n\n#27 \n(IfcProjectedCRS) \nName : EPSG:7856 \nDescription : \nGeodeticDatum : \nVerticalDatum : EPSG:5111 \nMapProjection : \nMapZone : \n\n\n\n28->27\n\n\nTargetCRS\n\n\n\n26\n\n#26 \n(IfcSIUnit) \nDimensions : None \nUnitType : LENGTHUNIT \nPrefix : None \nName : METRE \n\n\n\n27->26\n\n\nMapUnit\n\n\n\n", 227 | "text/plain": [ 228 | "" 229 | ] 230 | }, 231 | "execution_count": 9, 232 | "metadata": {}, 233 | "output_type": "execute_result" 234 | } 235 | ], 236 | "source": [ 237 | "IfcGraphViz.IfcGraphViz().plot_graph(file, file.by_id(28))" 238 | ] 239 | }, 240 | { 241 | "cell_type": "markdown", 242 | "metadata": {}, 243 | "source": [ 244 | "This **IfcMapConversion** and **IfcProjectedCRS** element of the **IfcProject**’s **IfcGeometricRepresentationContext** holds all of the georeferencing information that we require. These attributes contains all of the parameters required to perform a “Helmert transformation”, which is a fancy way of saying how to offset, rotate, and scale local project coordinates to a globally positioned coordinate system. For your surveyor to provide these transformation parameters properly, they will need multiple surveyed points (a minimum of two), ideally taken at extremes across the site, in both your local coordinates, as well as their equivalents in the target CRS. They will also need to know your desired building orientation (i.e. project north) to calculate the X axis abcissa and ordinate, and a nominated false origin to set the **Eastings** and **Northings**. The more points that are surveyed, the more accurate this **IfcMapConversion** will become.\n", 245 | "\n", 246 | "With all of the information defined above, to convert from local coordinates (X, Y, Z), to map grid coordinates (X', Y', Z'), you can use these relationships:\n", 247 | "\n", 248 | "![](img/ifcmapconversion-relationship.png)\n", 249 | "\n", 250 | "After all of this information is recorded, it’s interesting to note that the **IfcGeometricRepresentationContext** additionally has a **TrueNorth** attribute. Assuming the **IfcMapConversion** is already provided, there is actually no need for a **TrueNorth** attribute, and so if it is provided, it is merely duplicate data and there for convenience. IFC readers should not parse it and should not apply the same rotation twice. The **IfcMapConversion** takes priority over the **TrueNorth** attribute." 251 | ] 252 | }, 253 | { 254 | "cell_type": "markdown", 255 | "metadata": {}, 256 | "source": [ 257 | "#### **Coordinate system inheritance**\n", 258 | "The **IfcSite** is spatially contained in the **IfcProject**. However, the spatial containment is not the determining factor for how coordinates are inherited. Instead, the **IfcSite** has an **ObjectPlacement** and a **Representation** attribute. These are the important attributes to pay attention to. Let's take a closer look in our sample file:\n", 259 | "\n", 260 | "The **ObjectPlacement** attribute positions the **IfcSite** element relative to other objects. We will discuss the different placements below, but suffice to say that it merely deals with relative offsets of coordinates." 261 | ] 262 | }, 263 | { 264 | "cell_type": "code", 265 | "execution_count": 10, 266 | "metadata": {}, 267 | "outputs": [ 268 | { 269 | "data": { 270 | "text/plain": [ 271 | "{'id': 83,\n", 272 | " 'type': 'IfcSite',\n", 273 | " 'GlobalId': '2Naya6vn181f_Nk1xMGM7f',\n", 274 | " 'OwnerHistory': #20=IfcOwnerHistory(#18,#19,.READWRITE.,.NOTDEFINED.,1589249386,#18,#19,1589249386),\n", 275 | " 'Name': 'My Site',\n", 276 | " 'Description': None,\n", 277 | " 'ObjectType': None,\n", 278 | " 'ObjectPlacement': #82=IfcLocalPlacement($,#17),\n", 279 | " 'Representation': None,\n", 280 | " 'LongName': None,\n", 281 | " 'CompositionType': None,\n", 282 | " 'RefLatitude': None,\n", 283 | " 'RefLongitude': None,\n", 284 | " 'RefElevation': None,\n", 285 | " 'LandTitleNumber': None,\n", 286 | " 'SiteAddress': None}" 287 | ] 288 | }, 289 | "execution_count": 10, 290 | "metadata": {}, 291 | "output_type": "execute_result" 292 | } 293 | ], 294 | "source": [ 295 | "site = file.by_type(\"IfcSite\")[0]\n", 296 | "site.get_info()" 297 | ] 298 | }, 299 | { 300 | "cell_type": "markdown", 301 | "metadata": {}, 302 | "source": [ 303 | "The **Representation** attribute, however, contains an **IfcRepresentationContext** chosen from the list of contexts defined at the **IfcProject** level. It is this particular selection of the **IfcRepresentationContext** that allows the **IfcSite** to inherit a particular **WorldCoordinateSystem** and **MapConversion** attribute defined at the **IfcProject** level. (Note: In our sample model, **IfcSite** doesn't inherit from **IfcProject**)\n", 304 | "\n", 305 | "I would like to emphasize that the inheritance of coordinate transformation is not done due to spatial containment, but instead due to the selection of **IfcRepresentationContext**. This allows different IfcSite elements to have a different **IfcRepresentationContext**, and therefore have a different MapConversion. This is useful if you are working on a small town or any geographically large projects, as different sites will likely require different Helmert transformations. That said, I have heard talk that the **IfcMapConversion** could be moved to be defined at the **IfcSite** level, instead of the **IfcProject**.\n", 306 | "\n", 307 | "In fact, any IFC product that has a representation can select its own context. Let's look at our sample model:" 308 | ] 309 | }, 310 | { 311 | "cell_type": "code", 312 | "execution_count": 16, 313 | "metadata": {}, 314 | "outputs": [ 315 | { 316 | "data": { 317 | "application/vnd.jupyter.widget-view+json": { 318 | "model_id": "5a6dafed4c254cdca6061114edc0cd66", 319 | "version_major": 2, 320 | "version_minor": 0 321 | }, 322 | "text/plain": [ 323 | "HBox(children=(VBox(children=(HBox(children=(Checkbox(value=True, description='Axes', layout=Layout(height='au…" 324 | ] 325 | }, 326 | "metadata": {}, 327 | "output_type": "display_data" 328 | }, 329 | { 330 | "data": { 331 | "text/plain": [] 332 | }, 333 | "execution_count": 16, 334 | "metadata": {}, 335 | "output_type": "execute_result" 336 | } 337 | ], 338 | "source": [ 339 | "# First, let's load some fancy 3d graphics ;) - Thanks again to Jakob Beetz for the code adaptation 😀\n", 340 | "from utils.JupyterIFCRenderer import JupyterIFCRenderer\n", 341 | "viewer = JupyterIFCRenderer(file, size=(600,500))\n", 342 | "viewer" 343 | ] 344 | }, 345 | { 346 | "cell_type": "code", 347 | "execution_count": 17, 348 | "metadata": {}, 349 | "outputs": [ 350 | { 351 | "data": { 352 | "text/plain": [ 353 | "#104=IfcWall('2NJ_23i9r0G8LCrcGX2cKW',#20,'Cube',$,$,#95,#103,$,.MOVABLE.)" 354 | ] 355 | }, 356 | "execution_count": 17, 357 | "metadata": {}, 358 | "output_type": "execute_result" 359 | } 360 | ], 361 | "source": [ 362 | "selection = viewer.getSelectedProduct()\n", 363 | "selection" 364 | ] 365 | }, 366 | { 367 | "cell_type": "code", 368 | "execution_count": 18, 369 | "metadata": {}, 370 | "outputs": [ 371 | { 372 | "data": { 373 | "text/plain": [ 374 | "{'id': 104,\n", 375 | " 'type': 'IfcWall',\n", 376 | " 'GlobalId': '2NJ_23i9r0G8LCrcGX2cKW',\n", 377 | " 'OwnerHistory': #20=IfcOwnerHistory(#18,#19,.READWRITE.,.NOTDEFINED.,1589249386,#18,#19,1589249386),\n", 378 | " 'Name': 'Cube',\n", 379 | " 'Description': None,\n", 380 | " 'ObjectType': None,\n", 381 | " 'ObjectPlacement': #95=IfcLocalPlacement(#86,#94),\n", 382 | " 'Representation': #103=IfcProductDefinitionShape($,$,(#102)),\n", 383 | " 'Tag': None,\n", 384 | " 'PredefinedType': 'MOVABLE'}" 385 | ] 386 | }, 387 | "execution_count": 18, 388 | "metadata": {}, 389 | "output_type": "execute_result" 390 | } 391 | ], 392 | "source": [ 393 | "# We know the wall has an ID of #104, so let's have a look at its attributes:\n", 394 | "wall = file.by_id(104)\n", 395 | "representation = wall.Representation\n", 396 | "wall.get_info()" 397 | ] 398 | }, 399 | { 400 | "cell_type": "code", 401 | "execution_count": 19, 402 | "metadata": {}, 403 | "outputs": [ 404 | { 405 | "data": { 406 | "image/svg+xml": "\n\n\n\n\n\n\n\n\n14\n\n#14 \n(IfcCartesianPoint) \n\n\n\n17\n\n#17 \n(IfcAxis2Placement3D) \n\n\n\n17->14\n\n\nLocation\n\n\n\n17->14\n\n\nLocation\n\n\n\n17->14\n\n\nLocation\n\n\n\n15\n\n#15 \n(IfcDirection) \n\n\n\n17->15\n\n\nAxis\n\n\n\n17->15\n\n\nAxis\n\n\n\n17->15\n\n\nAxis\n\n\n\n16\n\n#16 \n(IfcDirection) \n\n\n\n17->16\n\n\nRefDirection\n\n\n\n17->16\n\n\nRefDirection\n\n\n\n17->16\n\n\nRefDirection\n\n\n\n82\n\n#82 \n(IfcLocalPlacement) \nPlacementRelTo : None \n\n\n\n82->17\n\n\nRelativePlacement\n\n\n\n84\n\n#84 \n(IfcLocalPlacement) \n\n\n\n84->17\n\n\nRelativePlacement\n\n\n\n84->82\n\n\nPlacementRelTo\n\n\n\n86\n\n#86 \n(IfcLocalPlacement) \n\n\n\n86->17\n\n\nRelativePlacement\n\n\n\n86->84\n\n\nPlacementRelTo\n\n\n\n95\n\n#95 \n(IfcLocalPlacement) \n\n\n\n95->86\n\n\nPlacementRelTo\n\n\n\n94\n\n#94 \n(IfcAxis2Placement3D) \n\n\n\n95->94\n\n\nRelativePlacement\n\n\n\n91\n\n#91 \n(IfcCartesianPoint) \n\n\n\n94->91\n\n\nLocation\n\n\n\n92\n\n#92 \n(IfcDirection) \n\n\n\n94->92\n\n\nAxis\n\n\n\n93\n\n#93 \n(IfcDirection) \n\n\n\n94->93\n\n\nRefDirection\n\n\n\n", 407 | "text/plain": [ 408 | "" 409 | ] 410 | }, 411 | "execution_count": 19, 412 | "metadata": {}, 413 | "output_type": "execute_result" 414 | } 415 | ], 416 | "source": [ 417 | "# Looking deeper into ObjectPlacement:\n", 418 | "IfcGraphViz.IfcGraphViz().plot_graph(file, wall.ObjectPlacement)" 419 | ] 420 | }, 421 | { 422 | "cell_type": "markdown", 423 | "metadata": {}, 424 | "source": [ 425 | "The graph above looks messy, but let's look at it step by step:\n", 426 | "1. IfcLocalPlacement-#95 places the **IfcWall** at a position (0,0,0) relative to IfcLocalPlacement-#86 (0,0,0)\n", 427 | "2. IfcLocalPlacement-#86 places the **IfcBuildingStorey** at a position (0,0,0) relative to IfcLocalPlacement-#84 (0,0,0)\n", 428 | "3. IfcLocalPlacement-#84 places the **IfcSite** with IfcAxis2Placement3D-#17 at position (0,0,0)\n", 429 | "\n", 430 | "The net result? Our beautiful arrow object is placed at (0,0,0)! \n", 431 | "\n", 432 | "Now let's say we change the placement of IfcBuildingStorey, such that IfcAxis2Placement3D-#17 points to a position of (0,0,3.3). If nothing else changes, we'd expect our object to also be at a position of (0,0,3.3), since it is placed relative to the IfcBuildingStorey. \n", 433 | "\n" 434 | ] 435 | }, 436 | { 437 | "cell_type": "markdown", 438 | "metadata": {}, 439 | "source": [ 440 | "#### Coordinate systems defined by IfcSite\n", 441 | "\n", 442 | "In addition to ordinary coordinates, the **IfcSite** provides **RefLatitude**, **RefLongitude**, and **RefElevation** attributes. As the prefix “Ref” suggests, this is a latitude and longitutude provided only for reference. It is not sufficient for proper geolocation and if there is a discrepancy between the **IfcMapConversion** and the data provided in **IfcSite**, the **IfcMapConversion** takes priority.\n", 443 | "\n", 444 | "Note that these **RefLatitude** and **RefLongitude** values are recorded in integers that are separated by a full stop to represent degrees, minutes, seconds, and an optional millionths of a second. West and south locations are negative, and east and north locations are positive.\n", 445 | "\n", 446 | "In a real project, a project may contain multiple **IfcSite** objects. Each **IfcSite** has a **Representation**, which may include terrain, for example. For most projects, there is a site boundary, such as a cadastral boundary which denotes the legal plot of land. The **ObjectPlacement** of the **IfcSite** is therefore likely to be a corner of the site boundary which is a point that has been surveyed." 447 | ] 448 | }, 449 | { 450 | "cell_type": "markdown", 451 | "metadata": {}, 452 | "source": [ 453 | "#### Coordinate systems defined by IfcBuilding\n", 454 | "The **IfcBuilding** contains a **Representation** of the building. It also contains an **IfcObjectPlacement**, which is relative to the **IfcSite**. This would place your building on your site model. The rotation of this placement also sets out the project north of the building. If your building has multiple wings, it may also define the individual project norths of each wing.\n", 455 | "\n", 456 | "The **IfcBuilding** additionally contains two attributes:\n", 457 | "- **ElevationOfRefHeight**: as one steps into your building, the finish floor level will be seen as the building’s internal reference height of +0.00. This attribute will record this “+0.00 reference height” in terms of the absolute values of elevation above sea level.\n", 458 | "- **ElevationOfTerrain**: this is the height in absolute values of elevation above sea level of the terrain immediately surrounding the perimeter of the building. If the terrain slopes, it is taken to be the lowest point.\n", 459 | "\n", 460 | "Time to go back to our sample model again:" 461 | ] 462 | }, 463 | { 464 | "cell_type": "code", 465 | "execution_count": 20, 466 | "metadata": {}, 467 | "outputs": [ 468 | { 469 | "data": { 470 | "text/plain": [ 471 | "{'id': 85,\n", 472 | " 'type': 'IfcBuilding',\n", 473 | " 'GlobalId': '3FllR6gD94WuJroKZKsPAi',\n", 474 | " 'OwnerHistory': #20=IfcOwnerHistory(#18,#19,.READWRITE.,.NOTDEFINED.,1589249386,#18,#19,1589249386),\n", 475 | " 'Name': 'My Building',\n", 476 | " 'Description': None,\n", 477 | " 'ObjectType': None,\n", 478 | " 'ObjectPlacement': #84=IfcLocalPlacement(#82,#17),\n", 479 | " 'Representation': None,\n", 480 | " 'LongName': None,\n", 481 | " 'CompositionType': None,\n", 482 | " 'ElevationOfRefHeight': None,\n", 483 | " 'ElevationOfTerrain': None,\n", 484 | " 'BuildingAddress': None}" 485 | ] 486 | }, 487 | "execution_count": 20, 488 | "metadata": {}, 489 | "output_type": "execute_result" 490 | } 491 | ], 492 | "source": [ 493 | "building = file.by_type(\"IfcBuilding\")[0]\n", 494 | "building.get_info()" 495 | ] 496 | }, 497 | { 498 | "cell_type": "markdown", 499 | "metadata": {}, 500 | "source": [ 501 | "Just like the reference point values in **IfcSite**, these are also duplications of data. It is not explicitly mentioned, but I believe that should there be a discrepancy, the derived coordinate from the **IfcMapConversion** takes priority.\n", 502 | "\n", 503 | "The **ElevationOfTerrain**, apart from being a reference value, also provides a datum to measure the **EavesHeight** and the **Height** (total height) of the building which is recorded in the **Qto_BuildingBaseQuantities**." 504 | ] 505 | }, 506 | { 507 | "cell_type": "markdown", 508 | "metadata": {}, 509 | "source": [ 510 | "#### Absolute coordinates\n", 511 | "If your object has an **IfcObjectPlacement**, it usually uses an **IfcLocalPlacement** which has a **PlacementRelTo**, thus inheriting the parent’s placement. If you omit the **PlacementRelTo**, it does not inherit any more parent coordinates, and ends up being an absolute coordinate. An absolute coordinate is defined as only relative to the **WorldCoordinateSystem** of the **IfcProject**.\n", 512 | "\n", 513 | "A common example for this is the **IfcSite** element which is the immediate child of the **IfcProject**. Because its only parent coordinate is the **WorldCoordinateSystem**, it is known as an absolute placement.\n", 514 | "\n", 515 | "You can also omit the **IfcObjectPlacement** altogether, and it will therefore also be treated as an absolute placement which is equal to the **WorldCoordinateSystem** of the **IfcProject**.\n", 516 | "\n", 517 | "Omission of the **IfcObjectPlacement** is a quick and easy way to say that your **IfcBuildingStorey**, **IfcBuilding**, and **IfcSite**, are all at the **WorldCoordinateSystem**. This behaviour has been noted in some software, such as Revit in some circumstances.\n", 518 | "\n", 519 | "Keep in mind that this behaviour is technically possible but it is not endorsed by buildingSMART. For more information, see this ISG implementation agreement CV-2x3-143 agreement on having the containment tree and the relative placement tree identical for spatial elements. It is only mentioned out of completeness." 520 | ] 521 | }, 522 | { 523 | "cell_type": "markdown", 524 | "metadata": {}, 525 | "source": [ 526 | "#### Spatial Composition and coordinates\n", 527 | "For the objects that we’ve described so far, they usually use the Spatial Composition concept to relate to one another. Keep in mind that spatial decomposition and inheritance of coordinates are two separate concepts. Just because something is spatially contained in a parent container does not mean that it inherits its coordinates.\n", 528 | "\n", 529 | "However, that being said, a convention is endorsed by the specification’s documentation of **IfcLocalPlacement** to place objects relative to the same container that it is spatially contained in. I’ve linked the page for you to read the details of the relationships that are endorsed.\n", 530 | "\n" 531 | ] 532 | }, 533 | { 534 | "cell_type": "markdown", 535 | "metadata": {}, 536 | "source": [ 537 | "#### Further Reading\n", 538 | "I'd you'd like a simpler introduction to georeferencing, I'd highly recommend reading the documentation here: https://blenderbim.org/docs/users/georeferencing.html - This particular guide will take you through the steps of georeferencing your BIM Model using the BlenderBIM Add-on. I've also added a copy of BuildingSmarts' \"User-Guide-for-Geo-referencing-in-IFC-v2.0\" to the \"helpful docs\" folder. Between all these sources, you should be well on your way to setting up your models correctly 😀." 539 | ] 540 | }, 541 | { 542 | "cell_type": "markdown", 543 | "metadata": {}, 544 | "source": [ 545 | "That's it for this notebook. If you have an idea for something else in IFC that you'd like explored in this format, let me know!\n", 546 | "\n", 547 | "Your friendly geek,\n", 548 | "\n", 549 | "Vukas Pajic" 550 | ] 551 | } 552 | ], 553 | "metadata": { 554 | "kernelspec": { 555 | "display_name": "Python 3.9.12 ('pyoccenv')", 556 | "language": "python", 557 | "name": "python3" 558 | }, 559 | "language_info": { 560 | "codemirror_mode": { 561 | "name": "ipython", 562 | "version": 3 563 | }, 564 | "file_extension": ".py", 565 | "mimetype": "text/x-python", 566 | "name": "python", 567 | "nbconvert_exporter": "python", 568 | "pygments_lexer": "ipython3", 569 | "version": "3.9.12" 570 | }, 571 | "orig_nbformat": 4, 572 | "vscode": { 573 | "interpreter": { 574 | "hash": "1bb4e4fe4b1d8afa5ea8acd82878d99e3cb7a638011b4109d2b68d62a8134730" 575 | } 576 | } 577 | }, 578 | "nbformat": 4, 579 | "nbformat_minor": 2 580 | } 581 | --------------------------------------------------------------------------------