Add exports for cesium tileset and glb format

This commit is contained in:
Guille Gutierrez 2023-10-10 11:16:26 +02:00
parent 568317ebf1
commit 6b24a59178
5 changed files with 236 additions and 7 deletions

View File

@ -7,9 +7,11 @@ Project Coder Guille Gutierrez guillermo.gutierrezmorote@concordia.ca
from pathlib import Path
from hub.exports.formats.glb import Glb
from hub.exports.formats.obj import Obj
from hub.exports.formats.simplified_radiosity_algorithm import SimplifiedRadiosityAlgorithm
from hub.exports.formats.stl import Stl
from hub.exports.formats.cesiumjs_tileset import CesiumjsTileset
from hub.helpers.utils import validate_import_export_type
@ -17,7 +19,7 @@ class ExportsFactory:
"""
Exports factory class
"""
def __init__(self, handler, city, path, target_buildings=None, adjacent_buildings=None):
def __init__(self, handler, city, path, target_buildings=None, adjacent_buildings=None, base_uri=None):
self._city = city
self._handler = '_' + handler.lower()
validate_import_export_type(ExportsFactory, handler)
@ -26,6 +28,7 @@ class ExportsFactory:
self._path = path
self._target_buildings = target_buildings
self._adjacent_buildings = adjacent_buildings
self._base_uri = base_uri
@property
def _citygml(self):
@ -61,9 +64,26 @@ class ExportsFactory:
Export the city to Simplified Radiosity Algorithm xml format
:return: None
"""
return SimplifiedRadiosityAlgorithm(self._city,
(self._path / f'{self._city.name}_sra.xml'),
target_buildings=self._target_buildings)
return SimplifiedRadiosityAlgorithm(
self._city, (self._path / f'{self._city.name}_sra.xml'), target_buildings=self._target_buildings
)
@property
def _cesiumjs_tileset(self):
"""
Export the city to a cesiumJs tileset format
:return: None
"""
return CesiumjsTileset(
self._city,
(self._path / f'{self._city.name}.json'),
target_buildings=self._target_buildings,
base_uri=self._base_uri
)
@property
def _glb(self):
return Glb(self._city, self._path, target_buildings=self._target_buildings)
def export(self):
"""

View File

@ -0,0 +1,137 @@
import json
import math
import pyproj
from pyproj import Transformer
from hub.helpers.geometry_helper import GeometryHelper
class CesiumjsTileset:
def __init__(self, city, file_name, target_buildings=None, base_uri=None):
self._city = city
self._file_name = file_name
self._target_buildings = target_buildings
if base_uri is None:
base_uri = '.'
self._base_uri = base_uri
try:
srs_name = self._city.srs_name
if self._city.srs_name in GeometryHelper.srs_transformations:
srs_name = GeometryHelper.srs_transformations[self._city.srs_name]
input_reference = pyproj.CRS(srs_name) # Projected coordinate system from input data
except pyproj.exceptions.CRSError as err:
raise pyproj.exceptions.CRSError from err
self._to_gps = Transformer.from_crs(input_reference, pyproj.CRS('EPSG:4326'))
self._tile_set = {
'asset': {
'version': '1.1',
"tilesetVersion": "1.2.3"
},
'schema': {
'id': "building",
'classes': {
'building': {
"properties": {
'name': {
'type': 'STRING'
},
'position': {
'type': 'SCALAR',
'array': True,
'componentType': 'FLOAT32'
},
'aliases': {
'type': 'STRING',
'array': True,
},
'volume': {
'type': 'SCALAR',
'componentType': 'FLOAT32'
},
'floor_area': {
'type': 'SCALAR',
'componentType': 'FLOAT32'
},
'max_height': {
'type': 'SCALAR',
'componentType': 'INT32'
},
'year_of_construction': {
'type': 'SCALAR',
'componentType': 'INT32'
},
'function': {
'type': 'STRING'
},
'usages_percentage': {
'type': 'STRING'
}
}
}
}
},
'geometricError': 240,
'root': {
'boundingVolume': {
'box': CesiumjsTileset._box_values(self._city.upper_corner, self._city.lower_corner)
},
'geometricError': 70,
'refine': 'ADD',
'children': []
}
}
self._export()
@staticmethod
def _box_values(upper_corner, lower_corner):
x = (upper_corner[0] - lower_corner[0]) / 2
y = (upper_corner[1] - lower_corner[1]) / 2
z = (upper_corner[2] - lower_corner[2]) / 2
return [x, y, z, x, 0, 0, 0, y, 0, 0, 0, z]
def _ground_coordinates(self, coordinates):
ground_coordinates = []
for coordinate in coordinates:
ground_coordinates.append(
(coordinate[0] - self._city.lower_corner[0], coordinate[1] - self._city.lower_corner[1])
)
return ground_coordinates
def _export(self):
for building in self._city.buildings:
upper_corner = [-math.inf, -math.inf, 0]
lower_corner = [math.inf, math.inf, 0]
for surface in building.grounds: # todo: maybe we should add the terrain?
coordinates = self._ground_coordinates(surface.solid_polygon.coordinates)
lower_corner = [min([c[0] for c in coordinates]), min([c[1] for c in coordinates]), 0]
upper_corner = [max([c[0] for c in coordinates]), max([c[1] for c in coordinates]), building.max_height]
tile = {
'boundingVolume': {
'box': CesiumjsTileset._box_values(upper_corner, lower_corner)
},
'geometricError': 70,
'metadata': {
'class': 'building',
'properties': {
'name': building.name,
'position': self._to_gps.transform(lower_corner[0], lower_corner[1]),
'aliases': building.aliases,
'volume': building.volume,
'floor_area': building.floor_area,
'max_height': building.max_height,
'year_of_construction': building.year_of_construction,
'function': building.function,
'usages_percentage': building.usages_percentage
}
},
'content': {
'uri': f'{self._base_uri}/{building.name}.glb'
}
}
self._tile_set['root']['children'].append(tile)
with open(self._file_name, 'w') as f:
json.dump(self._tile_set, f)

