""" Surface module SPDX - License - Identifier: LGPL - 3.0 - or -later Copyright © 2022 Concordia CERC group Project Coder Guille Gutierrez guillermo.gutierrezmorote@concordia.ca Code contributors: Pilar Monsalvete Alvarez de Uribarri pilar.monsalvete@concordia.ca """ from __future__ import annotations import uuid import numpy as np from typing import List, Union from city_model_structure.attributes.polygon import Polygon from city_model_structure.attributes.plane import Plane from city_model_structure.attributes.point import Point import helpers.constants as cte class Surface: """ Surface class """ def __init__(self, solid_polygon, perimeter_polygon, holes_polygons=None, name=None, surface_type=None, swr=None): self._type = surface_type self._swr = swr self._name = name self._id = None self._azimuth = None self._inclination = None self._area_above_ground = None self._area_below_ground = None self._lower_corner = None self._upper_corner = None self._shared_surfaces = [] self._global_irradiance = dict() self._perimeter_polygon = perimeter_polygon self._holes_polygons = holes_polygons self._solid_polygon = solid_polygon self._pv_system_installed = None self._inverse = None # todo: create self._associated_thermal_boundaries and bring the vegetation here instead of in thermal_boundary @property def name(self): """ Get the surface name :return: str """ if self._name is None: self._name = str(uuid.uuid4()) return self._name @property def id(self): """ Get the surface id :return: str """ if self._id is None: raise ValueError('Undefined surface id') return self._id @id.setter def id(self, value): """ Set the surface id :param value: str """ if value is not None: self._id = str(value) # todo: implement share surfaces @property def share_surfaces(self): """ Raises not implemented error """ raise NotImplementedError @property def swr(self) -> Union[None, float]: """ Get surface short wave reflectance :return: None or float """ return self._swr @swr.setter def swr(self, value): """ Set surface short wave reflectance :param value: float """ if value is not None: self._swr = float(value) def _max_coord(self, axis): if axis == 'x': axis = 0 elif axis == 'y': axis = 1 else: axis = 2 max_coordinate = '' for point in self.perimeter_polygon.coordinates: if max_coordinate == '': max_coordinate = point[axis] elif max_coordinate < point[axis]: max_coordinate = point[axis] return max_coordinate def _min_coord(self, axis): if axis == 'x': axis = 0 elif axis == 'y': axis = 1 else: axis = 2 min_coordinate = '' for point in self.perimeter_polygon.coordinates: if min_coordinate == '': min_coordinate = point[axis] elif min_coordinate > point[axis]: min_coordinate = point[axis] return min_coordinate @property def lower_corner(self): """ Get surface's lower corner [x, y, z] :return: [float] """ if self._lower_corner is None: self._lower_corner = [self._min_coord('x'), self._min_coord('y'), self._min_coord('z')] return self._lower_corner @property def upper_corner(self): """ Get surface's upper corner [x, y, z] :return: [float] """ if self._upper_corner is None: self._upper_corner = [self._max_coord('x'), self._max_coord('y'), self._max_coord('z')] return self._upper_corner @property def area_above_ground(self): """ Get surface area above ground in square meters :return: float """ if self._area_above_ground is None: self._area_above_ground = self.perimeter_polygon.area - self.area_below_ground return self._area_above_ground # todo: to be implemented when adding terrains @property def area_below_ground(self): """ Get surface area below ground in square meters :return: float """ return 0.0 @property def azimuth(self): """ Get surface azimuth in radians :return: float """ if self._azimuth is None: normal = self.perimeter_polygon.normal self._azimuth = np.arctan2(normal[1], normal[0]) return self._azimuth @property def inclination(self): """ Get surface inclination in radians :return: float """ if self._inclination is None: self._inclination = np.arccos(self.perimeter_polygon.normal[2]) return self._inclination @property def type(self): """ Get surface type Ground, Wall or Roof :return: str """ # todo: there are more types: internal wall, internal floor... this method must be redefined if self._type is None: grad = np.rad2deg(self.inclination) if grad >= 170: self._type = 'Ground' elif 80 <= grad <= 100: self._type = 'Wall' else: self._type = 'Roof' return self._type @property def global_irradiance(self) -> dict: """ Get global irradiance on surface in Wh/m2 :return: dict{DataFrame(float)} """ return self._global_irradiance @global_irradiance.setter def global_irradiance(self, value): """ Set global irradiance on surface in Wh/m2 :param value: dict{DataFrame(float)} """ self._global_irradiance = value @property def perimeter_polygon(self) -> Polygon: """ Get a polygon surface defined by the perimeter, merging solid and holes :return: Polygon """ return self._perimeter_polygon @property def solid_polygon(self) -> Polygon: """ Get the solid surface :return: Polygon """ return self._solid_polygon @solid_polygon.setter def solid_polygon(self, value): """ Set the solid surface :return: Polygon """ self._solid_polygon = value @property def holes_polygons(self) -> Union[List[Polygon], None]: """ Get hole surfaces, a list of hole polygons found in the surface :return: None, [] or [Polygon] None -> not known whether holes exist in reality or not due to low level of detail of input data [] -> no holes in the surface [Polygon] -> one or more holes in the surface """ return self._holes_polygons @holes_polygons.setter def holes_polygons(self, value): """ Set the hole surfaces :param value: [Polygon] """ self._holes_polygons = value @property def inverse(self) -> Surface: """ Get the inverse surface (the same surface pointing backwards) :return: Surface """ if self._inverse is None: new_solid_polygon = Polygon(self.solid_polygon.inverse) new_perimeter_polygon = Polygon(self.perimeter_polygon.inverse) new_holes_polygons = [] if self.holes_polygons is not None: for hole in self.holes_polygons: new_holes_polygons.append(Polygon(hole.inverse)) else: new_holes_polygons = None self._inverse = Surface(new_solid_polygon, new_perimeter_polygon, new_holes_polygons, cte.VIRTUAL_INTERNAL) return self._inverse def shared_surfaces(self): """ Raises not implemented error """ # todo: check https://trimsh.org/trimesh.collision.html as an option to implement this method raise NotImplementedError def divide(self, z): """ Divides a surface at Z plane :return: Surface, Surface, Any """ # todo: check return types # todo: recheck this method for LoD3 (windows) origin = Point([0, 0, z]) normal = np.array([0, 0, 1]) plane = Plane(normal=normal, origin=origin) polygon = self.perimeter_polygon part_1, part_2, intersection = polygon.divide(plane) surface_child = Surface(part_1, part_1, name=self.name, surface_type=self.type) rest_surface = Surface(part_2, part_2, name=self.name, surface_type=self.type) return surface_child, rest_surface, intersection