Add the building/city_objects hierarchy to the city

This commit is contained in:
Guille Gutierrez 2020-06-16 15:12:18 -04:00
parent 16e33b3772
commit 4a58387f23
10 changed files with 381 additions and 341 deletions

View File

@ -0,0 +1,317 @@
"""
CityObject module
SPDX - License - Identifier: LGPL - 3.0 - or -later
Copyright © 2020 Project Author Guille Gutierrez guillermo.gutierrezmorote@concordia.ca
"""
from pathlib import Path
from typing import Union, List
import matplotlib.patches as patches
import numpy as np
from matplotlib import pylab
from shapely import ops
from shapely.geometry import MultiPolygon
from city_model_structure.polyhedron import Polyhedron
from city_model_structure.surface import Surface
from city_model_structure.thermal_boundary import ThermalBoundary
from city_model_structure.thermal_zone import ThermalZone
from city_model_structure.usage_zone import UsageZone
from city_model_structure.city_object import CityObject
class Building(CityObject):
"""
CityObject class
"""
def __init__(self, name, lod, surfaces, terrains, year_of_construction, function, lower_corner, attic_heated=0,
basement_heated=0):
super().__init__(lod)
self._name = name
self._surfaces = surfaces
self._basement_heated = basement_heated
self._attic_heated = attic_heated
self._terrains = terrains
self._year_of_construction = year_of_construction
self._function = function
self._lower_corner = lower_corner
self._average_storey_height = None
self._storeys_above_ground = None
self._foot_print = None
self._usage_zones = []
self._type = 'building'
# 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))
for t_zones in self._thermal_zones:
t_zones.bounded = [ThermalBoundary(s, [t_zones]) for s in t_zones.surfaces]
surface_id = 0
for surface in self._surfaces:
surface.lower_corner = self._lower_corner
surface.parent(self, surface_id)
surface_id += 1
@property
def usage_zones(self) -> List[UsageZone]:
"""
Get city object usage zones
:return: [UsageZone]
"""
return self._usage_zones
@usage_zones.setter
def usage_zones(self, values):
"""
Set city objects usage zones
:param values: [UsageZones]
:return: None
"""
# 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]
@property
def terrains(self) -> List[Surface]:
"""
Get city object terrain surfaces
:return: [Surface]
"""
return self._terrains
@property
def attic_heated(self):
"""
Get if the city object attic is heated
:return: Boolean
"""
return self._attic_heated
@attic_heated.setter
def attic_heated(self, value):
"""
Set if the city object attic is heated
:param value: Boolean
:return: None
"""
self._attic_heated = value
@property
def basement_heated(self):
"""
Get if the city object basement is heated
:return: Boolean
"""
return self._basement_heated
@basement_heated.setter
def basement_heated(self, value):
"""
Set if the city object basement is heated
:param value: Boolean
:return: None
"""
self._attic_heated = value
@property
def name(self):
"""
City object name
:return: str
"""
return self._name
@property
def lod(self):
"""
City object level of detail 1, 2, 3 or 4
:return: int
"""
return self._lod
@property
def surfaces(self) -> List[Surface]:
"""
City object surfaces
:return: [Surface]
"""
return self._surfaces
def surface(self, name) -> Union[Surface, None]:
"""
Get the city object surface with a given name
:param name: str
:return: None or Surface
"""
for s in self.surfaces:
if s.name == name:
return s
return None
@property
def thermal_zones(self) -> List[ThermalZone]:
"""
City object thermal zones
:return: [ThermalZone]
"""
return self._thermal_zones
@property
def volume(self):
"""
City object volume in cubic meters
:return: float
"""
if self._polyhedron is None:
self._polyhedron = Polyhedron(self.surfaces)
return self._polyhedron.volume
@property
def heated_volume(self):
"""
City object heated volume in cubic meters
:return: float
"""
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):
"""
Export the city object to stl file (city_object_name.stl) to the given path
:param path: str
:return: None
"""
# todo: this is a method just for debugging, it will be soon removed
if self._polyhedron is None:
self._polyhedron = Polyhedron(self.surfaces)
full_path = (Path(path) / (self._name + '.stl')).resolve()
self._polyhedron.export(full_path)
@property
def year_of_construction(self):
"""
City object year of construction
:return: int
"""
return self._year_of_construction
@property
def function(self):
"""
City object function
:return: str
"""
return self._function
@property
def average_storey_height(self):
"""
Get city object average storey height in meters
:return: float
"""
return self._average_storey_height
@average_storey_height.setter
def average_storey_height(self, value):
"""
Set city object average storey height in meters
:param value: float
:return: None
"""
self._average_storey_height = value
@property
def storeys_above_ground(self):
"""
Get city object storeys number above ground
:return: int
"""
return self._storeys_above_ground
@storeys_above_ground.setter
def storeys_above_ground(self, value):
"""
Set city object storeys number above ground
:param value: int
:return:
"""
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:
point_1 = Building._tuple_to_point(point)
point_2 = Building._tuple_to_point(point_tuple)
if self._geometry.almost_equal(point_1, point_2):
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:
"""
City object foot print surface
:return: 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 isinstance(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 = Building._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
@property
def type(self):
"""
City object type
:return: str
"""
return self._type
@property
def max_height(self):
"""
City object maximal height in meters
:return: float
"""
if self._polyhedron is None:
self._polyhedron = Polyhedron(self.surfaces)
return self._polyhedron.max_z

View File

@ -10,6 +10,7 @@ import pyproj
import reverse_geocoder as rg
from pyproj import Transformer
from city_model_structure.building import Building
from city_model_structure.city_object import CityObject
@ -18,12 +19,12 @@ class City:
City class
"""
def __init__(self, lower_corner, upper_corner, srs_name, city_objects=None):
self._city_objects = None
def __init__(self, lower_corner, upper_corner, srs_name, buildings=None):
self._buildings = None
self._name = None
self._lower_corner = lower_corner
self._upper_corner = upper_corner
self._city_objects = city_objects
self._buildings = buildings
self._srs_name = srs_name
# todo: right now extracted at city level, in the future should be extracted also at building level if exist
self._location = None
@ -62,10 +63,42 @@ class City:
@property
def city_objects(self) -> Union[List[CityObject], None]:
"""
CityObjects belonging to the city
:return: None or a list of CityObjects
City objects belonging to the city
:return: None or [CityObject]
"""
return self._city_objects
return self.buildings
@property
def buildings(self) -> Union[List[Building], None]:
"""
Buildings belonging to the city
:return: None or [Building]
"""
return self._buildings
@property
def trees(self) -> NotImplementedError:
"""
Trees belonging to the city
:return: NotImplementedError
"""
raise NotImplementedError
@property
def bixi_features(self) -> NotImplementedError:
"""
Bixi features belonging to the city
:return: NotImplementedError
"""
raise NotImplementedError
@property
def composting_plants(self) -> NotImplementedError:
"""
Composting plants belonging to the city
:return: NotImplementedError
"""
raise NotImplementedError
@property
def lower_corner(self):
@ -89,7 +122,7 @@ class City:
:param name:str
:return: None or CityObject
"""
for city_object in self.city_objects:
for city_object in self.buildings:
if city_object.name == name:
return city_object
return None
@ -100,13 +133,15 @@ class City:
:param new_city_object:CityObject
:return: None
"""
if self._city_objects is None:
self._city_objects = []
for city_object in self.city_objects:
for surface in city_object.surfaces:
if new_city_object.type != 'building':
raise NotImplementedError(new_city_object.type)
if self._buildings is None:
self._buildings = []
for building in self.buildings:
for surface in building.surfaces:
for surface2 in new_city_object.surfaces:
surface.shared(surface2)
self._city_objects.append(new_city_object)
self._buildings.append(new_city_object)
@property
def srs_name(self):