View File

@ -0,0 +1,52 @@
import glob
import os
import shutil
import subprocess
from hub.city_model_structure.city import City
from hub.exports.formats.obj import Obj
class GltExceptionError(Exception):
"""
Glt execution error
"""
class Glb:
def __init__(self, city, path, target_buildings=None):
self._city = city
self._path = path
if target_buildings is None:
target_buildings = [b.name for b in self._city.buildings]
self._target_buildings = target_buildings
self._export()
@property
def _obj2gtl(self):
"""
Get the SRA installation path
:return: str
"""
return shutil.which('obj2gltf')
def _export(self):
try:
for building in self._city.buildings:
city = City(building.lower_corner, building.upper_corner, self._city.srs_name)
city.name = building.name
city.add_city_object(building)
Obj( city, self._path)
glb = f'{self._path}/{building.name}.glb'
subprocess.run([
self._obj2gtl,
'-i', f'{self._path}/{building.name}.obj',
'-o', f'{glb}',
'-b',
'--normalTexture', f'{self._path}/{building.name}.mtl'
])
os.unlink(f'{self._path}/{building.name}.obj')
os.unlink(f'{self._path}/{building.name}.mtl')
except (subprocess.SubprocessError, subprocess.TimeoutExpired, subprocess.CalledProcessError) as err:
raise GltExceptionError from err

View File

@ -55,7 +55,7 @@ class Obj:
with open(mtl_file_path, 'w', encoding='utf-8') as mtl:
mtl.write("newmtl cerc_base_material\n")
mtl.write("Ka 1.0 1.0 1.0 # Ambient color (white)\n")
mtl.write("Kd 0.3 0.8 0.3 # Diffuse color (greenish)\n")
mtl.write("Kd 0.3 0.1 0.3 # Diffuse color (greenish)\n")
mtl.write("Ks 1.0 1.0 1.0 # Specular color (white)\n")
mtl.write("Ns 400.0 # Specular exponent (defines shininess)\n")
vertices = {}

View File

@ -5,7 +5,7 @@ Copyright © 2022 Concordia CERC group
Project Coder Guille Gutierrez guillermo.gutierrezmorote@concordia.ca
Code contributors: Pilar Monsalvete Alvarez de Uribarri pilar.monsalvete@concordia.ca
"""
import json
import logging.handlers
from pathlib import Path
from unittest import TestCase
@ -66,7 +66,7 @@ class TestExports(TestCase):
def _export(self, export_type, from_pickle=False):
self._complete_city = self._get_complete_city(from_pickle)
ExportsFactory(export_type, self._complete_city, self._output_path).export()
ExportsFactory(export_type, self._complete_city, self._output_path, base_uri='../glb').export()
def _export_building_energy(self, export_type, from_pickle=False):
self._complete_city = self._get_complete_city(from_pickle)
@ -78,6 +78,26 @@ class TestExports(TestCase):
"""
self._export('obj', False)
def test_cesiumjs_tileset_export(self):
"""
export to cesiumjs tileset
"""
self._export('cesiumjs_tileset', False)
tileset = Path(self._output_path / f'{self._city.name}.json')
self.assertTrue(tileset.exists())
with open(tileset, 'r') as f:
json_tileset = json.load(f)
self.assertEqual(1, len(json_tileset['root']['children']), "Wrong number of children")
def test_glb_export(self):
"""
export to glb format
"""
self._export('glb', False)
for building in self._city.buildings:
glb_file = Path(self._output_path / f'{building.name}.glb')
self.assertTrue(glb_file.exists(), f'{building.name} Building glb wasn\'t correctly generated')
def test_energy_ade_export(self):
"""
export to energy ADE