diff --git a/hub/exports/exports_factory.py b/hub/exports/exports_factory.py index 7a63c1da..8778e758 100644 --- a/hub/exports/exports_factory.py +++ b/hub/exports/exports_factory.py @@ -9,6 +9,7 @@ from pathlib import Path from hub.exports.formats.glb import Glb from hub.exports.formats.obj import Obj +from hub.exports.formats.geojson import Geojson from hub.exports.formats.simplified_radiosity_algorithm import SimplifiedRadiosityAlgorithm from hub.exports.formats.stl import Stl from hub.exports.formats.cesiumjs_tileset import CesiumjsTileset @@ -85,6 +86,10 @@ class ExportsFactory: def _glb(self): return Glb(self._city, self._path, target_buildings=self._target_buildings) + @property + def _geojson(self): + return Geojson(self._city, self._path, target_buildings=self._target_buildings) + def export(self): """ Export the city given to the class using the given export type handler diff --git a/hub/exports/formats/cesiumjs_tileset.py b/hub/exports/formats/cesiumjs_tileset.py index 2f23c99d..df1adb52 100644 --- a/hub/exports/formats/cesiumjs_tileset.py +++ b/hub/exports/formats/cesiumjs_tileset.py @@ -1,3 +1,9 @@ +""" +export a city into Cesium tileset format +SPDX - License - Identifier: LGPL - 3.0 - or -later +Copyright © 2022 Concordia CERC group +Project Coder Guille Gutierrez guillermo.gutierrezmorote@concordia.ca +""" import json import math diff --git a/hub/exports/formats/geojson.py b/hub/exports/formats/geojson.py new file mode 100644 index 00000000..d9ef7dae --- /dev/null +++ b/hub/exports/formats/geojson.py @@ -0,0 +1,112 @@ +""" +export a city into Geojson format +SPDX - License - Identifier: LGPL - 3.0 - or -later +Copyright © 2022 Concordia CERC group +Project Coder Guille Gutierrez guillermo.gutierrezmorote@concordia.ca +""" +import json +from pathlib import Path + +import numpy as np +import pyproj +from pyproj import Transformer + +from hub.helpers.geometry_helper import GeometryHelper + + +class Geojson: + """ + Export to geojson format + """ + def __init__(self, city, path, target_buildings): + self._city = city + self._file_path = Path(path / f'{self._city.name}.geojson').resolve() + try: + srs_name = self._city.srs_name + if self._city.srs_name in GeometryHelper.srs_transformations: + srs_name = GeometryHelper.srs_transformations[self._city.srs_name] + input_reference = pyproj.CRS(srs_name) # Projected coordinate system from input data + except pyproj.exceptions.CRSError as err: + raise pyproj.exceptions.CRSError from err + self._to_gps = Transformer.from_crs(input_reference, pyproj.CRS('EPSG:4326')) + if target_buildings is None: + target_buildings = [b.name for b in self._city.buildings] + self._geojson_skeleton = { + 'type': 'FeatureCollection', + 'features': [] + } + self._feature_skeleton = { + 'type': 'Feature', + 'geometry': { + 'type': 'Polygon', + 'coordinates': [] + }, + 'properties': {} + } + self._export() + + def _export(self): + for building in self._city.buildings: + if len(building.grounds) == 1: + ground = building.grounds[0] + feature = self._polygon(ground) + else: + feature = self._multipolygon(building.grounds) + feature['id'] = building.name + feature['properties']['height'] = f'{building.max_height - building.lower_corner[2]}' + feature['properties']['function'] = f'{building.function}' + feature['properties']['year_of_construction'] = f'{building.year_of_construction}' + feature['properties']['aliases'] = building.aliases + feature['properties']['elevation'] = f'{building.lower_corner[2]}' + self._geojson_skeleton['features'].append(feature) + with open(self._file_path, 'w', encoding='utf-8') as f: + json.dump(self._geojson_skeleton, f, indent=2) + + def _polygon(self, ground): + feature = { + 'type': 'Feature', + 'geometry': { + 'type': 'Polygon', + 'coordinates': [] + }, + 'properties': {} + } + ground_coordinates = [] + for coordinate in ground.solid_polygon.coordinates: + gps_coordinate = self._to_gps.transform(coordinate[0], coordinate[1]) + ground_coordinates.insert(0, [gps_coordinate[1], gps_coordinate[0]]) + + first_gps_coordinate = self._to_gps.transform( + ground.solid_polygon.coordinates[0][0], + ground.solid_polygon.coordinates[0][1] + ) + ground_coordinates.insert(0, [first_gps_coordinate[1], first_gps_coordinate[0]]) + feature['geometry']['coordinates'].append(ground_coordinates) + return feature + + def _multipolygon(self, grounds): + feature = { + 'type': 'Feature', + 'geometry': { + 'type': 'MultiPolygon', + 'coordinates': [] + }, + 'properties': {} + } + polygons = [] + for ground in grounds: + ground_coordinates = [] + for coordinate in ground.solid_polygon.coordinates: + gps_coordinate = self._to_gps.transform(coordinate[0], coordinate[1]) + ground_coordinates.insert(0, [gps_coordinate[1], gps_coordinate[0]]) + + first_gps_coordinate = self._to_gps.transform( + ground.solid_polygon.coordinates[0][0], + ground.solid_polygon.coordinates[0][1] + ) + ground_coordinates.insert(0, [first_gps_coordinate[1], first_gps_coordinate[0]]) + polygons.append(ground_coordinates) + feature['geometry']['coordinates'].append(polygons) + return feature + + diff --git a/hub/exports/formats/glb.py b/hub/exports/formats/glb.py index 6329788d..d8c0a942 100644 --- a/hub/exports/formats/glb.py +++ b/hub/exports/formats/glb.py @@ -1,3 +1,9 @@ +""" +export a city into Glb format +SPDX - License - Identifier: LGPL - 3.0 - or -later +Copyright © 2022 Concordia CERC group +Project Coder Guille Gutierrez guillermo.gutierrezmorote@concordia.ca +""" import os import shutil import subprocess @@ -13,6 +19,9 @@ class GltExceptionError(Exception): class Glb: + """ + Glb class + """ def __init__(self, city, path, target_buildings=None): self._city = city self._path = path @@ -23,10 +32,6 @@ class Glb: @property def _obj2gtl(self): - """ - Get the SRA installation path - :return: str - """ return shutil.which('obj2gltf') def _export(self): diff --git a/hub/imports/geometry/citygml.py b/hub/imports/geometry/citygml.py index ab36522f..9e5d5e8b 100644 --- a/hub/imports/geometry/citygml.py +++ b/hub/imports/geometry/citygml.py @@ -4,6 +4,7 @@ SPDX - License - Identifier: LGPL - 3.0 - or -later Copyright © 2022 Concordia CERC group Project Coder Guille Gutierrez guillermo.gutierrezmorote@concordia.ca """ +import logging import numpy as np import xmltodict @@ -79,6 +80,7 @@ class CityGml: self._srs_name = envelope['@srsName'] else: # If not coordinate system given assuming hub standard + logging.warning('gml file contains no coordinate system assuming EPSG:26911 (North america with 4m error)') self._srs_name = "EPSG:26911" else: # get the boundary from the city objects instead diff --git a/tests/test_exports.py b/tests/test_exports.py index a617e67d..b0ad94ef 100644 --- a/tests/test_exports.py +++ b/tests/test_exports.py @@ -7,6 +7,7 @@ Code contributors: Pilar Monsalvete Alvarez de Uribarri pilar.monsalvete@concord """ import json import logging.handlers +import os from pathlib import Path from unittest import TestCase from hub.imports.geometry_factory import GeometryFactory @@ -98,6 +99,19 @@ class TestExports(TestCase): glb_file = Path(self._output_path / f'{building.name}.glb') self.assertTrue(glb_file.exists(), f'{building.name} Building glb wasn\'t correctly generated') + def test_geojson_export(self): + self._export('geojson', False) + geojson_file = Path(self._output_path / f'{self._city.name}.geojson') + self.assertTrue(geojson_file.exists(), f'{geojson_file} doesn\'t exists') + with open(geojson_file, 'r') as f: + geojson = json.load(f) + self.assertEqual(1, len(geojson['features']), 'Wrong number of buildings') + geometry = geojson['features'][0]['geometry'] + self.assertEqual('Polygon', geometry['type'], 'Wrong geometry type') + self.assertEqual(1, len(geometry['coordinates']), 'Wrong polygon structure') + self.assertEqual(11, len(geometry['coordinates'][0]), 'Wrong number of vertices') + os.unlink(geojson_file) # todo: this test need to cover a multipolygon example too + def test_energy_ade_export(self): """ export to energy ADE