Add exports for cesium tileset and glb format
This commit is contained in:
parent
568317ebf1
commit
6b24a59178
|
@ -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):
|
||||
"""
|
||||
|
|
137
hub/exports/formats/cesiumjs_tileset.py
Normal file
137
hub/exports/formats/cesiumjs_tileset.py
Normal 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)
|
52
hub/exports/formats/glb.py
Normal file
52
hub/exports/formats/glb.py
Normal 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
|
||||
|
|
@ -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 = {}
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue
Block a user