""" Surface module SPDX - License - Identifier: LGPL - 3.0 - or -later Copyright © 2020 Project Author Guille Gutierrez guillermo.gutierrezmorote@concordia.ca contributors Pilar Monsalvete Álvarez de Uribarri pilar.monsalvete@concordia.ca """ from __future__ import annotations import numpy as np from helpers.geometry_helper import GeometryHelper as gh from city_model_structure.attributes.polygon import Polygon class Surface: """ Surface class """ def __init__(self, coordinates, holes_coordinates=None, surface_type=None, name=None, swr='0.2', remove_last=True): self._coordinates = coordinates self._holes_coordinates = holes_coordinates self._type = surface_type self._name = name self._swr = swr self._remove_last = remove_last self._points = None self._points_list = None self._holes_points = None self._holes_points_list = None self._azimuth = None self._inclination = None self._area_above_ground = None self._area_below_ground = None self._parent = None self._min_x = None self._min_y = None self._min_z = None self._shared_surfaces = [] self._global_irradiance = dict() self._perimeter_surface = None self._hole_surfaces = None self._solid_surface = None self._perimeter_vertices = None def parent(self, parent, surface_id): """ Assign a city object as surface parent and a surface id :param parent: CityObject :param surface_id: str :return: None """ self._parent = parent self._name = str(surface_id) @property def name(self): """ Surface name :return: str """ if self._name is None: raise Exception('surface has no name') return self._name @property def swr(self): """ Get surface short wave reflectance :return: float """ return self._swr @swr.setter def swr(self, value): """ Set surface short wave reflectance :param value: float :return: None """ self._swr = value @property def points(self) -> np.ndarray: """ Surface point coordinates list [x, y, z, x, y, z,...] :return: np.ndarray """ if self._points is None: self._points = np.fromstring(self._coordinates, dtype=float, sep=' ') self._points = gh.to_points_matrix(self._points, self._remove_last) return self._points @property def holes_points(self) -> [np.ndarray]: """ Surface point coordinates list [x, y, z, x, y, z,...] :return: np.ndarray """ if self._holes_coordinates is not None: self._holes_points = [] for hole_coordinates in self._holes_coordinates: hole_points = np.fromstring(hole_coordinates, dtype=float, sep=' ') hole_points = gh.to_points_matrix(hole_points, self._remove_last) self._holes_points.append(hole_points) return self._holes_points @property def points_list(self) -> np.ndarray: """ Surface point matrix [[x, y, z],[x, y, z],...] :return: np.ndarray """ if self._points_list is None: s = self.points self._points_list = np.reshape(s, len(s) * 3) return self._points_list @property def holes_points_list(self) -> np.ndarray: """ Surface point matrix [[x, y, z],[x, y, z],...] :return: np.ndarray """ if self._holes_coordinates is not None: self._holes_points_list = np.array([]) for hole_points in self.holes_points: s = hole_points hole_points_list = np.reshape(s, len(s) * 3) np.add(self._holes_points_list, hole_points_list) return self._holes_points_list def _min_coord(self, axis): if axis == 'x': axis = 0 elif axis == 'y': axis = 1 else: axis = 2 min_coordinate = '' for point in self.points: if min_coordinate == '': min_coordinate = point[axis] elif min_coordinate > point[axis]: min_coordinate = point[axis] return min_coordinate @property def min_x(self): """ Surface minimal x value :return: float """ if self._min_x is None: self._min_x = self._min_coord('x') return self._min_x @property def min_y(self): """ Surface minimal y value :return: float """ if self._min_y is None: self._min_y = self._min_coord('y') return self._min_y @property def min_z(self): """ Surface minimal z value :return: float """ if self._min_z is None: self._min_z = self._min_coord('z') return self._min_z @property def area_above_ground(self): """ Surface area above ground in square meters :return: float """ if self._area_above_ground is None: self._area_above_ground = self.perimeter_surface.area - self.area_below_ground return self._area_above_ground # todo: to be implemented @property def area_below_ground(self): """ Surface area below ground in square meters :return: float """ return 0.0 @property def azimuth(self): """ Surface azimuth in radians :return: float """ if self._azimuth is None: normal = self.perimeter_surface.normal self._azimuth = np.arctan2(normal[1], normal[0]) return self._azimuth @property def inclination(self): """ Surface inclination in radians :return: float """ if self._inclination is None: self._inclination = np.arccos(self.perimeter_surface.normal[2]) return self._inclination @property def type(self): """ Surface type Ground, Wall or Roof :return: str """ 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 def add_shared(self, surface, intersection_area): """ Add a given surface and shared area in percent to this surface. :param surface: :param intersection_area: :return: """ percent = intersection_area / self.perimeter_surface.area self._shared_surfaces.append((percent, surface)) # todo reimplement def shared(self, surface): """ Check if given surface share some area with this surface :param surface: Surface :return: None """ # intersection_area = 0 # surface.add_shared(self, intersection_area) raise NotImplementedError @property def global_irradiance(self) -> dict: """ global irradiance on surface in Wh/m2 :return: dict{DataFrame(float)} """ return self._global_irradiance @global_irradiance.setter def global_irradiance(self, value): """ global irradiance on surface in Wh/m2 :param value: dict{DataFrame(float)} """ self._global_irradiance = value @property def perimeter_surface(self) -> Polygon: """ total surface defined by the perimeter, merging solid and holes :return: Polygon """ if self._perimeter_surface is None: self._perimeter_surface = Polygon(self.perimeter_vertices) return self._perimeter_surface @property def solid_surface(self) -> Polygon: """ solid surface :return: Polygon """ if self._solid_surface is None: self._solid_surface = Polygon(self.points) return self._solid_surface @property def hole_surfaces(self) -> [Polygon]: """ 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 """ if self._hole_surfaces is None: if self.holes_points is None: self._hole_surfaces = None else: self._hole_surfaces = [] for hole_vertices in self.holes_points: self._hole_surfaces.append(Polygon(hole_vertices)) return self._hole_surfaces @property def perimeter_vertices(self) -> np.ndarray: """ vertices of the perimeter organized in the same order as in coordinates :return: """ if self._perimeter_vertices is None: if self.holes_points is None: self._perimeter_vertices = self.points else: first_point = True for point in self.points: point_of_hole = False for hole_points in self.holes_points: for hole_point in hole_points: if gh().almost_equal(0.0, point, hole_point): point_of_hole = True if not point_of_hole: if first_point: self._perimeter_vertices = np.array([point]) first_point = False else: self._perimeter_vertices = np.append(self._perimeter_vertices, [point], axis=0) # remove duplicated points pv = np.array([self._perimeter_vertices[0]]) for point in self._perimeter_vertices: duplicated_point = False for p in pv: if gh().almost_equal(0.0, p, point): duplicated_point = True if not duplicated_point: pv = np.append(pv, [point], axis=0) self._perimeter_vertices = pv if not self._remove_last: self._perimeter_vertices = np.append(self._perimeter_vertices, [self._perimeter_vertices[0]], axis=0) return self._perimeter_vertices