2020-06-09 14:07:47 -04:00
|
|
|
"""
|
|
|
|
CityObject module
|
|
|
|
SPDX - License - Identifier: LGPL - 3.0 - or -later
|
|
|
|
Copyright © 2020 Project Author Guille Gutierrez guillermo.gutierrezmorote@concordia.ca
|
|
|
|
"""
|
2020-06-09 15:14:47 -04:00
|
|
|
from typing import Union, List
|
2020-05-18 13:25:08 -04:00
|
|
|
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
|
|
|
|
from pathlib import Path
|
|
|
|
import matplotlib.patches as patches
|
|
|
|
from helpers.geometry import Geometry
|
|
|
|
from city_model_structure.usage_zone import UsageZone
|
2020-06-09 15:14:47 -04:00
|
|
|
|
2020-05-18 13:25:08 -04:00
|
|
|
|
|
|
|
|
|
|
|
class CityObject:
|
2020-06-09 15:14:47 -04:00
|
|
|
"""
|
|
|
|
CityObject class
|
|
|
|
"""
|
2020-06-03 16:56:17 -04:00
|
|
|
def __init__(self, name, lod, surfaces, terrains, year_of_construction, function, lower_corner, attic_heated=0,
|
|
|
|
basement_heated=0):
|
2020-05-18 13:25:08 -04:00
|
|
|
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
|
2020-06-03 16:56:17 -04:00
|
|
|
self._lower_corner = lower_corner
|
2020-05-18 13:25:08 -04:00
|
|
|
self._geometry = Geometry()
|
|
|
|
self._average_storey_height = None
|
|
|
|
self._storeys_above_ground = None
|
|
|
|
self._foot_print = None
|
|
|
|
self._usage_zones = []
|
2020-05-28 12:07:36 -04:00
|
|
|
# ToDo: this need to be changed when we have other city_objects beside "buildings"
|
|
|
|
self._type = 'building'
|
2020-05-18 13:25:08 -04:00
|
|
|
|
|
|
|
# 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))
|
2020-05-28 12:07:36 -04:00
|
|
|
|
2020-05-18 13:25:08 -04:00
|
|
|
for t in self._thermal_zones:
|
|
|
|
t.bounded = [ThermalBoundary(s, [t]) for s in t.surfaces]
|
|
|
|
surface_id = 0
|
2020-06-09 15:14:47 -04:00
|
|
|
for surface in self._surfaces:
|
|
|
|
surface.lower_corner = self._lower_corner
|
|
|
|
surface.parent(self, surface_id)
|
2020-05-18 13:25:08 -04:00
|
|
|
surface_id += 1
|
|
|
|
|
|
|
|
@property
|
|
|
|
def usage_zones(self) -> List[UsageZone]:
|
2020-06-09 15:14:47 -04:00
|
|
|
"""
|
|
|
|
Get city object usage zones
|
|
|
|
:return: [UsageZone]
|
|
|
|
"""
|
2020-05-18 13:25:08 -04:00
|
|
|
return self._usage_zones
|
|
|
|
|
|
|
|
@usage_zones.setter
|
2020-05-28 12:07:36 -04:00
|
|
|
def usage_zones(self, values):
|
2020-06-09 15:14:47 -04:00
|
|
|
"""
|
|
|
|
Set city objects usage zones
|
|
|
|
:param values: [UsageZones]
|
|
|
|
:return: None
|
|
|
|
"""
|
2020-05-28 12:07:36 -04:00
|
|
|
# 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]
|
2020-05-18 13:25:08 -04:00
|
|
|
|
|
|
|
@property
|
|
|
|
def terrains(self) -> List[Surface]:
|
2020-06-09 15:14:47 -04:00
|
|
|
"""
|
|
|
|
Get city object terrain surfaces
|
|
|
|
:return: [Surface]
|
|
|
|
"""
|
2020-05-18 13:25:08 -04:00
|
|
|
return self._terrains
|
|
|
|
|
|
|
|
@property
|
|
|
|
def attic_heated(self):
|
2020-06-09 15:14:47 -04:00
|
|
|
"""
|
|
|
|
Get if the city object attic is heated
|
|
|
|
:return: Boolean
|
|
|
|
"""
|
2020-05-18 13:25:08 -04:00
|
|
|
return self._attic_heated
|
|
|
|
|
|
|
|
@attic_heated.setter
|
|
|
|
def attic_heated(self, value):
|
2020-06-09 15:14:47 -04:00
|
|
|
"""
|
|
|
|
Set if the city object attic is heated
|
|
|
|
:param value: Boolean
|
|
|
|
:return: None
|
|
|
|
"""
|
2020-05-18 13:25:08 -04:00
|
|
|
self._attic_heated = value
|
|
|
|
|
|
|
|
@property
|
|
|
|
def basement_heated(self):
|
2020-06-09 15:14:47 -04:00
|
|
|
"""
|
|
|
|
Get if the city object basement is heated
|
|
|
|
:return: Boolean
|
|
|
|
"""
|
2020-05-18 13:25:08 -04:00
|
|
|
return self._basement_heated
|
|
|
|
|
|
|
|
@basement_heated.setter
|
|
|
|
def basement_heated(self, value):
|
2020-06-09 15:14:47 -04:00
|
|
|
"""
|
|
|
|
Set if the city object basement is heated
|
|
|
|
:param value: Boolean
|
|
|
|
:return: None
|
|
|
|
"""
|
2020-05-18 13:25:08 -04:00
|
|
|
self._attic_heated = value
|
|
|
|
|
|
|
|
@property
|
|
|
|
def name(self):
|
2020-06-09 15:14:47 -04:00
|
|
|
"""
|
|
|
|
City object name
|
|
|
|
:return: str
|
|
|
|
"""
|
2020-05-18 13:25:08 -04:00
|
|
|
return self._name
|
|
|
|
|
|
|
|
@property
|
|
|
|
def lod(self):
|
2020-06-09 15:14:47 -04:00
|
|
|
"""
|
|
|
|
City object level of detail 1, 2, 3 or 4
|
|
|
|
:return: int
|
|
|
|
"""
|
2020-05-18 13:25:08 -04:00
|
|
|
return self._lod
|
|
|
|
|
|
|
|
@property
|
|
|
|
def surfaces(self) -> List[Surface]:
|
2020-06-09 15:14:47 -04:00
|
|
|
"""
|
|
|
|
City object surfaces
|
|
|
|
:return: [Surface]
|
|
|
|
"""
|
2020-05-18 13:25:08 -04:00
|
|
|
return self._surfaces
|
|
|
|
|
|
|
|
def surface(self, name) -> Union[Surface, None]:
|
2020-06-09 15:14:47 -04:00
|
|
|
"""
|
|
|
|
Get the city object surface with a given name
|
|
|
|
:param name: Surface name:str
|
|
|
|
:return: None or Surface
|
|
|
|
"""
|
2020-05-18 13:25:08 -04:00
|
|
|
for s in self.surfaces:
|
|
|
|
if s.name == name:
|
|
|
|
return s
|
|
|
|
return None
|
|
|
|
|
|
|
|
@property
|
|
|
|
def thermal_zones(self) -> List[ThermalZone]:
|
2020-06-09 15:14:47 -04:00
|
|
|
"""
|
|
|
|
City object thermal zones
|
|
|
|
:return: [ThermalZone]
|
|
|
|
"""
|
2020-05-18 13:25:08 -04:00
|
|
|
return self._thermal_zones
|
|
|
|
|
|
|
|
@property
|
|
|
|
def volume(self):
|
2020-06-09 15:14:47 -04:00
|
|
|
"""
|
|
|
|
City object volume in cubic meters
|
|
|
|
:return: float
|
|
|
|
"""
|
2020-05-18 13:25:08 -04:00
|
|
|
if self._polyhedron is None:
|
|
|
|
self._polyhedron = Polyhedron(self.surfaces)
|
|
|
|
return self._polyhedron.volume
|
|
|
|
|
|
|
|
@property
|
|
|
|
def heated_volume(self):
|
2020-06-09 15:14:47 -04:00
|
|
|
"""
|
|
|
|
City object heated volume in cubic meters
|
|
|
|
:return: float
|
|
|
|
"""
|
2020-05-18 13:25:08 -04:00
|
|
|
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):
|
2020-06-09 15:14:47 -04:00
|
|
|
"""
|
|
|
|
Export the city object to stl file (city_object_name.stl)
|
|
|
|
:param path: path to export
|
|
|
|
:return: None
|
|
|
|
"""
|
2020-05-18 13:25:08 -04:00
|
|
|
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):
|
2020-06-09 15:14:47 -04:00
|
|
|
"""
|
|
|
|
City object year of construction
|
|
|
|
:return: int
|
|
|
|
"""
|
2020-05-18 13:25:08 -04:00
|
|
|
return self._year_of_construction
|
|
|
|
|
|
|
|
@property
|
|
|
|
def function(self):
|
2020-06-09 15:14:47 -04:00
|
|
|
"""
|
|
|
|
City object function
|
|
|
|
:return: str
|
|
|
|
"""
|
2020-05-18 13:25:08 -04:00
|
|
|
return self._function
|
|
|
|
|
|
|
|
@property
|
|
|
|
def average_storey_height(self):
|
2020-06-09 15:14:47 -04:00
|
|
|
"""
|
|
|
|
Get city object average storey height in meters
|
|
|
|
:return: float
|
|
|
|
"""
|
2020-05-18 13:25:08 -04:00
|
|
|
return self._average_storey_height
|
|
|
|
|
|
|
|
@average_storey_height.setter
|
|
|
|
def average_storey_height(self, value):
|
2020-06-09 15:14:47 -04:00
|
|
|
"""
|
|
|
|
Set city object average storey height in meters
|
|
|
|
:param value: average storey height in meters:float
|
|
|
|
:return: None
|
|
|
|
"""
|
2020-05-18 13:25:08 -04:00
|
|
|
self._average_storey_height = value
|
|
|
|
|
|
|
|
@property
|
|
|
|
def storeys_above_ground(self):
|
|
|
|
return self._storeys_above_ground
|
|
|
|
|
|
|
|
@storeys_above_ground.setter
|
|
|
|
def storeys_above_ground(self, value):
|
|
|
|
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:
|
2020-06-09 15:14:47 -04:00
|
|
|
point_1 = CityObject._tuple_to_point(point)
|
|
|
|
point_2 = CityObject._tuple_to_point(point_tuple)
|
|
|
|
if self._geometry.almost_equal(point_1, point_2):
|
2020-05-18 13:25:08 -04:00
|
|
|
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:
|
|
|
|
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 type(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
|
|
|
|
|
2020-05-28 12:07:36 -04:00
|
|
|
@property
|
|
|
|
def type(self):
|
|
|
|
return self._type
|
|
|
|
|
2020-05-18 13:25:08 -04:00
|
|
|
@property
|
|
|
|
def max_height(self):
|
|
|
|
if self._polyhedron is None:
|
|
|
|
self._polyhedron = Polyhedron(self.surfaces)
|
|
|
|
return self._polyhedron.max_z
|