View File

@ -1,320 +1,8 @@
"""
CityObject module
SPDX - License - Identifier: LGPL - 3.0 - or -later
Copyright © 2020 Project Author Guille Gutierrez guillermo.gutierrezmorote@concordia.ca
"""
from pathlib import Path
from typing import Union, List
import matplotlib.patches as patches
import numpy as np
from matplotlib import pylab
from shapely import ops
from shapely.geometry import MultiPolygon
from city_model_structure.polyhedron import Polyhedron
from city_model_structure.surface import Surface
from city_model_structure.thermal_boundary import ThermalBoundary
from city_model_structure.thermal_zone import ThermalZone
from city_model_structure.usage_zone import UsageZone
from helpers.geometry_helper import GeometryHelper
class CityObject:
"""
CityObject class
"""
def __init__(self, name, lod, surfaces, terrains, year_of_construction, function, lower_corner, attic_heated=0,
basement_heated=0):
self._name = name
def __init__(self, lod):
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
self._lower_corner = lower_corner
self._geometry = GeometryHelper()
self._average_storey_height = None
self._storeys_above_ground = None
self._foot_print = None
self._usage_zones = []
# ToDo: this need to be changed when we have other city_objects beside "buildings"
self._type = 'building'
# 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))
for t_zones in self._thermal_zones:
t_zones.bounded = [ThermalBoundary(s, [t_zones]) for s in t_zones.surfaces]
surface_id = 0
for surface in self._surfaces:
surface.lower_corner = self._lower_corner
surface.parent(self, surface_id)
surface_id += 1
@property
def usage_zones(self) -> List[UsageZone]:
"""
Get city object usage zones
:return: [UsageZone]
"""
return self._usage_zones
@usage_zones.setter
def usage_zones(self, values):
"""
Set city objects usage zones
:param values: [UsageZones]
:return: None
"""
# 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]
@property
def terrains(self) -> List[Surface]:
"""
Get city object terrain surfaces
:return: [Surface]
"""
return self._terrains
@property
def attic_heated(self):
"""
Get if the city object attic is heated
:return: Boolean
"""
return self._attic_heated
@attic_heated.setter
def attic_heated(self, value):
"""
Set if the city object attic is heated
:param value: Boolean
:return: None
"""
self._attic_heated = value
@property
def basement_heated(self):
"""
Get if the city object basement is heated
:return: Boolean
"""
return self._basement_heated
@basement_heated.setter
def basement_heated(self, value):
"""
Set if the city object basement is heated
:param value: Boolean
:return: None
"""
self._attic_heated = value
@property
def name(self):
"""
City object name
:return: str
"""
return self._name
@property
def lod(self):
"""
City object level of detail 1, 2, 3 or 4
:return: int
"""
return self._lod
@property
def surfaces(self) -> List[Surface]:
"""
City object surfaces
:return: [Surface]
"""
return self._surfaces
def surface(self, name) -> Union[Surface, None]:
"""
Get the city object surface with a given name
:param name: str
:return: None or Surface
"""
for s in self.surfaces:
if s.name == name:
return s
return None
@property
def thermal_zones(self) -> List[ThermalZone]:
"""
City object thermal zones
:return: [ThermalZone]
"""
return self._thermal_zones
@property
def volume(self):
"""
City object volume in cubic meters
:return: float
"""
if self._polyhedron is None:
self._polyhedron = Polyhedron(self.surfaces)
return self._polyhedron.volume
@property
def heated_volume(self):
"""
City object heated volume in cubic meters
:return: float
"""
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):
"""
Export the city object to stl file (city_object_name.stl) to the given path
:param path: str
:return: None
"""
# todo: this is a method just for debugging, it will be soon removed
if self._polyhedron is None:
self._polyhedron = Polyhedron(self.surfaces)
full_path = (Path(path) / (self._name + '.stl')).resolve()
self._polyhedron.export(full_path)
@property
def year_of_construction(self):
"""
City object year of construction
:return: int
"""
return self._year_of_construction
@property
def function(self):
"""
City object function
:return: str
"""
return self._function
@property
def average_storey_height(self):
"""
Get city object average storey height in meters
:return: float
"""
return self._average_storey_height
@average_storey_height.setter
def average_storey_height(self, value):
"""
Set city object average storey height in meters
:param value: float
:return: None
"""
self._average_storey_height = value
@property
def storeys_above_ground(self):
"""
Get city object storeys number above ground
:return: int
"""
return self._storeys_above_ground
@storeys_above_ground.setter
def storeys_above_ground(self, value):
"""
Set city object storeys number above ground
:param value: int
:return:
"""
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:
point_1 = CityObject._tuple_to_point(point)
point_2 = CityObject._tuple_to_point(point_tuple)
if self._geometry.almost_equal(point_1, point_2):
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:
"""
City object foot print surface
:return: 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 isinstance(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
@property
def type(self):
"""
City object type
:return: str
"""
return self._type
@property
def max_height(self):
"""
City object maximal height in meters
:return: float
"""
if self._polyhedron is None:
self._polyhedron = Polyhedron(self.surfaces)
return self._polyhedron.max_z

View File

@ -26,7 +26,7 @@ class ThermalOpening:
Get thermal opening openable ratio, NOT IMPLEMENTED
:return: Exception
"""
raise Exception('Not implemented')
raise NotImplementedError()
@openable_ratio.setter
def openable_ratio(self, value):
@ -35,7 +35,7 @@ class ThermalOpening:
:param value: Any
:return: Exception
"""
raise Exception('Not implemented')
raise NotImplementedError()
@property
def conductivity(self):

View File

@ -7,7 +7,7 @@ import numpy as np
import xmltodict
from city_model_structure.city import City
from city_model_structure.city_object import CityObject
from city_model_structure.building import Building
from city_model_structure.surface import Surface
from helpers.geometry_helper import GeometryHelper
@ -115,6 +115,6 @@ class CityGml:
year_of_construction = o['Building']['yearOfConstruction']
if 'function' in o['Building']:
function = o['Building']['function']
self._city.add_city_object(CityObject(name, lod, surfaces, terrains, year_of_construction, function,
self._city.add_city_object(Building(name, lod, surfaces, terrains, year_of_construction, function,
self._lower_corner))
return self._city

View File

@ -14,4 +14,4 @@ class UsNewYorkCityPhysicsParameters(UsBasePhysicsParameters):
def __init__(self, city, base_path):
self._city = city
climate_zone = 'ASHRAE_2004:4A'
super().__init__(climate_zone, self._city.city_objects, Pf.function, base_path)
super().__init__(climate_zone, self._city.buildings, Pf.function, base_path)

View File

@ -14,4 +14,4 @@ class UsPhysicsParameters(UsBasePhysicsParameters):
def __init__(self, city, base_path):
self._city = city
self._climate_zone = UsToLibraryTypes.city_to_climate_zone(city.name)
super().__init__(self._climate_zone, self._city.city_objects, lambda function: function, base_path)
super().__init__(self._climate_zone, self._city.buildings, lambda function: function, base_path)

View File

@ -35,8 +35,8 @@ class TestGeometryFactory(TestCase):
:return: None
"""
city = self._get_citygml()
self.assertIsNotNone(city.city_objects, 'city_objects is none')
for city_object in city.city_objects:
self.assertIsNotNone(city.buildings, 'city_objects is none')
for city_object in city.buildings:
self.assertIsNotNone(city.city_object(city_object.name), 'city_object return none')
self.assertIsNotNone(city.srs_name, 'srs_name is none')
self.assertIsNotNone(city.lower_corner, 'lower_corner is none')
@ -50,7 +50,7 @@ class TestGeometryFactory(TestCase):
:return: None
"""
city = self._get_citygml()
for city_object in city.city_objects:
for city_object in city.buildings:
self.assertIsNotNone(city_object.name, 'city_object name is none')
self.assertIsNotNone(city_object.lod, 'city_object lod is none')
self.assertIsNotNone(city_object.year_of_construction, 'city_object year_of_construction is none')
@ -78,7 +78,7 @@ class TestGeometryFactory(TestCase):
:return: None
"""
city = self._get_citygml()
for city_object in city.city_objects:
for city_object in city.buildings:
for surface in city_object.surfaces:
self.assertIsNotNone(surface.name, 'surface name is none')
self.assertIsNotNone(surface.area, 'surface area is none')
@ -109,7 +109,7 @@ class TestGeometryFactory(TestCase):
:return: None
"""
city = self._get_citygml()
for city_object in city.city_objects:
for city_object in city.buildings:
for thermal_zone in city_object.thermal_zones:
self.assertIsNotNone(thermal_zone.surfaces, 'thermal_zone surfaces is none')
self.assertIsNotNone(thermal_zone.bounded, 'thermal_zone bounded is none')
@ -135,7 +135,7 @@ class TestGeometryFactory(TestCase):
:return: None
"""
city = self._get_citygml()
for city_object in city.city_objects:
for city_object in city.buildings:
for thermal_zone in city_object.thermal_zones:
for thermal_boundary in thermal_zone.bounded:
self.assertIsNotNone(thermal_boundary.type, 'thermal_boundary type is none')
@ -165,7 +165,7 @@ class TestGeometryFactory(TestCase):
:return: None
"""
city = self._get_citygml()
for city_object in city.city_objects:
for city_object in city.buildings:
for thermal_zone in city_object.thermal_zones:
for thermal_boundary in thermal_zone.bounded:
for thermal_opening in thermal_boundary.thermal_openings:

View File

@ -42,7 +42,7 @@ class TestPhysicsFactory(TestCase):
:return: None
"""
city = self._get_city_with_physics()
for city_object in city.city_objects:
for city_object in city.buildings:
self.assertIsNotNone(city_object.average_storey_height, 'average_storey_height is none')
self.assertIsNotNone(city_object.storeys_above_ground, 'storeys_above_ground is none')
for thermal_zone in city_object.thermal_zones:

View File

@ -21,7 +21,7 @@ class UsBaseUsageParameters:
path = str(Path.cwd() / 'data/usage/de_library.xml')
with open(path) as xml:
self._library = xmltodict.parse(xml.read(), force_list='zoneUsageVariant')
for city_object in self._city.city_objects:
for city_object in self._city.buildings:
# ToDo: Right now is just one usage zone but will be multiple in the future
usage_zone = UsageZone()
usage_zone.usage = function_to_usage(city_object.function)