""" CityObject module SPDX - License - Identifier: LGPL - 3.0 - or -later Copyright © 2020 Project Author Guille Gutierrez guillermo.gutierrezmorote@concordia.ca """ from typing import Union, List from pathlib import Path 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 import matplotlib.patches as patches from helpers.geometry_helper import GeometryHelper from city_model_structure.usage_zone import UsageZone 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 """ 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): """ 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