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 pathlib import Path
|
||||||
|
|
||||||
|
from hub.exports.formats.glb import Glb
|
||||||
from hub.exports.formats.obj import Obj
|
from hub.exports.formats.obj import Obj
|
||||||
from hub.exports.formats.simplified_radiosity_algorithm import SimplifiedRadiosityAlgorithm
|
from hub.exports.formats.simplified_radiosity_algorithm import SimplifiedRadiosityAlgorithm
|
||||||
from hub.exports.formats.stl import Stl
|
from hub.exports.formats.stl import Stl
|
||||||
|
from hub.exports.formats.cesiumjs_tileset import CesiumjsTileset
|
||||||
from hub.helpers.utils import validate_import_export_type
|
from hub.helpers.utils import validate_import_export_type
|
||||||
|
|
||||||
|
|
||||||
|
@ -17,7 +19,7 @@ class ExportsFactory:
|
||||||
"""
|
"""
|
||||||
Exports factory class
|
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._city = city
|
||||||
self._handler = '_' + handler.lower()
|
self._handler = '_' + handler.lower()
|
||||||
validate_import_export_type(ExportsFactory, handler)
|
validate_import_export_type(ExportsFactory, handler)
|
||||||
|
@ -26,6 +28,7 @@ class ExportsFactory:
|
||||||
self._path = path
|
self._path = path
|
||||||
self._target_buildings = target_buildings
|
self._target_buildings = target_buildings
|
||||||
self._adjacent_buildings = adjacent_buildings
|
self._adjacent_buildings = adjacent_buildings
|
||||||
|
self._base_uri = base_uri
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def _citygml(self):
|
def _citygml(self):
|
||||||
|
@ -61,9 +64,26 @@ class ExportsFactory:
|
||||||
Export the city to Simplified Radiosity Algorithm xml format
|
Export the city to Simplified Radiosity Algorithm xml format
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
return SimplifiedRadiosityAlgorithm(self._city,
|
return SimplifiedRadiosityAlgorithm(
|
||||||
(self._path / f'{self._city.name}_sra.xml'),
|
self._city, (self._path / f'{self._city.name}_sra.xml'), target_buildings=self._target_buildings
|
||||||
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):
|
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:
|
with open(mtl_file_path, 'w', encoding='utf-8') as mtl:
|
||||||
mtl.write("newmtl cerc_base_material\n")
|
mtl.write("newmtl cerc_base_material\n")
|
||||||
mtl.write("Ka 1.0 1.0 1.0 # Ambient color (white)\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("Ks 1.0 1.0 1.0 # Specular color (white)\n")
|
||||||
mtl.write("Ns 400.0 # Specular exponent (defines shininess)\n")
|
mtl.write("Ns 400.0 # Specular exponent (defines shininess)\n")
|
||||||
vertices = {}
|
vertices = {}
|
||||||
|
|
|
@ -5,7 +5,7 @@ Copyright © 2022 Concordia CERC group
|
||||||
Project Coder Guille Gutierrez guillermo.gutierrezmorote@concordia.ca
|
Project Coder Guille Gutierrez guillermo.gutierrezmorote@concordia.ca
|
||||||
Code contributors: Pilar Monsalvete Alvarez de Uribarri pilar.monsalvete@concordia.ca
|
Code contributors: Pilar Monsalvete Alvarez de Uribarri pilar.monsalvete@concordia.ca
|
||||||
"""
|
"""
|
||||||
|
import json
|
||||||
import logging.handlers
|
import logging.handlers
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from unittest import TestCase
|
from unittest import TestCase
|
||||||
|
@ -66,7 +66,7 @@ class TestExports(TestCase):
|
||||||
|
|
||||||
def _export(self, export_type, from_pickle=False):
|
def _export(self, export_type, from_pickle=False):
|
||||||
self._complete_city = self._get_complete_city(from_pickle)
|
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):
|
def _export_building_energy(self, export_type, from_pickle=False):
|
||||||
self._complete_city = self._get_complete_city(from_pickle)
|
self._complete_city = self._get_complete_city(from_pickle)
|
||||||
|
@ -78,6 +78,26 @@ class TestExports(TestCase):
|
||||||
"""
|
"""
|
||||||
self._export('obj', False)
|
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):
|
def test_energy_ade_export(self):
|
||||||
"""
|
"""
|
||||||
export to energy ADE
|
export to energy ADE
|
||||||
|
|
Loading…
Reference in New Issue
Block a user