""" CityObject module SPDX - License - Identifier: LGPL - 3.0 - or -later Copyright © 2020 Project Author Guille Gutierrez guillermo.gutierrezmorote@concordia.ca """ from pathlib import Path from typing import Union, List import matplotlib.patches as patches import numpy as np from matplotlib import pylab from shapely import ops from shapely.geometry import MultiPolygon from city_model_structure.polyhedron import Polyhedron from city_model_structure.surface import Surface from city_model_structure.thermal_boundary import ThermalBoundary from city_model_structure.thermal_zone import ThermalZone from city_model_structure.usage_zone import UsageZone from helpers.geometry_helper import GeometryHelper class CityObject: """ CityObject class """ 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 = GeometryHelper() 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_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 @property def usage_zones(self) -> List[UsageZone]: """ Get city object usage zones :return: [UsageZone] """ return self._usage_zones @usage_zones.setter def usage_zones(self, values): """ Set city objects usage zones :param values: [UsageZones] :return: None """ # 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]: """ Get city object terrain surfaces :return: [Surface] """ return self._terrains @property def attic_heated(self): """ Get if the city object attic is heated :return: Boolean """ return self._attic_heated @attic_heated.setter def attic_heated(self, value): """ Set if the city object attic is heated :param value: Boolean :return: None """ self._attic_heated = value @property def basement_heated(self): """ Get if the city object basement is heated :return: Boolean """ return self._basement_heated @basement_heated.setter def basement_heated(self, value): """ Set if the city object basement is heated :param value: Boolean :return: None """ self._attic_heated = value @property def name(self): """ City object name :return: str """ return self._name @property def lod(self): """ City object level of detail 1, 2, 3 or 4 :return: int """ return self._lod @property def surfaces(self) -> List[Surface]: """ City object surfaces :return: [Surface] """ return self._surfaces def surface(self, name) -> Union[Surface, None]: """ Get the city object surface with a given name :param name: str :return: None or Surface """ for s in self.surfaces: if s.name == name: return s return None @property def thermal_zones(self) -> List[ThermalZone]: """ City object thermal zones :return: [ThermalZone] """ return self._thermal_zones @property def volume(self): """ City object volume in cubic meters :return: float """ if self._polyhedron is None: self._polyhedron = Polyhedron(self.surfaces) return self._polyhedron.volume @property def heated_volume(self): """ City object heated volume in cubic meters :return: float """ 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): """ Export the city object to stl file (city_object_name.stl) to the given path :param path: str :return: None """ # todo: this is a method just for debugging, it will be soon removed if self._polyhedron is None: self._polyhedron = Polyhedron(self.surfaces) full_path = (Path(path) / (self._name + '.stl')).resolve() self._polyhedron.export(full_path) @property def year_of_construction(self): """ City object year of construction :return: int """ return self._year_of_construction @property def function(self): """ City object function :return: str """ return self._function @property def average_storey_height(self): """ Get city object average storey height in meters :return: float """ return self._average_storey_height @average_storey_height.setter def average_storey_height(self, value): """ Set city object average storey height in meters :param value: float :return: None """ self._average_storey_height = value @property def storeys_above_ground(self): """ Get city object storeys number above ground :return: int """ return self._storeys_above_ground @storeys_above_ground.setter def storeys_above_ground(self, value): """ Set city object storeys number above ground :param value: int :return: """ 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: point_1 = CityObject._tuple_to_point(point) point_2 = CityObject._tuple_to_point(point_tuple) if self._geometry.almost_equal(point_1, point_2): 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: """ City object foot print surface :return: 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 isinstance(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): """ City object type :return: str """ return self._type @property def max_height(self): """ City object maximal height in meters :return: float """ if self._polyhedron is None: self._polyhedron = Polyhedron(self.surfaces) return self._polyhedron.max_z