├── .gitignore ├── CityGML2OBJs.py ├── LICENSE ├── README.md ├── generateMTL.py ├── markup3dmodule.py ├── plotcolorbar.py └── polygon3dmodule.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.gml 3 | *.obj 4 | *.png 5 | *.pdf 6 | *.mtl -------------------------------------------------------------------------------- /CityGML2OBJs.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | # The MIT License (MIT) 5 | 6 | # This code is part of the CityGML2OBJs package 7 | 8 | # Copyright (c) 2014 9 | # Filip Biljecki 10 | # Delft University of Technology 11 | # fbiljecki@gmail.com 12 | 13 | # Permission is hereby granted, free of charge, to any person obtaining a copy 14 | # of this software and associated documentation files (the "Software"), to deal 15 | # in the Software without restriction, including without limitation the rights 16 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 17 | # copies of the Software, and to permit persons to whom the Software is 18 | # furnished to do so, subject to the following conditions: 19 | 20 | # The above copyright notice and this permission notice shall be included in 21 | # all copies or substantial portions of the Software. 22 | 23 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 24 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 25 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 26 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 27 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 28 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 29 | # THE SOFTWARE. 30 | 31 | import markup3dmodule 32 | import polygon3dmodule 33 | from lxml import etree 34 | import os 35 | import argparse 36 | import glob 37 | import numpy as np 38 | import itertools 39 | 40 | #-- ARGUMENTS 41 | # -i -- input directory (it will read and convert ALL CityGML files in a directory) 42 | # -o -- output directory (it will output the generated OBJs in that directory in the way that Delft.gml becomes Delft.obj) 43 | #-- SETTINGS of the converter (can be combined): 44 | # -s 0 (default) -- converts all geometries in one class in one file under the same object (plain OBJ file). 45 | # -s 1 -- differentiate between semantics, output each semantic class as one file, e.g. Delft-WallSurface.obj. Please note that in this case the grouped "plain" OBJ is not generated. 46 | # if no thematic boundaries are found, this option is ignored. 47 | # -g 0 (default) -- keeps all objects in the same bin. 48 | # -g 1 -- it creates one object for every building. 49 | # -v 1 -- validation 50 | # -p 1 -- skip triangulation and write polygons. Polys with interior not supported. 51 | # -t 1 -- translation (reduction) of coordinates so the smallest vertex (one with the minimum coordinates) is at (0, 0) 52 | # -a 1 or 2 or 3 -- this is a very custom setting for adding the texture based on attributes, here you can see the settings for my particular case of the solar radiation. By default it is off. 53 | 54 | #-- Text to be printed at the beginning of each OBJ 55 | header = """# Converted from CityGML to OBJ with CityGML2OBJs. 56 | # Conversion tool developed by Filip Biljecki, TU Delft , see more at Github: 57 | # https://github.com/tudelft3d/CityGML2OBJs 58 | # 59 | 60 | """ 61 | 62 | def get_index(point, list_vertices, shift=0): 63 | """Index the vertices. 64 | The third option is for incorporating a local index (building-level) to the global one (dataset-level).""" 65 | global vertices 66 | """Unique identifier and indexer of vertices.""" 67 | if point in list_vertices: 68 | return list_vertices.index(point) + 1 + shift, list_vertices 69 | else: 70 | list_vertices.append(point) 71 | return list_vertices.index(point) + 1 + shift, list_vertices 72 | 73 | def write_vertices(list_vertices, cla): 74 | """Write the vertices in the OBJ format.""" 75 | global vertices_output 76 | for each in list_vertices: 77 | vertices_output[cla].append("v" + " " + str(each[0]) + " " + str(each[1]) + " " + str(each[2]) + "\n") 78 | 79 | 80 | def remove_reccuring(list_vertices): 81 | """Removes recurring vertices, which messes up the triangulation. 82 | Inspired by http://stackoverflow.com/a/1143432""" 83 | # last_point = list_vertices[-1] 84 | list_vertices_without_last = list_vertices[:-1] 85 | found = set() 86 | for item in list_vertices_without_last: 87 | if str(item) not in found: 88 | yield item 89 | found.add(str(item)) 90 | 91 | 92 | def poly_to_obj(poly, cl, material=None): 93 | """Main conversion function of one polygon to one or more faces in OBJ, 94 | in a specific semantic class. Supports assigning a material.""" 95 | global local_vertices 96 | global vertices 97 | global face_output 98 | #-- Decompose the polygon into exterior and interior 99 | e, i = markup3dmodule.polydecomposer(poly) 100 | #-- Points forming the exterior LinearRing 101 | epoints = markup3dmodule.GMLpoints(e[0]) 102 | #-- Clean recurring points, except the last one 103 | last_ep = epoints[-1] 104 | epoints_clean = list(remove_reccuring(epoints)) 105 | epoints_clean.append(last_ep) 106 | # print epoints 107 | # print epoints_clean 108 | # print 109 | #-- LinearRing(s) forming the interior 110 | irings = [] 111 | for iring in i: 112 | ipoints = markup3dmodule.GMLpoints(iring) 113 | #-- Clean them in the same manner as the exterior ring 114 | last_ip = ipoints[-1] 115 | ipoints_clean = list(remove_reccuring(ipoints)) 116 | ipoints_clean.append(last_ip) 117 | irings.append(ipoints_clean) 118 | #-- If the polygon validation option is enabled 119 | if VALIDATION: 120 | #-- Check the polygon 121 | valid = polygon3dmodule.isPolyValid(epoints_clean, True) 122 | if valid: 123 | for iring in irings: 124 | if not polygon3dmodule.isPolyValid(iring, False): 125 | valid = False 126 | #-- If everything is valid send them to the Delaunay triangulation 127 | if valid: 128 | if SKIPTRI: 129 | #-- Triangulation is skipped, polygons are converted directly to faces 130 | #-- The last point is removed since it's equal to the first one 131 | t = [epoints_clean[:-1]] 132 | else: 133 | #-- Triangulate polys 134 | # t = polygon3dmodule.triangulation(epoints, irings) 135 | try: 136 | t = polygon3dmodule.triangulation(epoints_clean, irings) 137 | except: 138 | t = [] 139 | #-- Process the triangles/polygons 140 | for tri in t: 141 | #-- Face marker 142 | f = "f " 143 | #-- For each point in the triangle/polygon (face) get the index "v" or add it to the index 144 | for ep in range(0, len(tri)): 145 | v, local_vertices[cl] = get_index(tri[ep], local_vertices[cl], len(vertices[cl])) 146 | f += str(v) + " " 147 | #-- Add the material if invoked 148 | if material: 149 | face_output[cl].append("usemtl " + str(mtl(material, min_value, max_value, res)) + str("\n")) 150 | #-- Store all together 151 | face_output[cl].append(f + "\n") 152 | else: 153 | # Get the gml:id of the Polygon if it exists 154 | polyid = poly.xpath("@g:id", namespaces={'g' : ns_gml}) 155 | if polyid: 156 | polyid = polyid[0] 157 | print "\t\t!! Detected an invalid polygon (%s). Skipping..." %polyid 158 | else: 159 | print "\t\t!! Detected an invalid polygon. Skipping..." 160 | 161 | else: 162 | #-- Do exactly the same, but without the validation 163 | try: 164 | if SKIPTRI: 165 | t = [epoints_clean[:-1]] 166 | else: 167 | t = polygon3dmodule.triangulation(epoints_clean, irings) 168 | except: 169 | t = [] 170 | for tri in t: 171 | f = "f " 172 | for ep in range(0, len(tri)): 173 | v, local_vertices[cl] = get_index(tri[ep], local_vertices[cl], len(vertices[cl])) 174 | f += str(v) + " " 175 | if material: 176 | face_output[cl].append("usemtl " + str(mtl(material, min_value, max_value, res)) + str("\n")) 177 | face_output[cl].append(f + "\n") 178 | 179 | #-- Parse command-line arguments 180 | PARSER = argparse.ArgumentParser(description='Convert a CityGML to OBJ.') 181 | PARSER.add_argument('-i', '--directory', 182 | help='Directory containing CityGML file(s).', required=True) 183 | PARSER.add_argument('-o', '--results', 184 | help='Directory where the OBJ file(s) should be written.', required=True) 185 | PARSER.add_argument('-s', '--semantics', 186 | help='Write one OBJ (0) or multiple OBJ per semantic class (1). 0 is default.', required=False) 187 | PARSER.add_argument('-g', '--grouping', 188 | help='Writes all buildings in one group (0) or multiple groups (1). 0 is default.', required=False) 189 | PARSER.add_argument('-a', '--attribute', 190 | help='Creates a texture regarding the value of an attribute of the surface. No material is default.', required=False) 191 | PARSER.add_argument('-v', '--validation', 192 | help='Validates polygons, and if they are not valid give a warning and skip them. No validation is default.', required=False) 193 | PARSER.add_argument('-t', '--translate', 194 | help='Translates all vertices, so that the smallest vertex is at zero. No translation is default.', required=False) 195 | PARSER.add_argument('-p', '--polypreserve', 196 | help='Skip the triangulation (preserve polygons). Triangulation is default.', required=False) 197 | ARGS = vars(PARSER.parse_args()) 198 | DIRECTORY = os.path.join(ARGS['directory'], '') 199 | RESULT = os.path.join(ARGS['results'], '') 200 | 201 | SEMANTICS = ARGS['semantics'] 202 | if SEMANTICS == '1': 203 | SEMANTICS = True 204 | elif SEMANTICS == '0': 205 | SEMANTICS = False 206 | else: 207 | SEMANTICS = False 208 | 209 | OBJECTS = ARGS['grouping'] 210 | if OBJECTS == '1': 211 | OBJECTS = True 212 | elif OBJECTS == '0': 213 | OBJECTS = False 214 | else: 215 | OBJECTS = False 216 | 217 | ATTRIBUTE = ARGS['attribute'] 218 | if ATTRIBUTE == '1': 219 | ATTRIBUTE = 1 220 | elif ATTRIBUTE == '2': 221 | ATTRIBUTE = 2 222 | elif ATTRIBUTE == '3': 223 | ATTRIBUTE = 3 224 | elif ATTRIBUTE == '0': 225 | ATTRIBUTE = False 226 | else: 227 | ATTRIBUTE = False 228 | 229 | VALIDATION = ARGS['validation'] 230 | if VALIDATION == '1': 231 | VALIDATION = True 232 | elif VALIDATION == '0': 233 | VALIDATION = False 234 | else: 235 | VALIDATION = False 236 | 237 | TRANSLATE = ARGS['translate'] 238 | if TRANSLATE == '1': 239 | TRANSLATE = True 240 | elif TRANSLATE == '0': 241 | TRANSLATE = False 242 | else: 243 | TRANSLATE = False 244 | if TRANSLATE: 245 | global smallest_point 246 | 247 | SKIPTRI = ARGS['polypreserve'] 248 | if SKIPTRI == '1': 249 | SKIPTRI = True 250 | elif SKIPTRI == '0': 251 | SKIPTRI = False 252 | else: 253 | SKIPTRI = False 254 | 255 | #----------------------------------------------------------------- 256 | #-- Attribute stuff 257 | 258 | #-- Number of classes (colours) 259 | res = 101 260 | #-- Configuration 261 | #-- Color the surfaces based on the normalised kWh/m^2 value . The plain OBJ will be coloured for the total irradiation. 262 | if ATTRIBUTE == 1: 263 | min_value = 350.0#234.591880403 264 | max_value = 1300.0#1389.97943395 265 | elif ATTRIBUTE == 2: 266 | min_value = 157.0136575 267 | max_value = 83371.4359245 268 | elif ATTRIBUTE == 3: 269 | min_value = 24925.0 270 | max_value = 103454.0 271 | 272 | #-- Statistic parameter 273 | atts = [] 274 | 275 | #-- Colouring function 276 | def mtl(att, min_value, max_value, res): 277 | """Finds the corresponding material.""" 278 | ar = np.linspace(0, 1, res).tolist() 279 | #-- Get rid of floating point errors 280 | for i in range(0, len(ar)): 281 | ar[i] = round(ar[i], 4) 282 | #-- Normalise the attribute 283 | v = float(att-min_value) / (max_value-min_value) 284 | #-- Get the material 285 | assigned_material = min(ar, key=lambda x:abs(x-v)) 286 | return str(assigned_material) 287 | #----------------------------------------------------------------- 288 | 289 | #-- Start of the program 290 | print "CityGML2OBJ. Searching for CityGML files..." 291 | 292 | #-- Find all CityGML files in the directory 293 | os.chdir(DIRECTORY) 294 | #-- Supported extensions 295 | types = ('*.gml', '*.GML', '*.xml', '*.XML') 296 | files_found = [] 297 | for files in types: 298 | files_found.extend(glob.glob(files)) 299 | for f in files_found: 300 | FILENAME = f[:f.rfind('.')] 301 | FULLPATH = os.path.join(DIRECTORY, f) 302 | 303 | #-- Reading and parsing the CityGML file(s) 304 | CITYGML = etree.parse(FULLPATH) 305 | #-- Getting the root of the XML tree 306 | root = CITYGML.getroot() 307 | #-- Determine CityGML version 308 | # If 1.0 309 | if root.tag == "{http://www.opengis.net/citygml/1.0}CityModel": 310 | #-- Name spaces 311 | ns_citygml="http://www.opengis.net/citygml/1.0" 312 | 313 | ns_gml = "http://www.opengis.net/gml" 314 | ns_bldg = "http://www.opengis.net/citygml/building/1.0" 315 | ns_tran = "http://www.opengis.net/citygml/transportation/1.0" 316 | ns_veg = "http://www.opengis.net/citygml/vegetation/1.0" 317 | ns_gen = "http://www.opengis.net/citygml/generics/1.0" 318 | ns_xsi="http://www.w3.org/2001/XMLSchema-instance" 319 | ns_xAL="urn:oasis:names:tc:ciq:xsdschema:xAL:1.0" 320 | ns_xlink="http://www.w3.org/1999/xlink" 321 | ns_dem="http://www.opengis.net/citygml/relief/1.0" 322 | ns_frn="http://www.opengis.net/citygml/cityfurniture/1.0" 323 | ns_tun="http://www.opengis.net/citygml/tunnel/1.0" 324 | ns_wtr="http://www.opengis.net/citygml/waterbody/1.0" 325 | ns_brid="http://www.opengis.net/citygml/bridge/1.0" 326 | ns_app="http://www.opengis.net/citygml/appearance/1.0" 327 | #-- Else probably means 2.0 328 | else: 329 | #-- Name spaces 330 | ns_citygml="http://www.opengis.net/citygml/2.0" 331 | 332 | ns_gml = "http://www.opengis.net/gml" 333 | ns_bldg = "http://www.opengis.net/citygml/building/2.0" 334 | ns_tran = "http://www.opengis.net/citygml/transportation/2.0" 335 | ns_veg = "http://www.opengis.net/citygml/vegetation/2.0" 336 | ns_gen = "http://www.opengis.net/citygml/generics/2.0" 337 | ns_xsi="http://www.w3.org/2001/XMLSchema-instance" 338 | ns_xAL="urn:oasis:names:tc:ciq:xsdschema:xAL:2.0" 339 | ns_xlink="http://www.w3.org/1999/xlink" 340 | ns_dem="http://www.opengis.net/citygml/relief/2.0" 341 | ns_frn="http://www.opengis.net/citygml/cityfurniture/2.0" 342 | ns_tun="http://www.opengis.net/citygml/tunnel/2.0" 343 | ns_wtr="http://www.opengis.net/citygml/waterbody/2.0" 344 | ns_brid="http://www.opengis.net/citygml/bridge/2.0" 345 | ns_app="http://www.opengis.net/citygml/appearance/2.0" 346 | 347 | nsmap = { 348 | None : ns_citygml, 349 | 'gml': ns_gml, 350 | 'bldg': ns_bldg, 351 | 'tran': ns_tran, 352 | 'veg': ns_veg, 353 | 'gen' : ns_gen, 354 | 'xsi' : ns_xsi, 355 | 'xAL' : ns_xAL, 356 | 'xlink' : ns_xlink, 357 | 'dem' : ns_dem, 358 | 'frn' : ns_frn, 359 | 'tun' : ns_tun, 360 | 'brid': ns_brid, 361 | 'app' : ns_app 362 | } 363 | #-- Empty lists for cityobjects and buildings 364 | cityObjects = [] 365 | buildings = [] 366 | other = [] 367 | 368 | #-- This denotes the dictionaries in which the surfaces are put. 369 | output = {} 370 | vertices_output = {} 371 | face_output = {} 372 | 373 | #-- This denotes the dictionaries in which all surfaces are put. It is later ignored in the semantic option was invoked. 374 | output['All'] = [] 375 | output['All'].append(header) 376 | if ATTRIBUTE: 377 | output['All'].append("mtllib colormap.mtl\n") 378 | vertices_output['All'] = [] 379 | face_output['All'] = [] 380 | 381 | #-- If the semantic option was invoked, this part adds additional dictionaries. 382 | if SEMANTICS: 383 | #-- Easy to modify list of thematic boundaries 384 | semanticSurfaces = ['GroundSurface', 'WallSurface', 'RoofSurface', 'ClosureSurface', 'CeilingSurface', 'InteriorWallSurface', 'FloorSurface', 'OuterCeilingSurface', 'OuterFloorSurface', 'Door', 'Window'] 385 | for semanticSurface in semanticSurfaces: 386 | output[semanticSurface] = [] 387 | output[semanticSurface].append(header) 388 | #-- Add the material library 389 | if ATTRIBUTE: 390 | output[semanticSurface].append("mtllib colormap.mtl\n") 391 | vertices_output[semanticSurface] = [] 392 | face_output[semanticSurface] = [] 393 | 394 | 395 | #-- Directory of vertices (indexing) 396 | vertices = {} 397 | vertices['All'] = [] 398 | if SEMANTICS: 399 | for semanticSurface in semanticSurfaces: 400 | vertices[semanticSurface] = [] 401 | vertices['Other'] = [] 402 | face_output['Other'] = [] 403 | output['Other'] = [] 404 | 405 | #-- Find all instances of cityObjectMember and put them in a list 406 | for obj in root.getiterator('{%s}cityObjectMember'% ns_citygml): 407 | cityObjects.append(obj) 408 | 409 | print FILENAME 410 | 411 | if len(cityObjects) > 0: 412 | 413 | #-- Report the progress and contents of the CityGML file 414 | print "\tThere are", len(cityObjects), "cityObject(s) in this CityGML file." 415 | #-- Store each building separately 416 | for cityObject in cityObjects: 417 | for child in cityObject.getchildren(): 418 | if child.tag == '{%s}Building' %ns_bldg: 419 | buildings.append(child) 420 | for cityObject in cityObjects: 421 | for child in cityObject.getchildren(): 422 | if child.tag == '{%s}Road' %ns_tran or child.tag == '{%s}PlantCover' %ns_veg or \ 423 | child.tag == '{%s}GenericCityObject' %ns_gen or child.tag == '{%s}CityFurniture' %ns_frn or \ 424 | child.tag == '{%s}Relief' %ns_dem or child.tag == '{%s}Tunnel' %ns_tun or \ 425 | child.tag == '{%s}WaterBody' %ns_wtr or child.tag == '{%s}Bridge' %ns_brid: 426 | other.append(child) 427 | 428 | print "\tAnalysing objects and extracting the geometry..." 429 | 430 | #-- Count the buildings 431 | b_counter = 0 432 | b_total = len(buildings) 433 | 434 | #-- Do each building separately 435 | for b in buildings: 436 | 437 | #-- Build the local list of vertices to speed up the indexing 438 | local_vertices = {} 439 | local_vertices['All'] = [] 440 | if SEMANTICS: 441 | for semanticSurface in semanticSurfaces: 442 | local_vertices[semanticSurface] = [] 443 | 444 | 445 | #-- Increment the building counter 446 | b_counter += 1 447 | 448 | #-- If the object option is on, get the name for each building or create one 449 | if OBJECTS: 450 | ob = b.xpath("@g:id", namespaces={'g' : ns_gml}) 451 | if not ob: 452 | ob = b_counter 453 | else: 454 | ob = ob[0] 455 | 456 | #-- Print progress for large files every 1000 buildings. 457 | if b_counter == 1000: 458 | print "\t1000... ", 459 | elif b_counter % 1000 == 0 and b_counter == (b_total - b_total % 1000): 460 | print str(b_counter) + "..." 461 | elif b_counter > 0 and b_counter % 1000 == 0: 462 | print str(b_counter) + "... ", 463 | 464 | #-- Add the object identifier 465 | if OBJECTS: 466 | face_output['All'].append('o ' + str(ob) + '\n') 467 | 468 | #-- Add the attribute for the building 469 | if ATTRIBUTE: 470 | for ch in b.getchildren(): 471 | if ch.tag == "{%s}yearlyIrradiation" %ns_citygml: 472 | bAttVal = float(ch.text) 473 | 474 | #-- OBJ with all surfaces in the same bin 475 | polys = markup3dmodule.polygonFinder(b) 476 | #-- Process each surface 477 | for poly in polys: 478 | if ATTRIBUTE: 479 | poly_to_obj(poly, 'All', bAttVal) 480 | if ATTRIBUTE == 3: 481 | atts.append(bAttVal) 482 | else: 483 | #print etree.tostring(poly) 484 | poly_to_obj(poly, 'All') 485 | 486 | #-- Semantic decomposition, with taking special care about the openings 487 | if SEMANTICS: 488 | #-- First take care about the openings since they can mix up 489 | openings = [] 490 | openingpolygons = [] 491 | for child in b.getiterator(): 492 | if child.tag == '{%s}opening' %ns_bldg: 493 | openings.append(child) 494 | for o in child.findall('.//{%s}Polygon' %ns_gml): 495 | openingpolygons.append(o) 496 | 497 | #-- Process each opening 498 | for o in openings: 499 | for child in o.getiterator(): 500 | if child.tag == '{%s}Window' %ns_bldg or child.tag == '{%s}Door' %ns_bldg: 501 | if child.tag == '{%s}Window' %ns_bldg: 502 | t = 'Window' 503 | else: 504 | t = 'Door' 505 | polys = markup3dmodule.polygonFinder(o) 506 | for poly in polys: 507 | poly_to_obj(poly, t) 508 | 509 | #-- Process other thematic boundaries 510 | for cl in output: 511 | cls = [] 512 | for child in b.getiterator(): 513 | if child.tag == '{%s}%s' % (ns_bldg, cl): 514 | cls.append(child) 515 | #-- Is this the first feature of this object? 516 | firstF = True 517 | for feature in cls: 518 | #-- If it is the first feature, print the object identifier 519 | if OBJECTS and firstF: 520 | face_output[cl].append('o ' + str(ob) + '\n') 521 | firstF = False 522 | #-- This is not supposed to happen, but just to be sure... 523 | if feature.tag == '{%s}Window' %ns_bldg or feature.tag == '{%s}Door' %ns_bldg: 524 | continue 525 | #-- Find all polygons in this semantic boundary hierarchy 526 | for p in feature.findall('.//{%s}Polygon' %ns_gml): 527 | if ATTRIBUTE == 1 or ATTRIBUTE == 2: 528 | #-- Flush the previous value 529 | attVal = None 530 | if cl == 'RoofSurface': 531 | #print p.xpath("//@c:irradiation", namespaces={'c' : ns_citygml}) 532 | #-- Silly way but it works, as I can't get the above xpath to work for some reason 533 | for ch in p.getchildren(): 534 | if ATTRIBUTE == 1: 535 | if ch.tag == "{%s}irradiation" %ns_citygml: 536 | attVal = float(ch.text) 537 | atts.append(attVal) 538 | elif ATTRIBUTE == 2: 539 | if ch.tag == "{%s}totalIrradiation" %ns_citygml: 540 | attVal = float(ch.text) 541 | atts.append(attVal) 542 | elif ATTRIBUTE == 3: 543 | attVal = None 544 | if cl == 'RoofSurface': 545 | attVal = bAttVal 546 | else: 547 | #-- If the attribute option is off, pass no material 548 | attVal = None 549 | found_opening = False 550 | for optest in openingpolygons: 551 | if p == optest: 552 | found_opening = True 553 | break 554 | #-- If there is an opening skip it 555 | if found_opening: 556 | pass 557 | else: 558 | #-- Finally process the polygon 559 | poly_to_obj(p, cl, attVal) 560 | 561 | #-- Merge the local list of vertices to the global 562 | for cl in local_vertices: 563 | for vertex in local_vertices[cl]: 564 | vertices[cl].append(vertex) 565 | 566 | if len(other) > 0: 567 | vertices_output['Other'] = [] 568 | local_vertices = {} 569 | local_vertices['Other'] = [] 570 | for oth in other: 571 | # local_vertices = {} 572 | # local_vertices['All'] = [] 573 | polys = markup3dmodule.polygonFinder(oth) 574 | #-- Process each surface 575 | for poly in polys: 576 | poly_to_obj(poly, 'Other') 577 | for vertex in local_vertices['Other']: 578 | vertices['Other'].append(vertex) 579 | 580 | 581 | print "\tExtraction done. Sorting geometry and writing file(s)." 582 | 583 | #-- Translate (convert) the vertices to a local coordinate system 584 | if TRANSLATE: 585 | print "\tTranslating the coordinates of vertices." 586 | list_of_all_vertices = [] 587 | for cl in output: 588 | if len(vertices[cl]) > 0: 589 | for vtx in vertices[cl]: 590 | list_of_all_vertices.append(vtx) 591 | smallest_vtx = polygon3dmodule.smallestPoint(list_of_all_vertices) 592 | dx = smallest_vtx[0] 593 | dy = smallest_vtx[1] 594 | dz = smallest_vtx[2] 595 | for cl in output: 596 | if len(vertices[cl]) > 0: 597 | for idx, vtx in enumerate(vertices[cl]): 598 | vertices[cl][idx][0] = vtx[0] - dx 599 | vertices[cl][idx][1] = vtx[1] - dy 600 | vertices[cl][idx][2] = vtx[2] - dz 601 | 602 | 603 | #-- Write the OBJ(s) 604 | os.chdir(RESULT) 605 | #-- Theme by theme 606 | for cl in output: 607 | if len(vertices[cl]) > 0: 608 | write_vertices(vertices[cl], cl) 609 | output[cl].append("\n" + ''.join(vertices_output[cl])) 610 | output[cl].append("\n" + ''.join(face_output[cl])) 611 | if cl == 'All': 612 | adj_suffix = "" 613 | else: 614 | adj_suffix = "-" + str(cl) 615 | 616 | with open(RESULT + FILENAME + str(adj_suffix) + ".obj", "w") as obj_file: 617 | obj_file.write(''.join(output[cl])) 618 | 619 | print "\tOBJ file(s) written." 620 | 621 | #-- Print the range of attributes. Useful for defining the range of the colorbar. 622 | if ATTRIBUTE: 623 | print '\tRange of attributes:', min(atts), '--', max(atts) 624 | 625 | else: 626 | print "\tThere is a problem with this file: no cityObjects have been found. Please check if the file complies to CityGML." -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Filip Biljecki 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | ## IMPORTANT!!! 3 | 4 | This project will not be further developed and bugs won't be fixed. 5 | 6 | Instead, we advice you to use [CityJSON](https://cityjson.org) (and convert your files with [citygml-tools](https://github.com/citygml4j/citygml-tools)) and then use [cjio](https://github.com/tudelft3d/cjio) `export` operator to get an OBJ file. 7 | 8 | - - - 9 | 10 | 11 | 12 | CityGML2OBJs 13 | =========== 14 | 15 | ![CityGML2OBJs-header-image](http://filipbiljecki.com/code/img/whitesky-small.png) 16 | 17 | A robust semantic-aware utility to convert CityGML data to OBJ, featuring some additional options reflected through the suffix "s" in the name of the package: 18 | 19 | - semantics -- decoupling of thematically structured surfaces in CityGML and converting them into separate OBJs (that's where the "OBJs" in the name come from). 20 | - structured objects -- separation and storage of buildings into multiple objects in OBJ by structuring faces that belong to a building into the same group. 21 | - "see" the attributes from a CityGML file -- the utility converts quantitative attributes into colours to support their visualisation. 22 | - sturdy -- checks polygons for validity, considers different forms of geometry storage in GML, detects for lack of boundary surfaces, etc. 23 | - solution to finally make use of those CityGML files (sarcasm is also an S word :-)). OBJ is probably the most supported 3D format, and converting your CityGML files to OBJ opens a door to a large number of software packages and uses. 24 | 25 | 26 | 27 | Things to know 28 | --------------------- 29 | 30 | This is an experimental research software prototype. Therefore, support is limited, and the software is not without bugs. For instance, there are reports of crashes with large data sets and/or with invalid geometries. 31 | 32 | If you'd like to learn more about OBJ, I recommend to read the [Wikipedia entry](https://en.wikipedia.org/wiki/Wavefront_.obj_file) and/or the [specifications](http://www.martinreddy.net/gfx/3d/OBJ.spec). 33 | 34 | 35 | Publication and conditions for use 36 | --------------------- 37 | 38 | This software is free to use. You are kindly asked to acknowledge its use by citing it in a research paper you are writing, reports, and/or other applicable materials. If you used it for making a nice publication, please cite the following paper: 39 | 40 | Biljecki, F., & Arroyo Ohori, K. (2015). Automatic semantic-preserving conversion between OBJ and CityGML. Eurographics Workshop on Urban Data Modelling and Visualisation 2015, pp. 25-30. 41 | 42 | [[PDF]](http://filipbiljecki.com/publications/Biljecki2015vk.pdf) [[DOI]](http://doi.org/10.2312/udmv.20151345) 43 | 44 | 45 | ```bib 46 | @inproceedings{Biljecki:2015vk, 47 | author = {Biljecki, Filip and Arroyo Ohori, Ken}, 48 | title = {{Automatic Semantic-preserving Conversion Between OBJ and CityGML}}, 49 | booktitle = {Eurographics Workshop on Urban Data Modelling and Visualisation 2015}, 50 | year = {2015}, 51 | pages = {25--30}, 52 | address = {Delft, Netherlands}, 53 | doi = {10.2312/udmv.20151345}, 54 | month = nov 55 | } 56 | ``` 57 | 58 | 59 | 60 | Features explained in more details 61 | --------------------- 62 | 63 | + It re-uses repeating vertices, resulting in a reduced file size and redundancy, for which CityGML is not particularly known for. 64 | + Validates polygon and skips them if they are not valid. Further, the tool supports multiple GML conventions for geometry (e.g. `` vs ``). 65 | + The program is capable of batch processing of multiple files saving you time. 66 | + Supports polygon holes by triangulating all surfaces. Besides the holes, this is done by default because some software handles OBJs only if the faces are triangulated, especially when it comes to the texture, so not only holey polygons are triangulated. OBJ does not support polygons with holes, which are common in CityGML files (``), especially in LOD3 models due to doors, windows and holes left by building installations. For the Delaunay triangulation the tool uses Jonathan Richard Shewchuk's library, through its Python bindings Triangle. 67 | + It can store the semantic properties, and separate files for each of the thematic class, e.g. from the file `Delft.gml` it creates files `Delft-WallSurface.obj`, `Delft-RoofSurface.obj`, ... 68 | + OBJ does not really support the concept of attributes, hence if the CityGML file contains an attribute, this is generally lost in the conversion. However, this converter is capable of converting a quantitative attribute to OBJ as a texture (colour) of the feature. For instance, if the attribute about the yearly solar irradiation is available for each polygon in the CityGML file, it is converted to a graphical information and attached to each polygon as a surface, so now you can easily visualise your attributes in CityGML. Please note that this is a very custom setting, and you will need to adapt the code to match your needs. 69 | + Converts the coordinates to a local system. 70 | + Supports both CityGML 1.0 and CityGML 2.0. 71 | 72 | 73 | System requirements 74 | --------------------- 75 | 76 | Python packages: 77 | 78 | + [Numpy](http://docs.scipy.org/doc/numpy/user/install.html) (likely already on your system) 79 | + [Triangle](http://dzhelil.info/triangle/). If not on your system: `easy_install triangle` 80 | + [lxml](http://lxml.de) 81 | + [Shapely](https://github.com/Toblerity/Shapely) 82 | 83 | 84 | Optional: 85 | 86 | + [Matplotlib](http://matplotlib.org/users/installing.html) 87 | 88 | ### OS and Python version 89 | 90 | The software has been developed on Mac OSX in Python 2.7, and has not been tested with other configurations. Hence, it is possible that some of the functions will not work on Windows and on Python 3. 91 | 92 | CityGML requirements 93 | --------------------- 94 | 95 | Mandatory: 96 | 97 | + CityGML 1.0 or 2.0 98 | + Files must end with `.gml`, `.GML`, `.xml`, or `.XML` 99 | + Vertices in either `` or `` 100 | + Your files must be valid (see the next section) 101 | 102 | Optional, but recommended: 103 | 104 | + `` for each `` and other types of city objects 105 | + `` for each `` 106 | 107 | 108 | About the validity of CityGML files 109 | --------------------- 110 | 111 | There have been reports that the code crashes for some CityGML files. Unfortunately, in some cases the triangulation code halts with a peculiar error as it encounters a weird geometry, that cannot be skipped (excepted). If your files won't convert, there's a chance that you have invalid CityGML files (albeit a CityGML2OBJs bug is not excluded). Please ensure that your files are valid. Alternatively, you can invoke the option `-v 1` to skip such geometries--sometimes it works. 112 | 113 | [Hugo Ledoux](https://3d.bk.tudelft.nl/hledoux/) built [val3dity](http://geovalidation.bk.tudelft.nl/val3dity/), a thorough GML validator which is available for free through a web interface. Use this tool to test your files. 114 | 115 | 116 | Usage and options 117 | --------------------- 118 | 119 | ### Introduction 120 | 121 | To simply convert CityGML data into OBJ type the following command: 122 | 123 | ``` 124 | python CityGML2OBJs.py -i /path/to/CityGML/files/ -o /path/to/new/OBJ/files/ 125 | ``` 126 | 127 | The tool will convert all CityGML files it finds in that folder. The command works also with relative paths. 128 | 129 | ### Semantics 130 | 131 | If you call the `-s 1` option: 132 | 133 | ``` 134 | python CityGML2OBJs.py -i /path/to/CityGML/files/ -o /path/to/new/OBJ/files/ -s 1 135 | ``` 136 | 137 | the tool will create an OBJ file for each of the boundary surfaces it encounters, e.g. `Delft.gml` containing `RoofSurface`, `WallSurface` and `GroundSurface` will result in: `Delft-RoofSurface.obj`, and so on. 138 | 139 | Here is an example of the OBJ file representing the `WallSurface`: 140 | 141 | ![Triangulated WallSurface](http://filipbiljecki.com/code/img/sem-tri-small.png) 142 | 143 | Regardless of the semantic option, the program always outputs the plain OBJ. This is a useful approach if you load data which does not have boundary surfaces (e.g. only a bunch of solids) so you'll always get something back. The tool detects if there are no thematic boundaries, so doesn't write empty OBJ files, for instance, an empty `*-Window.obj` for an LOD2 model. 144 | 145 | ### Validate geometries 146 | 147 | Call the `-v 1` option: 148 | 149 | ``` 150 | python CityGML2OBJs.py -i /path/to/CityGML/files/ -o /path/to/new/OBJ/files/ -v 1 151 | ``` 152 | in order to validate the geometries and skip invalid ones. The tool will report invalid geometries with their ``. 153 | 154 | ### Objects 155 | 156 | CityGML is a structured format. If you call the flag `-g 1` you'll preserve the objects in the OBJ file 157 | 158 | ``` 159 | python CityGML2OBJs.py -i /path/to/CityGML/files/ -o /path/to/new/OBJ/files/ -g 1 160 | ``` 161 | 162 | For the object option the name of the object will be derived from the ``, if not, an ordered list will be made starting from 1, and each object will be named as an integer from 1 to *n*, where *n* is the number of objects. 163 | 164 | So this 165 | 166 | ``` 167 | 168 | 169 | 170 | 171 | ... 172 | ``` 173 | 174 | becomes 175 | 176 | ``` 177 | o ab76da5b-82d6-44ad-a670-c1f8b4f00edc 178 | f 635 636 637 179 | f 636 635 638 180 | f 639 640 641 181 | ... 182 | ``` 183 | 184 | ### Conversion of coordinates 185 | 186 | Normally CityGML data sets are geo-referenced. This may be a problem for some software packages. Invoke `-t 1` to convert the data set to a local system. The origin of the local system correspond to the point with the smallest coordinates (usually the one closest to south-west). 187 | 188 | ### Skip the triangulation 189 | 190 | OBJ supports polygons, but most software packages prefer triangles. Hence the polygons are triangulated by default (another reason is that OBJ doesn't support polys with holes). However, this may cause problems in some instances, or you might prefer to preserve polygons. If so, put `-p 1` to skip the triangulation. Sometimes it also helps to bypass invalid geometries in CityGML data sets. 191 | 192 | 193 | Known limitations 194 | --------------------- 195 | 196 | * Some polygon normals sometimes get inverted. Usually a (wrong) normal is preserved from the data set, but in rare instances a bug may cause a correct normal to be inverted (and the other way around--in that case it's a feature!). 197 | * Non-building thematic classes are not supported in the semantic sense (they will be converted together as `Other` class). However, all geometry will be converted to the plain OBJ regardless of the theme, when the corresponding option is invoked). 198 | * The texture from the CityGML is not converted to OBJ (future work). 199 | * The tool supports only single-LOD files. If you load a multi-LOD file, you'll get their union. 200 | * If the converter crashes, it's probably because your CityGML files contain invalid geometries. Run the code with the `-v 1` flag to validate and skip the invalid geometries. If that doesn't work, try to invoke the option `-p 1`. If that fails too, please report the error. 201 | * `XLink` is not supported, nor will be because for most files it will result in duplicate geometry. 202 | * The tool does not support non-convex polygons in the interior, for which might happen that the centroid of a hole is outside the hole, messing up the triangulation. This is on my todo list, albeit I haven't encountered many such cases. 203 | * CityGML can be a nasty format because there may be multiple ways to store the geometry. For instance, points can be stored under `` and ``. Check this interesting [blog post by Even Rouault](http://erouault.blogspot.nl/2014/04/gml-madness.html). I have tried to regard all cases, so it should work for your files, but if your file cannot be parsed, let me know. 204 | * Skipping triangulation does not work with polygons with holes. 205 | 206 | 207 | ### Colour attributes 208 | 209 | **Please note that this is an experimental feature adapted to specific settings.** 210 | 211 | I have CityGML files with the attribute value of the solar potential (derived with Solar3Dcity, an experimental tool that I have recently developed and release soon) for each polygon. Normally these values cannot be visualised and are lost in the conversion to other formats. This tool solves this problem by normalising the quantitative attributes and colour them according to a colorbar, which is stored as a material (MTL) file. 212 | 213 | Ancillary tools have been developed to make this feature use-friendly and visually appealing. Open each of these ancillary python files and fiddle with the values. 214 | 215 | Generate the MTL library: 216 | 217 | ``` 218 | python generateMTL.py 219 | ``` 220 | 221 | Print a colorbar with the transparent background: 222 | 223 | ``` 224 | python plotcolorbar.py 225 | ``` 226 | 227 | Put the newly generated MTL in the directory with the CityGML data and run the utility with the option `-s 1` or `-s 2` or `-s 3`: 228 | 229 | ``` 230 | python CityGML2OBJs.py -i /path/to/CityGML/files/ -o /path/to/new/OBJ/files/ -s 1 231 | ``` 232 | 233 | Now the values of the solar potential of roof surfaces in the CityGML file are stored as textures (colours), and such can be easily visualised: 234 | 235 | ![solar3dcity-header](http://filipbiljecki.com/code/img/ov-solar-n-legend-logo-small.png) 236 | 237 | 238 | The different options are for transfering the values of attributes between different hierarchical levels. For instance, the option 3 takes the attribute assigned to the building, and colours only the triangles representing the RoofSurface, instead of all faces representing that building. If you want to discuss this in further details to accommodate your needs, do not hesitate to contact me. 239 | 240 | ![Attributes](http://filipbiljecki.com/code/img/att-uml.png) 241 | 242 | 243 | Performance 244 | --------------------- 245 | 246 | The speed mainly depends on the invoked options and the level of detail of the data which substantially increases the number of triangles in the OBJ, mostly due to the openings. 247 | 248 | For benchmarking, I have tested the tool with a CityGML data set of 100 buildings, and the performance is as follows: 249 | 250 | * LOD2 (average 13 triangles per building) 251 | * plain options: 0.004 seconds per building 252 | * plain + semantics: 0.013 seconds per building 253 | * LOD3 (average 331 triangles per building) 254 | * plain options: 0.048 seconds per building 255 | * plain + semantics: 0.0933 seconds per building 256 | 257 | LOD0 and LOD1 have roughly the same performance as LOD2. Validation of polygons does not notably decrease the speed. 258 | 259 | 260 | Contact for questions and feedback 261 | --------------------- 262 | Filip Biljecki 263 | 264 | [3D Geoinformation Research Group](https://3d.bk.tudelft.nl/) 265 | 266 | Faculty of Architecture and the Built Environment 267 | 268 | Delft University of Technology 269 | 270 | email: fbiljecki at gmail dot com 271 | 272 | [Personal webpage](https://3d.bk.tudelft.nl/biljecki/) 273 | 274 | 275 | 276 | Acknowledgments 277 | --------------------- 278 | + This research is supported by the Dutch Technology Foundation STW, which is part of the Netherlands Organisation for Scientific Research (NWO), and which is partly funded by the Ministry of Economic Affairs (project code: 11300). 279 | 280 | + [Triangle](http://www.cs.cmu.edu/~quake/triangle.html), a 2D quality mesh generator and delaunay triangulator developed by [Jonathan Shewchuk](http://www.cs.berkeley.edu/~jrs/), and the [Python bindings](http://dzhelil.info/triangle/). CityGML2OBJs relies on this tool for triangulating the polygons, and its availability is highly appreaciated. 281 | 282 | + People who detected bugs and gave suggestions for improvements. 283 | 284 | My colleagues: 285 | 286 | + [Ken Arroyo Ohori](http://tudelft.nl/kenohori) 287 | 288 | + [Ravi Peters](https://3d.bk.tudelft.nl/rypeters) who developed a similar software [citygml2obj](https://code.google.com/p/citygml2obj/) in 2009, and gave me the permission to use the name of his software. 289 | 290 | + [Hugo Ledoux](http://homepage.tudelft.nl/23t4p/) 291 | 292 | + [Martijn Meijers](http://www.gdmc.nl/martijn/) 293 | -------------------------------------------------------------------------------- /generateMTL.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | # The MIT License (MIT) 5 | 6 | # This code is part of the CityGML2OBJs package 7 | 8 | # Copyright (c) 2014 9 | # Filip Biljecki 10 | # Delft University of Technology 11 | # fbiljecki@gmail.com 12 | 13 | # Permission is hereby granted, free of charge, to any person obtaining a copy 14 | # of this software and associated documentation files (the "Software"), to deal 15 | # in the Software without restriction, including without limitation the rights 16 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 17 | # copies of the Software, and to permit persons to whom the Software is 18 | # furnished to do so, subject to the following conditions: 19 | 20 | # The above copyright notice and this permission notice shall be included in 21 | # all copies or substantial portions of the Software. 22 | 23 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 24 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 25 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 26 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 27 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 28 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 29 | # THE SOFTWARE. 30 | 31 | import numpy as np 32 | import matplotlib.cm as cm 33 | 34 | #-- Number of classes of the colormap 35 | no_values = 101 36 | 37 | #-- Select the colormap and get its RGB values for each of the classes 38 | colormap = cm.get_cmap("afmhot", no_values) # http://matplotlib.org/examples/color/colormaps_reference.html 39 | colormap_vals = colormap(np.arange(no_values)).tolist() 40 | 41 | #-- This is the MTL file 42 | mtlcontents = "" 43 | 44 | #-- Class by class... MTL! 45 | for i in range(0, no_values): 46 | b = float(i)/100 47 | mtlcontents += "newmtl " + str(b) + "\n" 48 | mtlcontents += "Ka " + str(colormap_vals[i][0]) + " " + str(colormap_vals[i][1]) + " " + str(colormap_vals[i][2]) + "\n" 49 | mtlcontents += "Kd " + str(colormap_vals[i][0]) + " " + str(colormap_vals[i][1]) + " " + str(colormap_vals[i][2]) + "\n" 50 | #-- Write the MTL 51 | with open("colormap.mtl", "w") as mtl_file: 52 | mtl_file.write(mtlcontents) -------------------------------------------------------------------------------- /markup3dmodule.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | # The MIT License (MIT) 5 | 6 | # This code is part of the CityGML2OBJs package 7 | 8 | # Copyright (c) 2014 9 | # Filip Biljecki 10 | # Delft University of Technology 11 | # fbiljecki@gmail.com 12 | 13 | # Permission is hereby granted, free of charge, to any person obtaining a copy 14 | # of this software and associated documentation files (the "Software"), to deal 15 | # in the Software without restriction, including without limitation the rights 16 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 17 | # copies of the Software, and to permit persons to whom the Software is 18 | # furnished to do so, subject to the following conditions: 19 | 20 | # The above copyright notice and this permission notice shall be included in 21 | # all copies or substantial portions of the Software. 22 | 23 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 24 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 25 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 26 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 27 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 28 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 29 | # THE SOFTWARE. 30 | 31 | from lxml import etree 32 | 33 | #-- Name spaces 34 | ns_citygml="http://www.opengis.net/citygml/2.0" 35 | 36 | ns_gml = "http://www.opengis.net/gml" 37 | ns_bldg = "http://www.opengis.net/citygml/building/2.0" 38 | ns_xsi="http://www.w3.org/2001/XMLSchema-instance" 39 | ns_xAL="urn:oasis:names:tc:ciq:xsdschema:xAL:2.0" 40 | ns_xlink="http://www.w3.org/1999/xlink" 41 | ns_dem="http://www.opengis.net/citygml/relief/2.0" 42 | 43 | nsmap = { 44 | None : ns_citygml, 45 | 'gml': ns_gml, 46 | 'bldg': ns_bldg, 47 | 'xsi' : ns_xsi, 48 | 'xAL' : ns_xAL, 49 | 'xlink' : ns_xlink, 50 | 'dem' : ns_dem 51 | } 52 | 53 | def polydecomposer(polygon): 54 | """Extracts the and of a .""" 55 | exter = polygon.findall('.//{%s}exterior' %ns_gml) 56 | inter = polygon.findall('.//{%s}interior' %ns_gml) 57 | return exter, inter 58 | 59 | 60 | def polygonFinder(GMLelement): 61 | """Find the element.""" 62 | polygonsLocal = GMLelement.findall('.//{%s}Polygon' %ns_gml) 63 | return polygonsLocal 64 | 65 | 66 | def GMLpoints(ring): 67 | "Extract points from a ." 68 | #-- List containing points 69 | listPoints = [] 70 | #-- Read the value and convert to string 71 | if len(ring.findall('.//{%s}posList' %ns_gml)) > 0: 72 | points = ring.findall('.//{%s}posList' %ns_gml)[0].text 73 | #-- List of coordinates 74 | coords = points.split() 75 | assert(len(coords) % 3 == 0) 76 | #-- Store the coordinate tuple 77 | for i in range(0, len(coords), 3): 78 | listPoints.append([float(coords[i]), float(coords[i+1]), float(coords[i+2])]) 79 | elif len(ring.findall('.//{%s}pos' %ns_gml)) > 0: 80 | points = ring.findall('.//{%s}pos' %ns_gml) 81 | #-- Extract each point separately 82 | for p in points: 83 | coords = p.text.split() 84 | assert(len(coords) % 3 == 0) 85 | #-- Store the coordinate tuple 86 | for i in range(0, len(coords), 3): 87 | listPoints.append([float(coords[i]), float(coords[i+1]), float(coords[i+2])]) 88 | else: 89 | return None 90 | 91 | return listPoints -------------------------------------------------------------------------------- /plotcolorbar.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | # The MIT License (MIT) 5 | 6 | # This code is part of the CityGML2OBJs package 7 | 8 | # Copyright (c) 2014 9 | # Filip Biljecki 10 | # Delft University of Technology 11 | # fbiljecki@gmail.com 12 | 13 | # Permission is hereby granted, free of charge, to any person obtaining a copy 14 | # of this software and associated documentation files (the "Software"), to deal 15 | # in the Software without restriction, including without limitation the rights 16 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 17 | # copies of the Software, and to permit persons to whom the Software is 18 | # furnished to do so, subject to the following conditions: 19 | 20 | # The above copyright notice and this permission notice shall be included in 21 | # all copies or substantial portions of the Software. 22 | 23 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 24 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 25 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 26 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 27 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 28 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 29 | # THE SOFTWARE. 30 | 31 | import matplotlib as mpl 32 | import matplotlib.pyplot as plt 33 | 34 | #-- Resource: http://matplotlib.org/examples/api/colorbar_only.html 35 | plt.rc('text', usetex=True) 36 | plt.rc('font', family='serif') 37 | #-- Make a figure and axes with dimensions as desired 38 | fig = plt.figure(figsize=(8,1)) 39 | ax1 = fig.add_axes([0.05, 0.80, 0.9, 0.15]) 40 | 41 | #-- Bounds 42 | vmin = 350 43 | vmax = 1300 44 | 45 | #-- Colormap 46 | cmap = mpl.cm.afmhot 47 | norm = mpl.colors.Normalize(vmin=vmin, vmax=vmax) 48 | 49 | cb1 = mpl.colorbar.ColorbarBase(ax1, cmap=cmap, 50 | norm=norm, 51 | orientation='horizontal') 52 | 53 | #-- Label on the axis 54 | cb1.set_label(r"Annual solar irradiation [kWh/m$^{2}$/year]") 55 | 56 | 57 | cmap = mpl.colors.ListedColormap(['r', 'g', 'b', 'c']) 58 | cmap.set_over('0.25') 59 | cmap.set_under('0.75') 60 | 61 | bounds = [1, 2, 4, 7, 8] 62 | norm = mpl.colors.BoundaryNorm(bounds, cmap.N) 63 | 64 | #-- Change the last tick label 65 | labels = [item.get_text() for item in cb1.ax.get_xticklabels()] 66 | li = 0 67 | for l in labels: 68 | # labels[li] = r"$"+str(l)+"$" 69 | labels[li] = r""+str(l) 70 | li += 1 71 | labels[-1] = r"$\geq "+str(vmax) + "$" 72 | cb1.ax.set_xticklabels(labels) 73 | 74 | #-- Output 75 | plt.savefig('colorbar.pdf', transparent=True) 76 | plt.savefig('colorbar.png', dpi=600, transparent=True) 77 | plt.show() -------------------------------------------------------------------------------- /polygon3dmodule.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | # The MIT License (MIT) 5 | 6 | # This code is part of the CityGML2OBJs package 7 | 8 | # Copyright (c) 2014 9 | # Filip Biljecki 10 | # Delft University of Technology 11 | # fbiljecki@gmail.com 12 | 13 | # Permission is hereby granted, free of charge, to any person obtaining a copy 14 | # of this software and associated documentation files (the "Software"), to deal 15 | # in the Software without restriction, including without limitation the rights 16 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 17 | # copies of the Software, and to permit persons to whom the Software is 18 | # furnished to do so, subject to the following conditions: 19 | 20 | # The above copyright notice and this permission notice shall be included in 21 | # all copies or substantial portions of the Software. 22 | 23 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 24 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 25 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 26 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 27 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 28 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 29 | # THE SOFTWARE. 30 | 31 | import math 32 | import markup3dmodule 33 | from lxml import etree 34 | import copy 35 | import triangle 36 | import numpy as np 37 | import shapely 38 | 39 | def getAreaOfGML(poly, height=True): 40 | """Function which reads and returns its area. 41 | The function also accounts for the interior and checks for the validity of the polygon.""" 42 | exteriorarea = 0.0 43 | interiorarea = 0.0 44 | #-- Decompose the exterior and interior boundary 45 | e, i = markup3dmodule.polydecomposer(poly) 46 | #-- Extract points in the of 47 | epoints = markup3dmodule.GMLpoints(e[0]) 48 | if isPolyValid(epoints): 49 | if height: 50 | exteriorarea += get3DArea(epoints) 51 | else: 52 | exteriorarea += get2DArea(epoints) 53 | for idx, iring in enumerate(i): 54 | #-- Extract points in the of 55 | ipoints = markup3dmodule.GMLpoints(iring) 56 | if isPolyValid(ipoints): 57 | if height: 58 | interiorarea += get3DArea(ipoints) 59 | else: 60 | interiorarea += get2DArea(ipoints) 61 | #-- Account for the interior 62 | area = exteriorarea - interiorarea 63 | #-- Area in dimensionless units (coordinate units) 64 | return area 65 | 66 | #-- Validity of a polygon --------- 67 | def isPolyValid(polypoints, output=True): 68 | """Checks if a polygon is valid. Second option is to supress output.""" 69 | #-- Number of points of the polygon (including the doubled first/last point) 70 | npolypoints = len(polypoints) 71 | #-- Assume that it is valid, and try to disprove the assumption 72 | valid = True 73 | #-- Check if last point equal 74 | if polypoints[0] != polypoints[-1]: 75 | if output: 76 | print "\t\tA degenerate polygon. First and last points do not match." 77 | valid = False 78 | #-- Check if it has at least three points 79 | if npolypoints < 4: #-- Four because the first point is doubled as the last one in the ring 80 | if output: 81 | print "\t\tA degenerate polygon. The number of points is smaller than 3." 82 | valid = False 83 | #-- Check if the points are planar 84 | if not isPolyPlanar(polypoints): 85 | if output: 86 | print "\t\tA degenerate polygon. The points are not planar." 87 | valid = False 88 | #-- Check if some of the points are repeating 89 | for i in range (1, npolypoints): 90 | if polypoints[i] == polypoints[i-1]: 91 | if output: 92 | print "\t\tA degenerate polygon. There are identical points." 93 | valid = False 94 | #-- Check if the polygon does not have self-intersections 95 | #-- Disabled, something doesn't work here, will work on this later. 96 | #if not isPolySimple(polypoints): 97 | # print "A degenerate polygon. The edges are intersecting." 98 | # valid = False 99 | return valid 100 | 101 | 102 | def isPolyPlanar(polypoints): 103 | """Checks if a polygon is planar.""" 104 | #-- Normal of the polygon from the first three points 105 | try: 106 | normal = unit_normal(polypoints[0], polypoints[1], polypoints[2]) 107 | except: 108 | return False 109 | #-- Number of points 110 | npolypoints = len(polypoints) 111 | #-- Tolerance 112 | eps = 0.01 113 | #-- Assumes planarity 114 | planar = True 115 | for i in range (3, npolypoints): 116 | vector = [polypoints[i][0] - polypoints[0][0], polypoints[i][1] - polypoints[0][1], polypoints[i][2] - polypoints[0][2]] 117 | if math.fabs(dot(vector, normal)) > eps: 118 | planar = False 119 | return planar 120 | 121 | def isPolySimple(polypoints): 122 | """Checks if the polygon is simple, i.e. it does not have any self-intersections. 123 | Inspired by http://www.win.tue.nl/~vanwijk/2IV60/2IV60_exercise_3_answers.pdf""" 124 | npolypoints = len(polypoints) 125 | #-- Check if the polygon is vertical, i.e. a projection cannot be made. 126 | #-- First copy the list so the originals are not modified 127 | temppolypoints = copy.deepcopy(polypoints) 128 | newpolypoints = copy.deepcopy(temppolypoints) 129 | #-- If the polygon is vertical 130 | if math.fabs(unit_normal(temppolypoints[0], temppolypoints[1], temppolypoints[2])[2]) < 10e-6: 131 | vertical = True 132 | else: 133 | vertical = False 134 | #-- We want to project the vertical polygon to the XZ plane 135 | #-- If a polygon is parallel with the YZ plane that will not be possible 136 | YZ = True 137 | for i in range(1, npolypoints): 138 | if temppolypoints[i][0] != temppolypoints[0][0]: 139 | YZ = False 140 | continue 141 | #-- Project the plane in the special case 142 | if YZ: 143 | for i in range(0, npolypoints): 144 | newpolypoints[i][0] = temppolypoints[i][1] 145 | newpolypoints[i][1] = temppolypoints[i][2] 146 | #-- Project the plane 147 | elif vertical: 148 | for i in range(0, npolypoints): 149 | newpolypoints[i][1] = temppolypoints[i][2] 150 | else: 151 | pass #-- No changes here 152 | #-- Check for the self-intersection edge by edge 153 | for i in range(0, npolypoints-3): 154 | if i == 0: 155 | m = npolypoints - 3 156 | else: 157 | m = npolypoints - 2 158 | for j in range (i + 2, m): 159 | if intersection(newpolypoints[i], newpolypoints[i+1], newpolypoints[j%npolypoints], newpolypoints[(j+1)%npolypoints]): 160 | return False 161 | return True 162 | 163 | def intersection(p, q, r, s): 164 | """Check if two line segments (pq and rs) intersect. Computation is in 2D. 165 | Inspired by http://www.win.tue.nl/~vanwijk/2IV60/2IV60_exercise_3_answers.pdf""" 166 | 167 | eps = 10e-6 168 | 169 | V = [q[0] - p[0], q[1] - p[1]] 170 | W = [r[0] - s[0], r[1] - s[1]] 171 | 172 | d = V[0]*W[1] - W[0]*V[1] 173 | 174 | if math.fabs(d) < eps: 175 | return False 176 | else: 177 | return True 178 | #------------------------------------------ 179 | 180 | def collinear(p0, p1, p2): 181 | #-- http://stackoverflow.com/a/9609069 182 | x1, y1 = p1[0] - p0[0], p1[1] - p0[1] 183 | x2, y2 = p2[0] - p0[0], p2[1] - p0[1] 184 | return x1 * y2 - x2 * y1 < 1e-12 185 | 186 | #-- Area and other handy computations 187 | def det(a): 188 | """Determinant of matrix a.""" 189 | return a[0][0]*a[1][1]*a[2][2] + a[0][1]*a[1][2]*a[2][0] + a[0][2]*a[1][0]*a[2][1] - a[0][2]*a[1][1]*a[2][0] - a[0][1]*a[1][0]*a[2][2] - a[0][0]*a[1][2]*a[2][1] 190 | 191 | def unit_normal(a, b, c): 192 | """Unit normal vector of plane defined by points a, b, and c.""" 193 | x = det([[1,a[1],a[2]], 194 | [1,b[1],b[2]], 195 | [1,c[1],c[2]]]) 196 | y = det([[a[0],1,a[2]], 197 | [b[0],1,b[2]], 198 | [c[0],1,c[2]]]) 199 | z = det([[a[0],a[1],1], 200 | [b[0],b[1],1], 201 | [c[0],c[1],1]]) 202 | magnitude = (x**2 + y**2 + z**2)**.5 203 | if magnitude == 0.0: 204 | raise ValueError("The normal of the polygon has no magnitude. Check the polygon. The most common cause for this are two identical sequential points or collinear points.") 205 | return (x/magnitude, y/magnitude, z/magnitude) 206 | 207 | def dot(a, b): 208 | """Dot product of vectors a and b.""" 209 | return a[0]*b[0] + a[1]*b[1] + a[2]*b[2] 210 | 211 | def cross(a, b): 212 | """Cross product of vectors a and b.""" 213 | x = a[1] * b[2] - a[2] * b[1] 214 | y = a[2] * b[0] - a[0] * b[2] 215 | z = a[0] * b[1] - a[1] * b[0] 216 | return (x, y, z) 217 | 218 | def get3DArea(polypoints): 219 | """Function which reads the list of coordinates and returns its area. 220 | The code has been borrowed from http://stackoverflow.com/questions/12642256/python-find-area-of-polygon-from-xyz-coordinates""" 221 | #-- Compute the area 222 | total = [0, 0, 0] 223 | for i in range(len(polypoints)): 224 | vi1 = polypoints[i] 225 | if i is len(polypoints)-1: 226 | vi2 = polypoints[0] 227 | else: 228 | vi2 = polypoints[i+1] 229 | prod = cross(vi1, vi2) 230 | total[0] += prod[0] 231 | total[1] += prod[1] 232 | total[2] += prod[2] 233 | result = dot(total, unit_normal(polypoints[0], polypoints[1], polypoints[2])) 234 | return math.fabs(result*.5) 235 | 236 | 237 | def get2DArea(polypoints): 238 | """Reads the list of coordinates and returns its projected area (disregards z coords).""" 239 | flatpolypoints = copy.deepcopy(polypoints) 240 | for p in flatpolypoints: 241 | p[2] = 0.0 242 | return get3DArea(flatpolypoints) 243 | 244 | 245 | def getNormal(polypoints): 246 | """Get the normal of the first three points of a polygon. Assumes planarity.""" 247 | return unit_normal(polypoints[0], polypoints[1], polypoints[2]) 248 | 249 | 250 | def getAngles(normal): 251 | """Get the azimuth and altitude from the normal vector.""" 252 | #-- Convert from polar system to azimuth 253 | azimuth = 90 - math.degrees(math.atan2(normal[1], normal[0])) 254 | if azimuth >= 360.0: 255 | azimuth -= 360.0 256 | elif azimuth < 0.0: 257 | azimuth += 360.0 258 | t = math.sqrt(normal[0]**2 + normal[1]**2) 259 | if t == 0: 260 | tilt = 0.0 261 | else: 262 | tilt = 90 - math.degrees(math.atan(normal[2] / t)) #0 for flat roof, 90 for wall 263 | tilt = round(tilt, 3) 264 | 265 | return azimuth, tilt 266 | 267 | def GMLstring2points(pointstring): 268 | """Convert list of points in string to a list of points. Works for 3D points.""" 269 | listPoints = [] 270 | #-- List of coordinates 271 | coords = pointstring.split() 272 | #-- Store the coordinate tuple 273 | assert(len(coords) % 3 == 0) 274 | for i in range(0, len(coords), 3): 275 | listPoints.append([float(coords[i]), float(coords[i+1]), float(coords[i+2])]) 276 | return listPoints 277 | 278 | 279 | def smallestPoint(list_of_points): 280 | "Finds the smallest point from a three-dimensional tuple list." 281 | smallest = [] 282 | #-- Sort the points 283 | sorted_points = sorted(list_of_points, key=lambda x: (x[0], x[1], x[2])) 284 | #-- First one is the smallest one 285 | smallest = sorted_points[0] 286 | return smallest 287 | 288 | def highestPoint(list_of_points, a=None): 289 | "Finds the highest point from a three-dimensional tuple list." 290 | highest = [] 291 | #-- Sort the points 292 | sorted_points = sorted(list_of_points, key=lambda x: (x[0], x[1], x[2])) 293 | #-- Last one is the highest one 294 | if a is not None: 295 | equalZ = True 296 | for i in range(-1, -1 * len(list_of_points), -1): 297 | if equalZ: 298 | highest = sorted_points[i] 299 | if highest[2] != a[2]: 300 | equalZ = False 301 | break 302 | else: 303 | break 304 | else: 305 | highest = sorted_points[-1] 306 | return highest 307 | 308 | def centroid(list_of_points): 309 | """Returns the centroid of the list of points.""" 310 | sum_x = 0 311 | sum_y = 0 312 | sum_z = 0 313 | n = float(len(list_of_points)) 314 | for p in list_of_points: 315 | sum_x += float(p[0]) 316 | sum_y += float(p[1]) 317 | sum_z += float(p[2]) 318 | return [sum_x / n, sum_y / n, sum_z / n] 319 | 320 | 321 | def point_inside(list_of_points): 322 | """Returns a point that is guaranteed to be inside the polygon, thanks to Shapely.""" 323 | polygon = shapely.Polygon(list_of_points) 324 | return polygon.representative_point().coords 325 | 326 | 327 | def plane(a,b,c): 328 | """Returns the equation of a three-dimensional plane in space by entering the three coordinates of the plane.""" 329 | p_a = (b[1]-a[1])*(c[2]-a[2])-(c[1]-a[1])*(b[2]-a[2]) 330 | p_b = (b[2]-a[2])*(c[0]-a[0])-(c[2]-a[2])*(b[0]-a[0]) 331 | p_c = (b[0]-a[0])*(c[1]-a[1])-(c[0]-a[0])*(b[1]-a[1]) 332 | p_d = -1*(p_a * a[0] + p_b * a[1] + p_c * a[2]) 333 | return p_a, p_b, p_c, p_d 334 | 335 | def get_height(plane, x, y): 336 | """Get the missing coordinate from the plane equation and the partial coordinates.""" 337 | p_a, p_b, p_c, p_d = plane 338 | z = (-p_a*x - p_b*y - p_d)/p_c 339 | return z 340 | 341 | def get_y(plane, x, z): 342 | """Get the missing coordinate from the plane equation and the partial coordinates.""" 343 | p_a, p_b, p_c, p_d = plane 344 | y = (-p_a*x - p_c*z - p_d)/p_b 345 | return y 346 | 347 | def compare_normals(n1, n2): 348 | """Compares if two normals are equal or opposite. Takes into account a small tolerance to overcome floating point errors.""" 349 | tolerance = 10e-2 350 | #-- Assume equal and prove otherwise 351 | equal = True 352 | #-- i 353 | if math.fabs(n1[0] - n2[0]) > tolerance: 354 | equal = False 355 | #-- j 356 | elif math.fabs(n1[1] - n2[1]) > tolerance: 357 | equal = False 358 | #-- k 359 | elif math.fabs(n1[2] - n2[2]) > tolerance: 360 | equal = False 361 | return equal 362 | 363 | def reverse_vertices(vertices): 364 | """Reverse vertices. Useful to reorient the normal of the polygon.""" 365 | reversed_vertices = [] 366 | nv = len(vertices) 367 | for i in range(nv-1, -1, -1): 368 | reversed_vertices.append(vertices[i]) 369 | return reversed_vertices 370 | 371 | def triangulation(e, i): 372 | """Triangulate the polygon with the exterior and interior list of points. Works only for convex polygons. 373 | Assumes planarity. Projects to a 2D plane and goes back to 3D.""" 374 | vertices = [] 375 | holes = [] 376 | segments = [] 377 | index_point = 0 378 | #-- Slope computation points 379 | a = [[], [], []] 380 | b = [[], [], []] 381 | for ip in range(len(e)-1): 382 | vertices.append(e[ip]) 383 | if a == [[], [], []] and index_point == 0: 384 | a = [e[ip][0], e[ip][1], e[ip][2]] 385 | if index_point > 0 and (e[ip] != e[ip-1]): 386 | if b == [[], [], []]: 387 | b = [e[ip][0], e[ip][1], e[ip][2]] 388 | if ip == len(e) - 2: 389 | segments.append([index_point, 0]) 390 | else: 391 | segments.append([index_point, index_point+1]) 392 | index_point += 1 393 | for hole in i: 394 | first_point_in_hole = index_point 395 | for p in range(len(hole)-1): 396 | if p == len(hole)-2: 397 | segments.append([index_point, first_point_in_hole]) 398 | else: 399 | segments.append([index_point, index_point+1]) 400 | index_point += 1 401 | vertices.append(hole[p]) 402 | #-- A more robust way to get the point inside the hole, should work for non-convex interior polygons 403 | holes.append(point_inside(hole[:-1])) 404 | #-- Alternative, use centroid 405 | # holes.append(centroid(hole[:-1])) 406 | 407 | #-- Project to 2D since the triangulation cannot be done in 3D with the library that is used 408 | npolypoints = len(vertices) 409 | nholes = len(holes) 410 | #-- Check if the polygon is vertical, i.e. a projection cannot be made. 411 | #-- First copy the list so the originals are not modified 412 | temppolypoints = copy.deepcopy(vertices) 413 | newpolypoints = copy.deepcopy(vertices) 414 | tempholes = copy.deepcopy(holes) 415 | newholes = copy.deepcopy(holes) 416 | #-- Compute the normal of the polygon for detecting vertical polygons and 417 | #-- for the correct orientation of the new triangulated faces 418 | #-- If the polygon is vertical 419 | normal = unit_normal(temppolypoints[0], temppolypoints[1], temppolypoints[2]) 420 | if math.fabs(normal[2]) < 10e-2: 421 | vertical = True 422 | else: 423 | vertical = False 424 | #-- We want to project the vertical polygon to the XZ plane 425 | #-- If a polygon is parallel with the YZ plane that will not be possible 426 | YZ = True 427 | for i in range(1, npolypoints): 428 | if temppolypoints[i][0] != temppolypoints[0][0]: 429 | YZ = False 430 | continue 431 | #-- Project the plane in the special case 432 | if YZ: 433 | for i in range(0, npolypoints): 434 | newpolypoints[i][0] = temppolypoints[i][1] 435 | newpolypoints[i][1] = temppolypoints[i][2] 436 | for i in range(0, nholes): 437 | newholes[i][0] = tempholes[i][1] 438 | newholes[i][1] = tempholes[i][2] 439 | #-- Project the plane 440 | elif vertical: 441 | for i in range(0, npolypoints): 442 | newpolypoints[i][1] = temppolypoints[i][2] 443 | for i in range(0, nholes): 444 | newholes[i][1] = tempholes[i][2] 445 | else: 446 | pass #-- No changes here 447 | 448 | #-- Drop the last point (identical to first) 449 | for p in newpolypoints: 450 | p.pop(-1) 451 | 452 | #-- If there are no holes 453 | if len(newholes) == 0: 454 | newholes = None 455 | else: 456 | for h in newholes: 457 | h.pop(-1) 458 | 459 | #-- Plane information (assumes planarity) 460 | a = e[0] 461 | b = e[1] 462 | c = e[2] 463 | #-- Construct the plane 464 | pl = plane(a, b, c) 465 | 466 | #-- Prepare the polygon to be triangulated 467 | poly = {'vertices' : np.array(newpolypoints), 'segments' : np.array(segments), 'holes' : np.array(newholes)} 468 | # print poly 469 | #-- Triangulate 470 | t = triangle.triangulate(poly, "pQjz") 471 | #-- Get the triangles and their vertices 472 | tris = t['triangles'] 473 | vert = t['vertices'].tolist() 474 | #-- Store the vertices of each triangle in a list 475 | tri_points = [] 476 | for tri in tris: 477 | tri_points_tmp = [] 478 | for v in tri.tolist(): 479 | vert_adj = [[], [], []] 480 | if YZ: 481 | vert_adj[0] = temppolypoints[0][0] 482 | vert_adj[1] = vert[v][0] 483 | vert_adj[2] = vert[v][1] 484 | elif vertical: 485 | vert_adj[0] = vert[v][0] 486 | vert_adj[2] = vert[v][1] 487 | vert_adj[1] = get_y(pl, vert_adj[0], vert_adj[2]) 488 | else: 489 | vert_adj[0] = vert[v][0] 490 | vert_adj[1] = vert[v][1] 491 | vert_adj[2] = get_height(pl, vert_adj[0], vert_adj[1]) 492 | tri_points_tmp.append(vert_adj) 493 | try: 494 | tri_normal = unit_normal(tri_points_tmp[0], tri_points_tmp[1], tri_points_tmp[2]) 495 | except: 496 | continue 497 | if compare_normals(normal, tri_normal): 498 | tri_points.append(tri_points_tmp) 499 | else: 500 | tri_points_tmp = reverse_vertices(tri_points_tmp) 501 | tri_points.append(tri_points_tmp) 502 | return tri_points --------------------------------------------------------------------------------