""" CityObject module SPDX - License - Identifier: LGPL - 3.0 - or -later Copyright © 2020 Project Author Guille Gutierrez guillermo.gutierrezmorote@concordia.ca """ from matplotlib import pylab from city_model_structure.polyhedron import Polyhedron from city_model_structure.thermal_zone import ThermalZone from city_model_structure.thermal_boundary import ThermalBoundary from city_model_structure.surface import Surface from shapely import ops from shapely.geometry import MultiPolygon import numpy as np from pathlib import Path import matplotlib.patches as patches from helpers.geometry import Geometry from city_model_structure.usage_zone import UsageZone from typing import Union, List class CityObject: def __init__(self, name, lod, surfaces, terrains, year_of_construction, function, lower_corner, attic_heated=0, basement_heated=0): self._name = name self._lod = lod self._surfaces = surfaces self._polyhedron = None self._basement_heated = basement_heated self._attic_heated = attic_heated self._terrains = terrains self._year_of_construction = year_of_construction self._function = function self._lower_corner = lower_corner self._geometry = Geometry() self._average_storey_height = None self._storeys_above_ground = None self._foot_print = None self._usage_zones = [] # ToDo: this need to be changed when we have other city_objects beside "buildings" self._type = 'building' # ToDo: Check this for LOD4 self._thermal_zones = [] if self.lod < 8: # for lod under 4 is just one thermal zone self._thermal_zones.append(ThermalZone(self.surfaces)) for t in self._thermal_zones: t.bounded = [ThermalBoundary(s, [t]) for s in t.surfaces] surface_id = 0 for s in self._surfaces: s.lower_corner = self._lower_corner s.parent(self, surface_id) surface_id += 1 @property def usage_zones(self) -> List[UsageZone]: return self._usage_zones @usage_zones.setter def usage_zones(self, values): # ToDo: this is only valid for one usage zone need to be revised for multiple usage zones. self._usage_zones = values for thermal_zone in self.thermal_zones: thermal_zone.usage_zones = [(100, usage_zone) for usage_zone in values] @property def terrains(self) -> List[Surface]: return self._terrains @property def attic_heated(self): return self._attic_heated @attic_heated.setter def attic_heated(self, value): self._attic_heated = value @property def basement_heated(self): return self._basement_heated @basement_heated.setter def basement_heated(self, value): self._attic_heated = value @property def name(self): return self._name @property def lod(self): return self._lod @property def surfaces(self) -> List[Surface]: return self._surfaces def surface(self, name) -> Union[Surface, None]: for s in self.surfaces: if s.name == name: return s return None @property def thermal_zones(self) -> List[ThermalZone]: return self._thermal_zones @property def volume(self): if self._polyhedron is None: self._polyhedron = Polyhedron(self.surfaces) return self._polyhedron.volume @property def heated_volume(self): if self._polyhedron is None: self._polyhedron = Polyhedron(self.surfaces) # ToDo: this need to be the calculated based on the basement and attic heated values return self._polyhedron.volume def stl_export(self, path): if self._polyhedron is None: self._polyhedron = Polyhedron(self.surfaces) full_path = (Path(path) / (self._name + '.stl')).resolve() self._polyhedron.save(full_path) @property def year_of_construction(self): return self._year_of_construction @property def function(self): return self._function @property def average_storey_height(self): return self._average_storey_height @average_storey_height.setter def average_storey_height(self, value): self._average_storey_height = value @property def storeys_above_ground(self): return self._storeys_above_ground @storeys_above_ground.setter def storeys_above_ground(self, value): self._storeys_above_ground = value @staticmethod def _tuple_to_point(xy_tuple): return [xy_tuple[0], xy_tuple[1], 0.0] def _plot(self, polygon): points = () for point_tuple in polygon.exterior.coords: almost_equal = False for point in points: p1 = CityObject._tuple_to_point(point) p2 = CityObject._tuple_to_point(point_tuple) if self._geometry.almost_equal(p1, p2): almost_equal = True break if not almost_equal: points = points + (point_tuple,) points = points + (points[0],) pylab.scatter([point[0] for point in points], [point[1] for point in points]) pylab.gca().add_patch(patches.Polygon(points, closed=True, fill=True)) pylab.grid() pylab.show() @property def foot_print(self) -> Surface: if self._foot_print is None: shapelys = [] union = None for surface in self.surfaces: if surface.shapely.is_empty or not surface.shapely.is_valid: continue shapelys.append(surface.shapely) union = ops.unary_union(shapelys) shapelys = [union] if type(union) == MultiPolygon: Exception('foot print returns a multipolygon') points_list = [] for point_tuple in union.exterior.coords: # ToDo: should be Z 0.0 or min Z? point = CityObject._tuple_to_point(point_tuple) almost_equal = False for existing_point in points_list: if self._geometry.almost_equal(point, existing_point): almost_equal = True break if not almost_equal: points_list.append(point) points_list = np.reshape(points_list, len(points_list) * 3) points = np.array_str(points_list).replace('[', '').replace(']', '') self._foot_print = Surface(points, remove_last=False, is_projected=True) return self._foot_print @property def type(self): return self._type @property def max_height(self): if self._polyhedron is None: self._polyhedron = Polyhedron(self.surfaces) return self._polyhedron.max_z