hub/city_model_structure/attributes/surface.py

397 lines
10 KiB
Python

"""
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
import uuid
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=None):
self._coordinates = coordinates
self._holes_coordinates = holes_coordinates
self._type = surface_type
self._name = name
self._swr = swr
self._points = None
self._points_list = None
self._holes_points = None
self._holes_points_list = None
self._perimeter_points = None
self._perimeter_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._max_x = None
self._max_y = None
self._max_z = None
self._shared_surfaces = []
self._global_irradiance = dict()
self._perimeter_polygon = None
self._hole_polygons = None
self._solid_polygons = 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:
self._name = uuid.uuid4()
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:
"""
Solid surface point matrix [[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)
return self._points
@property
def holes_points(self) -> [np.ndarray]:
"""
Holes surfaces point matrices [[[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._holes_points.append(hole_points)
return self._holes_points
@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
@property
def points_list(self) -> np.ndarray:
"""
Solid surface point coordinates list [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:
"""
Holes surfaces point coordinates list [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
@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
def _max_coord(self, axis):
if axis == 'x':
axis = 0
elif axis == 'y':
axis = 1
else:
axis = 2
max_coordinate = ''
for point in self.points:
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.points:
if min_coordinate == '':
min_coordinate = point[axis]
elif min_coordinate > point[axis]:
min_coordinate = point[axis]
return min_coordinate
@property
def max_x(self):
"""
Surface maximal x value
:return: float
"""
if self._max_x is None:
self._max_x = self._max_coord('x')
return self._max_x
@property
def max_y(self):
"""
Surface maximal y value
:return: float
"""
if self._max_y is None:
self._max_y = self._max_coord('y')
return self._max_y
@property
def max_z(self):
"""
Surface maximal z value
:return: float
"""
if self._max_z is None:
self._max_z = self._max_coord('z')
return self._max_z
@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_polygon.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_polygon.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_polygon.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_polygon.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_polygon(self) -> Polygon:
"""
total surface defined by the perimeter, merging solid and holes
:return: Polygon
"""
if self._perimeter_polygon is None:
self._perimeter_polygon = Polygon(self.perimeter_points)
return self._perimeter_polygon
@property
def solid_polygon(self) -> Polygon:
"""
solid surface
:return: Polygon
"""
if self._solid_polygons is None:
self._solid_polygons = Polygon(self.points)
return self._solid_polygons
@property
def hole_polygons(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_polygons is None:
if self.holes_points is None:
self._hole_polygons = None
else:
self._hole_polygons = []
for hole_points in self.holes_points:
self._hole_polygons.append(Polygon(hole_points))
return self._hole_polygons