2020-10-28 13:42:58 -04:00
|
|
|
"""
|
|
|
|
Surface module
|
|
|
|
SPDX - License - Identifier: LGPL - 3.0 - or -later
|
|
|
|
Copyright © 2020 Project Author Guille Gutierrez guillermo.gutierrezmorote@concordia.ca
|
2021-03-03 16:33:33 -05:00
|
|
|
contributors Pilar Monsalvete Álvarez de Uribarri pilar.monsalvete@concordia.ca
|
2020-10-28 13:42:58 -04:00
|
|
|
"""
|
|
|
|
|
2021-03-03 16:33:33 -05:00
|
|
|
from __future__ import annotations
|
2020-10-28 13:42:58 -04:00
|
|
|
import numpy as np
|
|
|
|
|
2021-03-02 18:57:09 -05:00
|
|
|
from helpers.geometry_helper import GeometryHelper as gh
|
|
|
|
from city_model_structure.attributes.polygon import Polygon
|
2020-10-28 13:42:58 -04:00
|
|
|
|
|
|
|
|
|
|
|
class Surface:
|
|
|
|
"""
|
|
|
|
Surface class
|
|
|
|
"""
|
2021-03-15 11:47:30 -04:00
|
|
|
def __init__(self, coordinates, holes_coordinates=None, surface_type=None, name=None, swr=None):
|
2020-10-28 13:42:58 -04:00
|
|
|
self._coordinates = coordinates
|
2021-03-03 16:33:33 -05:00
|
|
|
self._holes_coordinates = holes_coordinates
|
2020-10-28 13:42:58 -04:00
|
|
|
self._type = surface_type
|
|
|
|
self._name = name
|
|
|
|
self._swr = swr
|
|
|
|
self._points = None
|
|
|
|
self._points_list = None
|
2021-03-03 16:33:33 -05:00
|
|
|
self._holes_points = None
|
|
|
|
self._holes_points_list = None
|
2021-03-08 12:56:19 -05:00
|
|
|
self._perimeter_points = None
|
2021-03-08 18:27:14 -05:00
|
|
|
self._perimeter_points_list = None
|
2020-10-28 13:42:58 -04:00
|
|
|
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 = []
|
2020-10-30 16:01:12 -04:00
|
|
|
self._global_irradiance = dict()
|
2021-03-08 18:27:14 -05:00
|
|
|
self._perimeter_polygon = None
|
|
|
|
self._hole_polygons = None
|
|
|
|
self._solid_polygons = None
|
2020-10-28 13:42:58 -04:00
|
|
|
|
|
|
|
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:
|
|
|
|
"""
|
2021-03-08 12:56:19 -05:00
|
|
|
Solid surface point matrix [[x, y, z],[x, y, z],...]
|
2020-10-28 13:42:58 -04:00
|
|
|
:return: np.ndarray
|
|
|
|
"""
|
|
|
|
if self._points is None:
|
|
|
|
self._points = np.fromstring(self._coordinates, dtype=float, sep=' ')
|
2021-03-08 12:56:19 -05:00
|
|
|
self._points = gh.to_points_matrix(self._points)
|
2020-10-28 13:42:58 -04:00
|
|
|
return self._points
|
|
|
|
|
2021-03-03 16:33:33 -05:00
|
|
|
@property
|
|
|
|
def holes_points(self) -> [np.ndarray]:
|
|
|
|
"""
|
2021-03-08 12:56:19 -05:00
|
|
|
Holes surfaces point matrices [[[x, y, z],[x, y, z],...]]
|
2021-03-03 16:33:33 -05:00
|
|
|
: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=' ')
|
2021-03-08 12:56:19 -05:00
|
|
|
hole_points = gh.to_points_matrix(hole_points)
|
2021-03-03 16:33:33 -05:00
|
|
|
self._holes_points.append(hole_points)
|
|
|
|
return self._holes_points
|
|
|
|
|
2021-03-08 18:27:14 -05:00
|
|
|
@property
|
|
|
|
def perimeter_points(self) -> np.ndarray:
|
|
|
|
"""
|
|
|
|
Matrix of points of the perimeter in the same order as in coordinates [[x, y, z],[x, y, z],...]
|
|
|
|
:return: np.ndarray
|
|
|
|
"""
|
|
|
|
if self._perimeter_points is None:
|
|
|
|
if self.holes_points is None:
|
|
|
|
self._perimeter_points = self.points
|
|
|
|
else:
|
|
|
|
_perimeter_coordinates = self._coordinates
|
|
|
|
for hole_points in self.holes_points:
|
|
|
|
_hole = np.append(hole_points, hole_points[0])
|
|
|
|
_closed_hole = ' '.join(str(e) for e in [*_hole[:]])
|
|
|
|
# add a mark 'M' to ensure that the recombination of points does not provoke errors in finding holes
|
|
|
|
_perimeter_coordinates = _perimeter_coordinates.replace(_closed_hole, 'M')
|
|
|
|
_perimeter_coordinates = _perimeter_coordinates.replace('M', '')
|
|
|
|
self._perimeter_points = np.fromstring(_perimeter_coordinates, dtype=float, sep=' ')
|
|
|
|
self._perimeter_points = gh.to_points_matrix(self._perimeter_points)
|
|
|
|
# remove duplicated points
|
|
|
|
pv = np.array([self._perimeter_points[0]])
|
|
|
|
for point in self._perimeter_points:
|
|
|
|
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_points = pv
|
|
|
|
return self._perimeter_points
|
|
|
|
|
2021-03-03 16:33:33 -05:00
|
|
|
@property
|
|
|
|
def points_list(self) -> np.ndarray:
|
|
|
|
"""
|
2021-03-08 12:56:19 -05:00
|
|
|
Solid surface point coordinates list [x, y, z, x, y, z,...]
|
2021-03-03 16:33:33 -05:00
|
|
|
: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:
|
|
|
|
"""
|
2021-03-08 12:56:19 -05:00
|
|
|
Holes surfaces point coordinates list [x, y, z, x, y, z,...]
|
2021-03-03 16:33:33 -05:00
|
|
|
: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
|
|
|
|
|
2021-03-08 18:27:14 -05:00
|
|
|
@property
|
|
|
|
def perimeter_points_list(self) -> np.ndarray:
|
|
|
|
"""
|
|
|
|
Solid surface point coordinates list [x, y, z, x, y, z,...]
|
|
|
|
:return: np.ndarray
|
|
|
|
"""
|
|
|
|
if self._perimeter_points_list is None:
|
|
|
|
s = self.perimeter_points
|
|
|
|
self._perimeter_points_list = np.reshape(s, len(s) * 3)
|
|
|
|
return self._perimeter_points_list
|
|
|
|
|
2020-10-28 13:42:58 -04:00
|
|
|
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:
|
2021-03-08 18:27:14 -05:00
|
|
|
self._area_above_ground = self.perimeter_polygon.area - self.area_below_ground
|
2020-10-28 13:42:58 -04:00
|
|
|
return self._area_above_ground
|
|
|
|
|
2021-03-03 16:33:33 -05:00
|
|
|
# todo: to be implemented
|
2020-10-28 13:42:58 -04:00
|
|
|
@property
|
|
|
|
def area_below_ground(self):
|
|
|
|
"""
|
|
|
|
Surface area below ground in square meters
|
|
|
|
:return: float
|
|
|
|
"""
|
2021-03-03 16:33:33 -05:00
|
|
|
return 0.0
|
2020-10-28 13:42:58 -04:00
|
|
|
|
|
|
|
@property
|
|
|
|
def azimuth(self):
|
|
|
|
"""
|
|
|
|
Surface azimuth in radians
|
|
|
|
:return: float
|
|
|
|
"""
|
|
|
|
if self._azimuth is None:
|
2021-03-08 18:27:14 -05:00
|
|
|
normal = self.perimeter_polygon.normal
|
2020-10-28 13:42:58 -04:00
|
|
|
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:
|
2021-03-08 18:27:14 -05:00
|
|
|
self._inclination = np.arccos(self.perimeter_polygon.normal[2])
|
2020-10-28 13:42:58 -04:00
|
|
|
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:
|
|
|
|
"""
|
2021-03-08 18:27:14 -05:00
|
|
|
percent = intersection_area / self.perimeter_polygon.area
|
2020-10-28 13:42:58 -04:00
|
|
|
self._shared_surfaces.append((percent, surface))
|
|
|
|
|
2021-03-03 16:33:33 -05:00
|
|
|
# todo reimplement
|
2020-10-28 13:42:58 -04:00
|
|
|
def shared(self, surface):
|
|
|
|
"""
|
|
|
|
Check if given surface share some area with this surface
|
|
|
|
:param surface: Surface
|
|
|
|
:return: None
|
|
|
|
"""
|
2021-03-03 16:33:33 -05:00
|
|
|
# intersection_area = 0
|
|
|
|
# surface.add_shared(self, intersection_area)
|
|
|
|
raise NotImplementedError
|
2020-10-28 13:42:58 -04:00
|
|
|
|
|
|
|
@property
|
2020-10-30 16:01:12 -04:00
|
|
|
def global_irradiance(self) -> dict:
|
2020-10-28 13:42:58 -04:00
|
|
|
"""
|
2020-10-30 16:01:12 -04:00
|
|
|
global irradiance on surface in Wh/m2
|
|
|
|
:return: dict{DataFrame(float)}
|
2020-10-28 13:42:58 -04:00
|
|
|
"""
|
2020-10-30 16:01:12 -04:00
|
|
|
return self._global_irradiance
|
2020-10-28 13:42:58 -04:00
|
|
|
|
2020-10-30 16:01:12 -04:00
|
|
|
@global_irradiance.setter
|
|
|
|
def global_irradiance(self, value):
|
2020-10-28 13:42:58 -04:00
|
|
|
"""
|
2020-10-30 16:01:12 -04:00
|
|
|
global irradiance on surface in Wh/m2
|
|
|
|
:param value: dict{DataFrame(float)}
|
2020-10-28 13:42:58 -04:00
|
|
|
"""
|
2020-10-30 16:01:12 -04:00
|
|
|
self._global_irradiance = value
|
2020-10-28 13:42:58 -04:00
|
|
|
|
|
|
|
@property
|
2021-03-08 18:27:14 -05:00
|
|
|
def perimeter_polygon(self) -> Polygon:
|
2020-10-28 13:42:58 -04:00
|
|
|
"""
|
2021-03-03 16:33:33 -05:00
|
|
|
total surface defined by the perimeter, merging solid and holes
|
|
|
|
:return: Polygon
|
2020-10-28 13:42:58 -04:00
|
|
|
"""
|
2021-03-08 18:27:14 -05:00
|
|
|
if self._perimeter_polygon is None:
|
|
|
|
self._perimeter_polygon = Polygon(self.perimeter_points)
|
|
|
|
return self._perimeter_polygon
|
2020-10-28 13:42:58 -04:00
|
|
|
|
|
|
|
@property
|
2021-03-08 18:27:14 -05:00
|
|
|
def solid_polygon(self) -> Polygon:
|
2020-10-28 13:42:58 -04:00
|
|
|
"""
|
2021-03-03 16:33:33 -05:00
|
|
|
solid surface
|
|
|
|
:return: Polygon
|
|
|
|
"""
|
2021-03-08 18:27:14 -05:00
|
|
|
if self._solid_polygons is None:
|
|
|
|
self._solid_polygons = Polygon(self.points)
|
|
|
|
return self._solid_polygons
|
2020-12-01 07:33:23 -05:00
|
|
|
|
|
|
|
@property
|
2021-03-08 18:27:14 -05:00
|
|
|
def hole_polygons(self) -> [Polygon]:
|
2021-03-03 16:33:33 -05:00
|
|
|
"""
|
|
|
|
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
|
|
|
|
"""
|
2021-03-08 18:27:14 -05:00
|
|
|
if self._hole_polygons is None:
|
2021-03-03 16:33:33 -05:00
|
|
|
if self.holes_points is None:
|
2021-03-08 18:27:14 -05:00
|
|
|
self._hole_polygons = None
|
2021-03-03 16:33:33 -05:00
|
|
|
else:
|
2021-03-08 18:27:14 -05:00
|
|
|
self._hole_polygons = []
|
2021-03-08 12:56:19 -05:00
|
|
|
for hole_points in self.holes_points:
|
2021-03-08 18:27:14 -05:00
|
|
|
self._hole_polygons.append(Polygon(hole_points))
|
|
|
|
return self._hole_polygons
|