From 1e68319e7efa6edd2243b3dd57baeb92c369e32e Mon Sep 17 00:00:00 2001 From: guille Date: Mon, 1 May 2023 18:05:09 -0400 Subject: [PATCH] partial re-implementation of geojson read. --- .../building_demand/internal_zone.py | 4 +- hub/exports/exports_factory.py | 8 - hub/imports/geometry/geojson.py | 232 ++++++++++-------- hub/unittests/test_geometry_factory.py | 44 +--- ...test_imports.py => test_results_import.py} | 3 +- 5 files changed, 144 insertions(+), 147 deletions(-) rename hub/unittests/{test_imports.py => test_results_import.py} (97%) diff --git a/hub/city_model_structure/building_demand/internal_zone.py b/hub/city_model_structure/building_demand/internal_zone.py index 802c7e20..c8669652 100644 --- a/hub/city_model_structure/building_demand/internal_zone.py +++ b/hub/city_model_structure/building_demand/internal_zone.py @@ -78,7 +78,7 @@ class InternalZone: def usages(self) -> [Usage]: """ Get internal zone usage zones - :return: [UsageZone] + :return: [Usage] """ return self._usages @@ -86,7 +86,7 @@ class InternalZone: def usages(self, value): """ Set internal zone usage zones - :param value: [UsageZone] + :param value: [Usage] """ self._usages = value diff --git a/hub/exports/exports_factory.py b/hub/exports/exports_factory.py index ae99dd0d..83fde861 100644 --- a/hub/exports/exports_factory.py +++ b/hub/exports/exports_factory.py @@ -65,14 +65,6 @@ class ExportsFactory: """ return Obj(self._city, self._path) - @property - def _grounded_obj(self): - """ - Export the city geometry to obj with grounded coordinates - :return: None - """ - return Obj(self._city, self._path) - @property def _sra(self): """ diff --git a/hub/imports/geometry/geojson.py b/hub/imports/geometry/geojson.py index 92086b22..6c238e28 100644 --- a/hub/imports/geometry/geojson.py +++ b/hub/imports/geometry/geojson.py @@ -61,66 +61,51 @@ class Geojson: self._min_y = y @staticmethod - def _create_buildings_lod0(name, year_of_construction, function, surfaces_coordinates): - surfaces = [] - buildings = [] - for zone, surface_coordinates in enumerate(surfaces_coordinates): - points = igh.points_from_string(igh.remove_last_point_from_string(surface_coordinates)) - # geojson provides the roofs, need to be transform into grounds - points = igh.invert_points(points) - polygon = Polygon(points) - polygon.area = igh.ground_area(points) - surface = Surface(polygon, polygon) - if len(buildings) == 1: - buildings[0].surfaces.append(surface) - else: - surfaces.append(surface) - buildings.append(Building(f'{name}', surfaces, year_of_construction, function)) - return buildings + def _create_building_lod0(name, year_of_construction, function, surface_coordinates): + points = igh.points_from_string(igh.remove_last_point_from_string(surface_coordinates)) + points = igh.invert_points(points) + polygon = Polygon(points) + polygon.area = igh.ground_area(points) + surface = Surface(polygon, polygon, name=f'{name}_ground') + return Building(f'{name}', [surface], year_of_construction, function) @staticmethod - def _create_buildings_lod1(name, year_of_construction, function, height, surface_coordinates): - lod0_buildings = Geojson._create_buildings_lod0(name, year_of_construction, function, surface_coordinates) + def _create_building_lod1(name, year_of_construction, function, height, surface_coordinates): + building = Geojson._create_building_lod0(name, year_of_construction, function, surface_coordinates) surfaces = [] - buildings = [] - - for zone, lod0_building in enumerate(lod0_buildings): - # print(zone, lod0_building.name) - volume = 0 - for surface in lod0_building.grounds: - volume = volume + surface.solid_polygon.area * height - surfaces.append(surface) - roof_coordinates = [] - # adding a roof means invert the polygon coordinates and change the Z value - for coordinate in surface.solid_polygon.coordinates: - roof_coordinate = np.array([coordinate[0], coordinate[1], height]) - # insert the roof rotated already - roof_coordinates.insert(0, roof_coordinate) - polygon = Polygon(roof_coordinates) - polygon.area = surface.solid_polygon.area - roof = Surface(polygon, polygon) - surfaces.append(roof) - # adding a wall means add the point coordinates and the next point coordinates with Z's height and 0 - coordinates_length = len(roof.solid_polygon.coordinates) - for i, coordinate in enumerate(roof.solid_polygon.coordinates): - j = i + 1 - if j == coordinates_length: - j = 0 - next_coordinate = roof.solid_polygon.coordinates[j] - wall_coordinates = [ - np.array([coordinate[0], coordinate[1], 0.0]), - np.array([next_coordinate[0], next_coordinate[1], 0.0]), - np.array([next_coordinate[0], next_coordinate[1], next_coordinate[2]]), - np.array([coordinate[0], coordinate[1], coordinate[2]]) - ] - polygon = Polygon(wall_coordinates) - wall = Surface(polygon, polygon) - surfaces.append(wall) + volume = 0 + for ground in building.grounds: + volume += ground.solid_polygon.area * height + surfaces.append(ground) + roof_coordinates = [] + # adding a roof means invert the polygon coordinates and change the Z value + for coordinate in ground.solid_polygon.coordinates: + roof_coordinate = np.array([coordinate[0], coordinate[1], height]) + # insert the roof rotated already + roof_coordinates.insert(0, roof_coordinate) + roof_polygon = Polygon(roof_coordinates) + roof_polygon.area = ground.solid_polygon.area + roof = Surface(roof_polygon, roof_polygon) + surfaces.append(roof) + # adding a wall means add the point coordinates and the next point coordinates with Z's height and 0 + coordinates_length = len(roof.solid_polygon.coordinates) + for i, coordinate in enumerate(roof.solid_polygon.coordinates): + j = i + 1 + if j == coordinates_length: + j = 0 + next_coordinate = roof.solid_polygon.coordinates[j] + wall_coordinates = [ + np.array([coordinate[0], coordinate[1], 0.0]), + np.array([next_coordinate[0], next_coordinate[1], 0.0]), + np.array([next_coordinate[0], next_coordinate[1], next_coordinate[2]]), + np.array([coordinate[0], coordinate[1], coordinate[2]]) + ] + polygon = Polygon(wall_coordinates) + wall = Surface(polygon, polygon) + surfaces.append(wall) building = Building(f'{name}', surfaces, year_of_construction, function) building.volume = volume - buildings.append(building) - - return buildings + return building def _get_polygons(self, polygons, coordinates): if type(coordinates[0][self.X]) != float: @@ -186,14 +171,13 @@ class Geojson: Get city out of a Geojson file """ if self._city is None: - missing_functions = [] buildings = [] - building_id = 0 - lod = 1 + lod = 0 for feature in self._geojson['features']: extrusion_height = 0 if self._extrusion_height_field is not None: extrusion_height = float(feature['properties'][self._extrusion_height_field]) + lod = 0.5 year_of_construction = None if self._year_of_construction_field is not None: year_of_construction = int(feature['properties'][self._year_of_construction_field]) @@ -204,57 +188,111 @@ class Geojson: # use the transformation dictionary to retrieve the proper function if function in self._function_to_hub: function = self._function_to_hub[function] - else: - if function not in missing_functions: - missing_functions.append(function) - function = function geometry = feature['geometry'] if 'id' in feature: building_name = feature['id'] - else: - building_name = f'building_{building_id}' - building_id += 1 if self._name_field is not None: building_name = feature['properties'][self._name_field] - polygons = [] - for part, coordinates in enumerate(geometry['coordinates']): - polygons = self._get_polygons(polygons, coordinates) - for polygon in polygons: - if extrusion_height == 0: - buildings = buildings + Geojson._create_buildings_lod0(f'{building_name}', - year_of_construction, - function, - [polygon]) - lod = 0 - else: - if self._max_z < extrusion_height: - self._max_z = extrusion_height - if part == 0: - buildings = buildings + Geojson._create_buildings_lod1(f'{building_name}', - year_of_construction, - function, - extrusion_height, - [polygon]) - else: - new_part = Geojson._create_buildings_lod1(f'{building_name}', - year_of_construction, - function, - extrusion_height, - [polygon]) - surfaces = buildings[len(buildings) - 1].surfaces + new_part[0].surfaces - volume = buildings[len(buildings) - 1].volume + new_part[0].volume - buildings[len(buildings) - 1] = Building(f'{building_name}', surfaces, year_of_construction, function) - buildings[len(buildings) - 1].volume = volume + if str(geometry['type']).lower() == 'polygon': + buildings.append(self._parse_polygon(geometry['coordinates'], + building_name, + function, + year_of_construction, + extrusion_height)) + + elif str(geometry['type']).lower() == 'multipolygon': + buildings.append(self._parse_multi_polygon(geometry['coordinates'], + building_name, + function, + year_of_construction, + extrusion_height)) + else: + raise NotImplementedError(f'Geojson geometry type [{geometry["type"]}] unknown') self._city = City([self._min_x, self._min_y, 0.0], [self._max_x, self._max_y, self._max_z], 'epsg:26911') for building in buildings: # Do not include "small building-like structures" to buildings if building.floor_area >= 25: self._city.add_city_object(building) self._city.level_of_detail.geometry = lod - if lod == 1: + if lod > 0: lines_information = GeometryHelper.city_mapping(self._city, plot=False) self._store_shared_percentage_to_walls(self._city, lines_information) - if len(missing_functions) > 0: - print(f'There are unknown functions {missing_functions}') + return self._city + + def _polygon_coordinates_to_3d(self, polygon_coordinates): + transformed_coordinates = '' + for coordinate in polygon_coordinates: + transformed = self._transformer.transform(coordinate[self.Y], coordinate[self.X]) + self._save_bounds(transformed[self.X], transformed[self.Y]) + transformed_coordinates = f'{transformed_coordinates} {transformed[self.X]} {transformed[self.Y]} 0.0' + return transformed_coordinates.lstrip(' ') + + def _parse_polygon(self, coordinates, building_name, function, year_of_construction, extrusion_height): + print('poly') + for polygon_coordinates in coordinates: + coordinates_3d = self._polygon_coordinates_to_3d(polygon_coordinates) + if extrusion_height == 0: + building = Geojson._create_building_lod0(f'{building_name}', + year_of_construction, + function, + coordinates_3d) + else: + if self._max_z < extrusion_height: + self._max_z = extrusion_height + building = Geojson._create_building_lod1(f'{building_name}', + year_of_construction, + function, + extrusion_height, + coordinates_3d) + return building + + def _parse_multi_polygon(self, coordinates, building_name, function, year_of_construction, extrusion_height): + print('multi') + surfaces = [] + for polygon_coordinate in coordinates: + building = self._parse_polygon(polygon_coordinate, building_name, function, year_of_construction, 0) + for surface in building.surfaces: + if surface.type == cte.GROUND: + surfaces.append(surface) + else: + # overwrite last surface by adding the "hole" in the polygon + polygon = Polygon(surfaces[-1].solid_polygon.coordinates + surface.solid_polygon.coordinates) + surfaces[-1] = Surface(polygon, polygon) + if extrusion_height == 0: + return Building(building_name, surfaces, year_of_construction, function) + else: + volume = 0 + for ground in building.grounds: + volume += ground.solid_polygon.area * extrusion_height + surfaces.append(ground) + roof_coordinates = [] + # adding a roof means invert the polygon coordinates and change the Z value + for coordinate in ground.solid_polygon.coordinates: + roof_coordinate = np.array([coordinate[0], coordinate[1], extrusion_height]) + # insert the roof rotated already + roof_coordinates.insert(0, roof_coordinate) + roof_polygon = Polygon(roof_coordinates) + roof_polygon.area = ground.solid_polygon.area + roof = Surface(roof_polygon, roof_polygon) + surfaces.append(roof) + # adding a wall means add the point coordinates and the next point coordinates with Z's height and 0 + coordinates_length = len(roof.solid_polygon.coordinates) + for i, coordinate in enumerate(roof.solid_polygon.coordinates): + j = i + 1 + if j == coordinates_length: + j = 0 + next_coordinate = roof.solid_polygon.coordinates[j] + wall_coordinates = [ + np.array([coordinate[0], coordinate[1], 0.0]), + np.array([next_coordinate[0], next_coordinate[1], 0.0]), + np.array([next_coordinate[0], next_coordinate[1], next_coordinate[2]]), + np.array([coordinate[0], coordinate[1], coordinate[2]]) + ] + polygon = Polygon(wall_coordinates) + wall = Surface(polygon, polygon) + surfaces.append(wall) + building = Building(f'{building_name}', surfaces, year_of_construction, function) + building.volume = volume + return building diff --git a/hub/unittests/test_geometry_factory.py b/hub/unittests/test_geometry_factory.py index 0e43a59f..6bdeca21 100644 --- a/hub/unittests/test_geometry_factory.py +++ b/hub/unittests/test_geometry_factory.py @@ -137,18 +137,16 @@ class TestGeometryFactory(TestCase): """ Test geojson import """ - file = '2000_buildings.geojson' + file = 'hole_building.geojson' city = GeometryFactory('geojson', path=(self._example_path / file).resolve(), - height_field='building_height', + height_field='citygml_me', year_of_construction_field='ANNEE_CONS', name_field='ID_UEV', function_field='CODE_UTILI', function_to_hub=MontrealFunctionToHubFunction().dictionary).city - # include 25 square meter condition for a building reduces buildings number from 2289 to 2057 - for building in city.buildings: - print(building.name) - self.assertEqual(2057, len(city.buildings), 'wrong number of buildings') + hub.exports.exports_factory.ExportsFactory('obj', city, self._output_path).export_debug() + self.assertEqual(1964, len(city.buildings), 'wrong number of buildings') def test_map_neighbours(self): """ @@ -164,7 +162,7 @@ class TestGeometryFactory(TestCase): year_of_construction_field='ANNEE_CONS', function_field='LIBELLE_UT') - # info_lod0 = GeometryHelper.city_mapping(city, plot=False) + info_lod0 = GeometryHelper.city_mapping(city, plot=False) hub.exports.exports_factory.ExportsFactory('obj', city, self._output_path).export() self.assertEqual(info_lod0, info_lod1) for building in city.buildings: @@ -177,36 +175,6 @@ class TestGeometryFactory(TestCase): self.assertEqual('1_part_0_zone_0', city.city_object('3_part_0_zone_0').neighbours[0].name) self.assertEqual('2_part_0_zone_0', city.city_object('3_part_0_zone_0').neighbours[1].name) - def test_neighbours(self): - """ - Test neighbours map creation - """ - file_path = (self._example_path / 'concordia_clean.geojson').resolve() - city = GeometryFactory('geojson', - path=file_path, - height_field='citygml_me', - year_of_construction_field='ANNEE_CONS', - name_field='OBJECTID_12', - function_field='CODE_UTILI', - function_to_hub=Dictionaries().montreal_function_to_hub_function).city - # print(city.lower_corner, city.upper_corner) - for building in city.buildings: - #for ground in building.grounds: - # print(ground.perimeter_polygon.coordinates) - # print(ground.perimeter_polygon.coordinates[0][0] - city.lower_corner[0], ground.perimeter_polygon.coordinates[0][1] - city.lower_corner[1]) - # print(ground.perimeter_polygon.coordinates[1][0] - city.lower_corner[0], ground.perimeter_polygon.coordinates[1][1] - city.lower_corner[1]) - break - ConstructionFactory('nrcan', city).enrich() - UsageFactory('nrcan', city).enrich() - info_lod1 = GeometryHelper.city_mapping(city, plot=True) - for building in city.buildings: - print(building.name) - ns = '' - for n in building.neighbours: - ns = f'{ns} {n.name}' - for surface in n.surfaces: - print('shared', surface.percentage_shared) - print('\t', ns) - # EnergyBuildingsExportsFactory('idf', city, self._output_path).export() + diff --git a/hub/unittests/test_imports.py b/hub/unittests/test_results_import.py similarity index 97% rename from hub/unittests/test_imports.py rename to hub/unittests/test_results_import.py index 4df02e3c..c8a8e702 100644 --- a/hub/unittests/test_imports.py +++ b/hub/unittests/test_results_import.py @@ -20,7 +20,7 @@ from hub.imports.results_factory import ResultFactory from hub.imports.usage_factory import UsageFactory -class TestImports(TestCase): +class TestResultsImport(TestCase): """ TestImports class contains the unittest for import functionality """ @@ -69,7 +69,6 @@ class TestImports(TestCase): def test_peak_loads(self): # todo: this is not technically a import - # WeatherFactory('epw', self._city, file_name='CAN_PQ_Montreal.Intl.AP.716270_CWEC.epw').enrich() weather_file = (self._example_path / 'CAN_PQ_Montreal.Intl.AP.716270_CWEC.epw').resolve() ExportsFactory('sra', self._city, self._output_path, weather_file=weather_file, weather_format='epw').export() sra_path = (self._output_path / f'{self._city.name}_sra.xml').resolve()