From 636047b99cc2eb36c9ce213334296d1013dde7e1 Mon Sep 17 00:00:00 2001 From: guille Date: Thu, 25 Mar 2021 09:11:30 -0400 Subject: [PATCH] Partial implementation energy ADE, till geometry export (LOD2) this commit is broken, due some missing attribute (max_(coord)) at surface class --- city_model_structure/building.py | 52 +++++++++++-- exports/formats/energy_ade.py | 125 ++++++++++++++++++++++++++++--- exports/formats/stl.py | 2 +- exports/formats/triangular.py | 8 +- tests/test_exports.py | 3 +- 5 files changed, 169 insertions(+), 21 deletions(-) diff --git a/city_model_structure/building.py b/city_model_structure/building.py index 862ad9ff..bdc493d6 100644 --- a/city_model_structure/building.py +++ b/city_model_structure/building.py @@ -7,6 +7,7 @@ contributors: Pilar Monsalvete pilar_monsalvete@yahoo.es from typing import List +import numpy as np from city_model_structure.attributes.surface import Surface from city_model_structure.attributes.thermal_boundary import ThermalBoundary @@ -35,6 +36,7 @@ class Building(CityObject): self._average_storey_height = None self._storeys_above_ground = None self._floor_area = None + self._roof_type = None self._usage_zones = [] self._type = 'building' self._heating = dict() @@ -43,7 +45,9 @@ class Building(CityObject): self._global_horizontal = dict() self._diffuse = dict() self._beam = dict() - + self._grounds = [] + self._roofs = [] + self._walls = [] # ToDo: Check this for LOD4 self._thermal_zones = [] if self.lod < 4: @@ -52,11 +56,34 @@ class Building(CityObject): for t_zones in self._thermal_zones: t_zones.bounded = [ThermalBoundary(s, [t_zones]) for s in t_zones.surfaces] - surface_id = 0 for surface in self.surfaces: - surface.lower_corner = self._lower_corner - surface.parent(self, surface_id) - surface_id += 1 + if surface.type == 'Ground': + self._grounds.append(surface) + elif surface.type == 'Wall': + self._walls.append(surface) + else: + self._roofs.append(surface) + + @property + def grounds(self) -> [Surface]: + """ + Building ground surfaces + """ + return self._grounds + + @property + def roofs(self) -> [Surface]: + """ + Building roof surfaces + """ + return self._roofs + + @property + def walls(self) -> [Surface]: + """ + Building wall surfaces + """ + return self._walls @property def usage_zones(self) -> List[UsageZone]: @@ -325,6 +352,21 @@ class Building(CityObject): storeys.append(rest_trimesh) return storeys + @property + def roof_type(self): + """ + Roof type for the building flat or pitch + """ + if self._roof_type is None: + self._roof_type = 'flat' + for roof in self.roofs: + grads = np.rad2deg(roof.inclination) + if 355 > grads > 5: + self._roof_type = 'pitch' + break + print (self._roof_type) + return self._roof_type + @property def floor_area(self): """ diff --git a/exports/formats/energy_ade.py b/exports/formats/energy_ade.py index f3cac58a..2a293ac1 100644 --- a/exports/formats/energy_ade.py +++ b/exports/formats/energy_ade.py @@ -53,25 +53,26 @@ class EnergyAde: for building in self._city.buildings: building_dic = { 'bldg:Building': { - '@gml:id': building.name, + '@gml:id': building.name } } - building_dic = EnergyAde._get_measures(building, building_dic) + building_dic = EnergyAde._measures(building, building_dic) + building_dic = EnergyAde._building_geometry(building, building_dic, self._city) buildings.append(building_dic) energy_ade['core:CityModel']['core:cityObjectMember'] = buildings file_name = self._city.name + '_ade.gml' file_path = Path(self._path / file_name).resolve() with open(file_path, 'w' ) as file: - file.write(xmltodict.unparse(energy_ade,pretty=True)) + file.write(xmltodict.unparse(energy_ade,pretty=True, short_empty_elements=True)) @staticmethod - def _get_measures(building, building_dic): + def _measures(building, building_dic): measures = [] - measure = EnergyAde._get_measure(building.heating, 'year', 'Energy demand heating', 'INSEL') + measure = EnergyAde._measure(building.heating, 'year', 'Energy demand heating', 'INSEL') if measure is not None: measures.append(measure) - measure = EnergyAde._get_measure(building.cooling, 'year', 'Energy demand cooling', 'INSEL') + measure = EnergyAde._measure(building.cooling, 'year', 'Energy demand cooling', 'INSEL') if measure is not None: measures.append(measure) if len(measures) != 0: @@ -80,12 +81,12 @@ class EnergyAde: periods = [] for key in building.heating: if key != 'year': - period = EnergyAde._get_period(building.heating, key, 'Heating energy', 'INSEL') + period = EnergyAde._period(building.heating, key, 'Heating energy', 'INSEL') periods.append(period) for key in building.cooling: if key != 'year': - period = EnergyAde._get_period(building.cooling, key, 'Cooling energy', 'INSEL') + period = EnergyAde._period(building.cooling, key, 'Cooling energy', 'INSEL') periods.append(period) if len(periods) != 0: @@ -94,7 +95,7 @@ class EnergyAde: return building_dic @staticmethod - def _get_measure(measure_dict, key_value, name, source): + def _measure(measure_dict, key_value, name, source): measure = None if key_value in measure_dict: measure = { @@ -107,7 +108,7 @@ class EnergyAde: return measure @staticmethod - def _get_period(measure_dict, key_value, description, source): + def _period(measure_dict, key_value, description, source): period = { '@gml:id': uuid.uuid4(), 'energy:energyAmount': { @@ -130,4 +131,106 @@ class EnergyAde: } } } - return period \ No newline at end of file + return period + + @staticmethod + def _building_geometry(building, building_dic, city): + + building_dic['bldg:function'] = building.function + building_dic['bldg:usage'] = ', '.join([u.usage for u in building.usage_zones]) + building_dic['bldg:yearOfConstruction'] = building.year_of_construction + building_dic['bldg:roofType'] = building.roof_type + + building_dic['bldg:measuredHeight'] = { + '@uom': 'm', + '#text': f'{building.max_height}' + } + + building_dic['bldg:storeysAboveGround'] = building.storeys_above_ground + if building.lod == 1: + building_dic = EnergyAde._lod1(building, building_dic, city) + elif building.lod == 2: + building_dic = EnergyAde._lod2(building, building_dic, city) + else: + raise NotImplementedError('Only lod 1 and 2 can be exported') + return building_dic + + @staticmethod + def _lod1(building, building_dic, city): + raise NotImplementedError('Only lod 1 and 2 can be exported') + @staticmethod + def _lod2(building, building_dic, city): + surface_members = [] + boundaries = [{ + 'gml:Envelope': { + '@srsName': city.srs_name, + '@srsDimension': 3, + 'gml:lowerCorner': ' '.join([str(e) for e in city.lower_corner]), + 'gml:upperCorner': ' '.join([str(e) for e in city.upper_corner]) + }}] + for surface in building.surfaces: + surface_member = { '@xlink:href': f'#PolyId{surface.name}'} + surface_members.append(surface_member) + if surface.type == 'Wall': + surface_type = 'bldg:WallSurface' + elif surface.type == 'Ground': + surface_type = 'bldg:GroundSurface' + else: + surface_type = 'bldg:RoofSurface' + + + + surface_dic = { + surface_type: { + '@gml:id': f'GML_{uuid.uuid4()}', + 'gml:name': f'{surface.name} ({surface.type})', + 'gml:boundedBy': { + 'gml:Envelope': { + '@srsName': city.srs_name, + 'gml:lowerCorner': f'{surface.min_x} {surface.min_y} {surface.min_z}', + 'gml:upperCorner': f'{surface.max_x} {surface.max_y} {surface.max_z}' + } + }, + 'bldg:lod2MultiSurface': { + 'gml:MultiSurface': { + '@srsName': city.srs_name, + '@gml:id': f'GML_{uuid.uuid4()}', + 'surfaceMember': { + 'gml:Polygon': { + '@srsName': city.srs_name, + '@gml:id': f'#PolyId{surface.name}', + 'gml:exterior': { + 'gml:LinearRing': { + '@gml:id': f'#PolyId{surface.name}_0', + 'gml:posList': { + '@srsDimension': '3', + '@count': len(surface.points_list) + 1, + '#text': f'{" ".join(map(str, surface.points_list))} {" ".join(map(str, surface.points[0]))}' + } + } + } + } + } + } + } + } + } + print(surface_dic) + boundaries.append(surface_dic) + print(surface_dic) + building_dic['bldg:lod2Solid'] = { + 'gml:Solid': { + '@gml:id': f'GML_{uuid.uuid4()}', + 'gml:exterior': { + 'gml:CompositeSurface': { + '@srsName': city.srs_name, + '@gml:id': f'GML_{uuid.uuid4()}', + 'gml:surfaceMember': surface_members + } + } + } + } + + building_dic['gml:boundedBy'] = boundaries + print(building_dic) + return building_dic \ No newline at end of file diff --git a/exports/formats/stl.py b/exports/formats/stl.py index 40af72fb..a913689f 100644 --- a/exports/formats/stl.py +++ b/exports/formats/stl.py @@ -9,4 +9,4 @@ from exports.formats.triangular import Triangular class Stl(Triangular): def __init__(self, city, path): - super().__init__(city, path, 'stl') + super().__init__(city, path, 'stl', write_mode='wb') diff --git a/exports/formats/triangular.py b/exports/formats/triangular.py index a69182c8..62f307da 100644 --- a/exports/formats/triangular.py +++ b/exports/formats/triangular.py @@ -8,20 +8,22 @@ from trimesh import Trimesh class Triangular: - def __init__(self, city, path, triangular_format): + def __init__(self, city, path, triangular_format, write_mode='w'): self._city = city self._path = path self._triangular_format = triangular_format + self._write_mode = write_mode self._export() + def _export(self): if self._city.name is None: self._city.name = 'unknown_city' file_name = self._city.name + '.' + self._triangular_format file_path = (Path(self._path).resolve() / file_name).resolve() - print(file_path) trimesh = Trimesh() for building in self._city.buildings: trimesh = trimesh.union(building.simplified_polyhedron.trimesh) - with open(file_path, 'w') as file: + + with open(file_path, self._write_mode) as file: file.write(trimesh.export(file_type=self._triangular_format)) diff --git a/tests/test_exports.py b/tests/test_exports.py index 84461af9..4d3dceb8 100644 --- a/tests/test_exports.py +++ b/tests/test_exports.py @@ -6,6 +6,7 @@ Copyright © 2020 Project Author Guille Gutierrez guillermo.gutierrezmorote@conc from pathlib import Path from unittest import TestCase +from imports.geometry_factory import GeometryFactory from exports.exports_factory import ExportsFactory from city_model_structure.city import City @@ -25,7 +26,7 @@ class TestExports(TestCase): def _get_city(self): if self._city_gml is None: - file_path = (self._example_path / 'kelowna.pickle').resolve() + file_path = (self._example_path / 'one_building_in_kelowna.pickle').resolve() self._city_gml = City.load(file_path) return self._city_gml