diff --git a/.gitignore b/.gitignore index c1c7f0df..5cb348dd 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ /data/energy_systems/heat_pumps/*.csv /data/energy_systems/heat_pumps/*.insel .DS_Store +**/__pycache__/ diff --git a/MACOS_INSTALL.md b/MACOS_INSTALL.md index 1432ac1e..caccd92e 100644 --- a/MACOS_INSTALL.md +++ b/MACOS_INSTALL.md @@ -164,7 +164,7 @@ When all the dependencies are satisfied, you are all set to start importing your Add the following code to your main.py from imports.geometry_factory import GeometryFactory -city = GeometryFactory('citygml', 'myfile.gml').city +city = GeometryFactory('citygml', path='myfile.gml').city Always remember to push your own project changes as the last thing you do before ending your working day! First, commit your changes by clicking on the green check in the top-right corner of Pycharm. Add a comment diff --git a/WINDOWS_INSTALL.md b/WINDOWS_INSTALL.md index d38e41ec..548497f5 100644 --- a/WINDOWS_INSTALL.md +++ b/WINDOWS_INSTALL.md @@ -237,7 +237,7 @@ Add the following code to your main.py from imports.geometry_factory import GeometryFactory -city = GeometryFactory('citygml', 'myfile.gml').city +city = GeometryFactory('citygml', path='myfile.gml').city ``` 9. Always remember to push your own project changes as the last thing you do before ending your working day! diff --git a/catalog_factories/greenery_catalog_factory.py b/catalog_factories/greenery_catalog_factory.py index f9284750..08864c41 100644 --- a/catalog_factories/greenery_catalog_factory.py +++ b/catalog_factories/greenery_catalog_factory.py @@ -12,7 +12,7 @@ Catalog = TypeVar('Catalog') class GreeneryCatalogFactory: """ - GeometryFactory class + GreeneryCatalogFactory class """ def __init__(self, file_type, base_path=None): if base_path is None: diff --git a/exports/formats/obj.py b/exports/formats/obj.py index bf1bee2b..f7237005 100644 --- a/exports/formats/obj.py +++ b/exports/formats/obj.py @@ -26,7 +26,7 @@ class Obj(Triangular): file_name_out = self._city.name + '_ground.' + self._triangular_format file_path_in = (Path(self._path).resolve() / file_name_in).resolve() file_path_out = (Path(self._path).resolve() / file_name_out).resolve() - scene = GeometryFactory('obj', file_path_in).scene + scene = GeometryFactory('obj', path=file_path_in).scene scene.rezero() obj_file = trimesh.exchange.obj.export_obj(scene) with open(file_path_out, 'w') as file: diff --git a/imports/geometry/gpandas.py b/imports/geometry/gpandas.py new file mode 100644 index 00000000..12fa4c95 --- /dev/null +++ b/imports/geometry/gpandas.py @@ -0,0 +1,115 @@ +""" +gpandas module parses geopandas input table and import the geometry into the city model structure +SPDX - License - Identifier: LGPL - 3.0 - or -later +Copyright © 2022 Concordia CERC group +Project Coder: Milad Aghamohamadnia --- milad.aghamohamadnia@concordia.ca +""" + +import trimesh +import trimesh.exchange.load +import trimesh.geometry +import trimesh.creation +import trimesh.repair +from shapely.geometry import Point +from shapely.geometry import Polygon as ShapelyPoly +from trimesh import Scene + +from city_model_structure.attributes.polygon import Polygon +from city_model_structure.building import Building +from city_model_structure.building_demand.surface import Surface +from city_model_structure.city import City + +import helpers.constants as cte + + +class GPandas: + """ + GeoPandas class + """ + + def __init__(self, dataframe, srs_name='EPSG:26911'): + """_summary_ + Arguments: + dataframe {Geopandas.Dataframe} -- input geometry data in geopandas table + Keyword Arguments: + srs_name {str} -- coordinate system of coordinate system (default: {'EPSG:26911'}) + """ + + self._srs_name = srs_name + self._city = None + self._scene = dataframe + self._scene = self._scene.to_crs(self._srs_name) + min_x, min_y, max_x, max_y = self._scene.total_bounds + print(min_x) + self._lower_corner = [min_x, min_y, 0] + self._upper_corner = [max_x, max_y, 0] + + @property + def scene(self) -> Scene: + """ + Get GeoPandas scene + """ + return self._scene + + @property + def city(self) -> City: + """ + Get city out of a GeoPandas Table + """ + if self._city is None: + self._city = City(self._lower_corner, self._upper_corner, self._srs_name) + for scene_index, bldg in self._scene.iterrows(): + geometry = bldg.geom + polygon = ShapelyPoly(geometry['coordinates'][0]) + height = float(bldg['height']) + building_mesh = trimesh.creation.extrude_polygon(polygon, height) + trimesh.repair.fill_holes(building_mesh) + trimesh.repair.fix_winding(building_mesh) + year_of_construction = int(bldg['year_built']) + name = str(scene_index) + lod = 1 + if year_of_construction > 2000: + function = cte.RESIDENTIAL + else: + function = cte.INDUSTRY + + surfaces = [] + for face_index, face in enumerate(building_mesh.faces): + points = [] + for vertex_index in face: + points.append(building_mesh.vertices[vertex_index]) + solid_polygon = Polygon(points) + perimeter_polygon = solid_polygon + surface = Surface(solid_polygon, perimeter_polygon) + surfaces.append(surface) + building = Building(name, lod, surfaces, year_of_construction, function, self._lower_corner, terrains=None) + self._city.add_city_object(building) + return self._city + + @staticmethod + def resize_polygon(poly, factor=0.10, expand=False) -> ShapelyPoly: + """ + returns the shapely polygon which is smaller or bigger by passed factor. + Arguments: + poly {shapely.geometry.Polygon} -- an input geometry in shapely polygon format + + Keyword Arguments: + factor {float} -- factor of expansion (default: {0.10}) + expand {bool} -- If expand = True , then it returns bigger polygon, else smaller (default: {False}) + + Returns: + {shapely.geometry.Polygon} -- output geometry in shapely polygon format + """ + xs = list(poly.exterior.coords.xy[0]) + ys = list(poly.exterior.coords.xy[1]) + x_center = 0.5 * min(xs) + 0.5 * max(xs) + y_center = 0.5 * min(ys) + 0.5 * max(ys) + min_corner = Point(min(xs), min(ys)) + center = Point(x_center, y_center) + shrink_distance = center.distance(min_corner) * factor + + if expand: + poly_resized = poly.buffer(shrink_distance) # expand + else: + poly_resized = poly.buffer(-shrink_distance) # shrink + return poly_resized diff --git a/imports/geometry_factory.py b/imports/geometry_factory.py index b1ac2f35..22b8df8b 100644 --- a/imports/geometry_factory.py +++ b/imports/geometry_factory.py @@ -10,15 +10,17 @@ from imports.geometry.citygml import CityGml from imports.geometry.obj import Obj from imports.geometry.osm_subway import OsmSubway from imports.geometry.rhino import Rhino - +from imports.geometry.gpandas import GPandas +import geopandas class GeometryFactory: """ GeometryFactory class """ - def __init__(self, file_type, path): + def __init__(self, file_type, path=None, data_frame=None): self._file_type = '_' + file_type.lower() self._path = path + self._data_frame = data_frame @property def _citygml(self) -> City: @@ -35,6 +37,16 @@ class GeometryFactory: :return: City """ return Obj(self._path).city + + @property + def _gpandas(self) -> City: + """ + Enrich the city by using GeoPandas information as data source + :return: City + """ + if self._data_frame is None: + self._data_frame = geopandas.read_file(self._path) + return GPandas(self._data_frame).city @property def _osm_subway(self) -> City: @@ -66,4 +78,4 @@ class GeometryFactory: Enrich the city given to the class using the class given handler :return: City """ - return CityGml(self._path).city + return GPandas(geopandas.read_file(self._path)).city diff --git a/requirements.txt b/requirements.txt index 5813e91f..71e5f372 100644 --- a/requirements.txt +++ b/requirements.txt @@ -15,4 +15,7 @@ ply rhino3dm==7.7.0 scipy PyYAML -pyecore==0.12.2 \ No newline at end of file +pyecore==0.12.2 +shapely +geopandas +triangle \ No newline at end of file diff --git a/unittests/test_city_merge.py b/unittests/test_city_merge.py index e1ecaac9..3e087fa0 100644 --- a/unittests/test_city_merge.py +++ b/unittests/test_city_merge.py @@ -23,7 +23,7 @@ class TestCityMerge(TestCase): def _get_citygml(self, file): file_path = (self._example_path / file).resolve() - city = GeometryFactory('citygml', file_path).city + city = GeometryFactory('citygml', path=file_path).city self.assertIsNotNone(city, 'city is none') return city diff --git a/unittests/test_construction_factory.py b/unittests/test_construction_factory.py index 6ca15a2c..e6c6dd50 100644 --- a/unittests/test_construction_factory.py +++ b/unittests/test_construction_factory.py @@ -26,7 +26,7 @@ class TestConstructionFactory(TestCase): def _get_citygml(self, file): file_path = (self._example_path / file).resolve() - self._city = GeometryFactory('citygml', file_path).city + self._city = GeometryFactory('citygml', path=file_path).city self.assertIsNotNone(self._city, 'city is none') return self._city diff --git a/unittests/test_doe_idf.py b/unittests/test_doe_idf.py index b7eddcf7..0afb40b9 100644 --- a/unittests/test_doe_idf.py +++ b/unittests/test_doe_idf.py @@ -27,7 +27,7 @@ class TestBuildings(TestCase): def test_doe_idf(self): city_file = "../unittests/tests_data/one_building_in_kelowna.gml" output_path = Path('../unittests/tests_outputs/').resolve() - city = GeometryFactory('citygml', city_file).city + city = GeometryFactory('citygml', path=city_file).city for building in city.buildings: building.year_of_construction = 2006 ConstructionFactory('nrel', city).enrich() diff --git a/unittests/test_energy_systems_air_source_hp.py b/unittests/test_energy_systems_air_source_hp.py index 6f4edad8..ef06a648 100644 --- a/unittests/test_energy_systems_air_source_hp.py +++ b/unittests/test_energy_systems_air_source_hp.py @@ -25,7 +25,7 @@ class TestEnergySystemsFactory(TestCase): """ city_file = "../unittests/tests_data/C40_Final.gml" self._output_path = "../unittests/tests_data/as_user_output.csv" - self._city = GeometryFactory('citygml', city_file).city + self._city = GeometryFactory('citygml', path=city_file).city EnergySystemsFactory('air source hp', self._city).enrich() def test_air_source_heat_pump_import(self): diff --git a/unittests/test_energy_systems_water_to_water_hp.py b/unittests/test_energy_systems_water_to_water_hp.py index 889f9127..a455834a 100644 --- a/unittests/test_energy_systems_water_to_water_hp.py +++ b/unittests/test_energy_systems_water_to_water_hp.py @@ -26,7 +26,7 @@ class TestEnergySystemsFactory(TestCase): """ city_file = "../unittests/tests_data/C40_Final.gml" self._output_path = "../unittests/tests_data/w2w_user_output.csv" - self._city = GeometryFactory('citygml', city_file).city + self._city = GeometryFactory('citygml', path=city_file).city EnergySystemsFactory('water to water hp', self._city).enrich() def test_water_to_water_heat_pump_import(self): diff --git a/unittests/test_enrichement.py b/unittests/test_enrichement.py index 16831dea..4d8ac187 100644 --- a/unittests/test_enrichement.py +++ b/unittests/test_enrichement.py @@ -27,7 +27,7 @@ class TestGeometryFactory(TestCase): def _get_citygml(self, file): file_path = (self._example_path / file).resolve() - self._city = GeometryFactory('citygml', file_path).city + self._city = GeometryFactory('citygml', path=file_path).city self.assertIsNotNone(self._city, 'city is none') return self._city @@ -93,9 +93,7 @@ class TestGeometryFactory(TestCase): _construction_keys = ['nrel'] _usage_keys = ['comnet', 'hft'] for construction_key in _construction_keys: - print('construction key: ', construction_key) for usage_key in _usage_keys: - print('usage key: ', usage_key) # construction factory called first city = self._get_citygml(file) for building in city.buildings: diff --git a/unittests/test_exports.py b/unittests/test_exports.py index 75dc96f0..c33fa715 100644 --- a/unittests/test_exports.py +++ b/unittests/test_exports.py @@ -34,7 +34,7 @@ class TestExports(TestCase): def _get_citygml(self, file): file_path = (self._example_path / file).resolve() - self._city = GeometryFactory('citygml', file_path).city + self._city = GeometryFactory('citygml', path=file_path).city self.assertIsNotNone(self._city, 'city is none') return self._city diff --git a/unittests/test_geometry_factory.py b/unittests/test_geometry_factory.py index 5551403c..580fd207 100644 --- a/unittests/test_geometry_factory.py +++ b/unittests/test_geometry_factory.py @@ -8,9 +8,11 @@ from pathlib import Path from unittest import TestCase from numpy import inf +from pyproj import Proj, transform from imports.geometry_factory import GeometryFactory from imports.construction_factory import ConstructionFactory +import geopandas class TestGeometryFactory(TestCase): @@ -28,20 +30,26 @@ class TestGeometryFactory(TestCase): def _get_citygml(self, file): file_path = (self._example_path / file).resolve() - self._city = GeometryFactory('citygml', file_path).city + self._city = GeometryFactory('citygml', path=file_path).city + self.assertIsNotNone(self._city, 'city is none') + return self._city + + def _get_geojson(self, file): + file_path = (self._example_path / file).resolve() + self._city = GeometryFactory('gpandas', path=file_path).city self.assertIsNotNone(self._city, 'city is none') return self._city def _get_obj(self, file): # todo: solve the incongruities between city and city_debug file_path = (self._example_path / file).resolve() - self._city = GeometryFactory('obj', file_path).city + self._city = GeometryFactory('obj', path=file_path).city self.assertIsNotNone(self._city, 'city is none') return self._city def _get_rhino(self, file): file_path = (self._example_path / file).resolve() - self._city = GeometryFactory('rhino', file_path).city + self._city = GeometryFactory('rhino', path=file_path).city self.assertIsNotNone(self._city, 'city is none') return self._city @@ -147,6 +155,21 @@ class TestGeometryFactory(TestCase): for building in city.buildings: self._check_surfaces(building) + def test_import_geopandas(self): + """ + Test geopandas import + """ + file = 'sample.geojson' + city = self._get_geojson(file) + self.assertIsNotNone(city, 'city is none') + self.assertTrue(len(city.buildings) == 1) + self._check_buildings(city) + + for building in city.buildings: + self._check_surfaces(building) + self.assertEqual(1912.0898135701814, building.volume) + self.assertEqual(146.19493345171213, building.floor_area) + # osm def test_subway(self): """ @@ -155,7 +178,7 @@ class TestGeometryFactory(TestCase): """ file_path = (self._example_path / 'subway.osm').resolve() - city = GeometryFactory('osm_subway', file_path).city + city = GeometryFactory('osm_subway', path=file_path).city self.assertIsNotNone(city, 'subway entrances is none') self.assertEqual(len(city.city_objects), 20, 'Wrong number of subway entrances') diff --git a/unittests/test_greenery_catalog.py b/unittests/test_greenery_catalog.py index 7959b127..efcde06e 100644 --- a/unittests/test_greenery_catalog.py +++ b/unittests/test_greenery_catalog.py @@ -1,5 +1,5 @@ """ -TestGeometryFactory test and validate the city model structure geometric parameters +Test greenery factory test and validate the greenery construction SPDX - License - Identifier: LGPL - 3.0 - or -later Copyright © 2022 Concordia CERC group Project Coder Guille Gutierrez guillermo.gutierrezmorote@concordia.ca diff --git a/unittests/test_greenery_in_idf.py b/unittests/test_greenery_in_idf.py index 216ea390..fe251329 100644 --- a/unittests/test_greenery_in_idf.py +++ b/unittests/test_greenery_in_idf.py @@ -27,7 +27,7 @@ class GreeneryInIdf(TestCase): city_file = "../unittests/tests_data/one_building_in_kelowna.gml" output_path = Path('../unittests/tests_outputs/').resolve() - city = GeometryFactory('citygml', city_file).city + city = GeometryFactory('citygml', path=city_file).city for building in city.buildings: building.year_of_construction = 2006 ConstructionFactory('nrel', city).enrich() @@ -80,7 +80,7 @@ class GreeneryInIdf(TestCase): print('With greenery') print(f'heating: {heating} MWh/yr, cooling: {cooling} MWh/yr') - city = GeometryFactory('citygml', city_file).city + city = GeometryFactory('citygml', path=city_file).city for building in city.buildings: building.year_of_construction = 2006 ConstructionFactory('nrel', city).enrich() diff --git a/unittests/test_life_cycle_assessment_factory.py b/unittests/test_life_cycle_assessment_factory.py index 43ba7feb..ef00cd79 100644 --- a/unittests/test_life_cycle_assessment_factory.py +++ b/unittests/test_life_cycle_assessment_factory.py @@ -24,28 +24,28 @@ class TestLifeCycleAssessment(TestCase): def test_fuel(self): city_file = "../unittests/tests_data/C40_Final.gml" - city = GeometryFactory('citygml', city_file).city + city = GeometryFactory('citygml', path=city_file).city LifeCycleAssessment('fuel', city).enrich() for fuel in city.fuels: self.assertTrue(len(city.fuels) > 0) def test_vehicle(self): city_file = "../unittests/tests_data/C40_Final.gml" - city = GeometryFactory('citygml', city_file).city + city = GeometryFactory('citygml', path=city_file).city LifeCycleAssessment('vehicle', city).enrich() for vehicle in city.vehicles: self.assertTrue(len(city.vehicles) > 0) def test_machine(self): city_file = "../unittests/tests_data/C40_Final.gml" - city = GeometryFactory('citygml', city_file).city + city = GeometryFactory('citygml', path=city_file).city LifeCycleAssessment('machine', city).enrich() for machine in city.machines: self.assertTrue(len(city.machines) > 0) def test_material(self): city_file = "../unittests/tests_data/C40_Final.gml" - city = GeometryFactory('citygml', city_file).city + city = GeometryFactory('citygml', path=city_file).city LifeCycleAssessment('material', city).enrich() for material in city.lca_materials: self.assertTrue(len(city.lca_materials) > 0) diff --git a/unittests/test_schedules_factory.py b/unittests/test_schedules_factory.py index 85a3c45d..3d7f5180 100644 --- a/unittests/test_schedules_factory.py +++ b/unittests/test_schedules_factory.py @@ -28,7 +28,7 @@ class TestSchedulesFactory(TestCase): def _get_citygml(self, file): file_path = (self._example_path / file).resolve() - _city = GeometryFactory('citygml', file_path).city + _city = GeometryFactory('citygml', path=file_path).city for building in _city.buildings: building.year_of_construction = 2006 ConstructionFactory('nrel', _city).enrich() diff --git a/unittests/test_usage_factory.py b/unittests/test_usage_factory.py index 15138579..f4feb04c 100644 --- a/unittests/test_usage_factory.py +++ b/unittests/test_usage_factory.py @@ -26,7 +26,7 @@ class TestUsageFactory(TestCase): def _get_citygml(self, file): file_path = (self._example_path / file).resolve() - self._city = GeometryFactory('citygml', file_path).city + self._city = GeometryFactory('citygml', path=file_path).city self.assertIsNotNone(self._city, 'city is none') return self._city diff --git a/unittests/tests_data/sample.geojson b/unittests/tests_data/sample.geojson new file mode 100644 index 00000000..3b1f2eca --- /dev/null +++ b/unittests/tests_data/sample.geojson @@ -0,0 +1,18 @@ +{ "type": "FeatureCollection", + "features": [ + { "type": "Feature", + "geometry": { + "type": "Polygon", + "coordinates": [ + [ [[-73.5027962600162, 45.6572759731914], [-73.5027463586105, 45.6572669735158], [-73.5027513584185, 45.6572530729948], [-73.5026715592026, 45.6572412737672], [-73.5026410593539, 45.6573430727752], [-73.5027703584728, 45.6573621728624], [-73.5027962600162, 45.6572759731914]] ] + ] + + }, + "properties": { + "geom": {"type": "Polygon", "crs": {"type": "name", "properties": {"name": "urn:ogc:def:crs:EPSG::4326"}}, "coordinates": [[[3849322.0855625975, 6060583.24800576], [3849326.3956304314, 6060584.796717078], [3849327.0180495544, 6060583.089519385], [3849333.725799462, 6060585.837955164], [3849328.71788522, 6060598.03498192], [3849317.850609142, 6060593.57976506], [3849322.0855625975, 6060583.24800576]]]}, + "height": 13.0790429485, + "year_built": 2000 + } + } + ] + } \ No newline at end of file