diff --git a/city_model_structure/building.py b/city_model_structure/building.py new file mode 100644 index 00000000..830f370e --- /dev/null +++ b/city_model_structure/building.py @@ -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 diff --git a/city_model_structure/city.py b/city_model_structure/city.py index 1fe3a69c..f12cc89f 100644 --- a/city_model_structure/city.py +++ b/city_model_structure/city.py @@ -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): diff --git a/city_model_structure/city_object.py b/city_model_structure/city_object.py index 3e2a599e..7a368d16 100644 --- a/city_model_structure/city_object.py +++ b/city_model_structure/city_object.py @@ -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 diff --git a/city_model_structure/thermal_opening.py b/city_model_structure/thermal_opening.py index f5ab59a9..3ed23816 100644 --- a/city_model_structure/thermal_opening.py +++ b/city_model_structure/thermal_opening.py @@ -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): diff --git a/geometry/geometry_feeders/city_gml.py b/geometry/geometry_feeders/city_gml.py index 1fa2b6fe..b801c965 100644 --- a/geometry/geometry_feeders/city_gml.py +++ b/geometry/geometry_feeders/city_gml.py @@ -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._lower_corner)) + self._city.add_city_object(Building(name, lod, surfaces, terrains, year_of_construction, function, + self._lower_corner)) return self._city diff --git a/physics/physics_feeders/us_new_york_city_physics_parameters.py b/physics/physics_feeders/us_new_york_city_physics_parameters.py index 6e8816df..ed87f819 100644 --- a/physics/physics_feeders/us_new_york_city_physics_parameters.py +++ b/physics/physics_feeders/us_new_york_city_physics_parameters.py @@ -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) diff --git a/physics/physics_feeders/us_physics_parameters.py b/physics/physics_feeders/us_physics_parameters.py index 09d15b1a..3630eb30 100644 --- a/physics/physics_feeders/us_physics_parameters.py +++ b/physics/physics_feeders/us_physics_parameters.py @@ -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) diff --git a/tests/test_geometry_factory.py b/tests/test_geometry_factory.py index 3a2049ed..3af9134e 100644 --- a/tests/test_geometry_factory.py +++ b/tests/test_geometry_factory.py @@ -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: diff --git a/tests/test_physics_factory.py b/tests/test_physics_factory.py index 588e7fad..4395fc10 100644 --- a/tests/test_physics_factory.py +++ b/tests/test_physics_factory.py @@ -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: diff --git a/usage/usage_feeders/us_base_usage_parameters.py b/usage/usage_feeders/us_base_usage_parameters.py index ab38bce1..0614ce46 100644 --- a/usage/usage_feeders/us_base_usage_parameters.py +++ b/usage/usage_feeders/us_base_usage_parameters.py @@ -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)