From c682bb934998476ca45118ab8acd417a738de7ec Mon Sep 17 00:00:00 2001 From: AlexanderJuestel Date: Sat, 20 Jul 2024 17:20:10 +0200 Subject: [PATCH 01/63] Fix #334 --- environment.yml | 12 ++++++------ environment_dev.yml | 6 +++--- gemgis/vector.py | 24 +++++++++++++++++++++++- requirements.txt | 8 ++++---- 4 files changed, 36 insertions(+), 14 deletions(-) diff --git a/environment.yml b/environment.yml index 69f597fd..a58f9ddc 100644 --- a/environment.yml +++ b/environment.yml @@ -1,13 +1,13 @@ -# Requirements as of October 2023 +# Requirements as of July 2024 name: gemgis_env channels: - conda-forge dependencies: - - python>=3.10 - # geopandas will also install numpy, pandas, shapely, fiona, and pyproj - - geopandas>=0.14.0 + - python>=3.11 + # geopandas will also install numpy, pandas, shapely, pyogrio, and pyproj + - geopandas>=1.0.1 # rasterio will also install affine - - rasterio>=1.3.8 + - rasterio>=1.3.10 # pyvista also install pooch and matplotlib - - pyvista>=0.42.2 + - pyvista>=0.44.1 - gemgis>=1.1 diff --git a/environment_dev.yml b/environment_dev.yml index 11c6b989..c01a36dc 100644 --- a/environment_dev.yml +++ b/environment_dev.yml @@ -2,9 +2,9 @@ name: gemgis_dev_env channels: - conda-forge dependencies: - - python>=3.10 - # geopandas will also install numpy, pandas, shapely, fiona, and pyproj - - geopandas>=0.14.0 + - python>=3.11 + # geopandas will also install numpy, pandas, shapely, pyogrio, and pyproj + - geopandas>=1.0.1 # rasterio will also install affine #- rasterio>=1.3.8 will be installed through rioxarray # pyvista also install pooch and matplotlib diff --git a/gemgis/vector.py b/gemgis/vector.py index e421aec2..0f8105e6 100644 --- a/gemgis/vector.py +++ b/gemgis/vector.py @@ -28,7 +28,6 @@ import geopandas as gpd from gemgis.raster import sample_from_array, sample_from_rasterio from typing import Union, List, Tuple, Optional, Sequence, Collection -import fiona import pyvista as pv __all__ = [geometry] @@ -7194,6 +7193,13 @@ def load_gpx(path: str, """ + # Trying to import fiona but returning error if fiona is not installed + try: + import fiona + except ModuleNotFoundError: + raise ModuleNotFoundError( + 'fiona package is not installed. Use pip install fiona to install the latest version') + # Checking that the path is of type string if not isinstance(path, str): raise TypeError('The path must be provided as string') @@ -7275,6 +7281,13 @@ def load_gpx_as_dict(path: str, """ + # Trying to import fiona but returning error if fiona is not installed + try: + import fiona + except ModuleNotFoundError: + raise ModuleNotFoundError( + 'fiona package is not installed. Use pip install fiona to install the latest version') + # Checking that the path is of type string if not isinstance(path, str): raise TypeError('The path must be provided as string') @@ -7342,6 +7355,13 @@ def load_gpx_as_geometry(path: str, """ + # Trying to import fiona but returning error if fiona is not installed + try: + import fiona + except ModuleNotFoundError: + raise ModuleNotFoundError( + 'fiona package is not installed. Use pip install fiona to install the latest version') + # Checking that the path is of type string if not isinstance(path, str): raise TypeError('The path must be provided as string') @@ -7375,6 +7395,8 @@ def load_gpx_as_geometry(path: str, return shape +#def load_gpx_as_gdf(): + # Miscellaneous Functions ######################### diff --git a/requirements.txt b/requirements.txt index 2af1968c..03afcaf4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,10 +1,10 @@ -# Requirements as of October 2023 +# Requirements as of July 2024 # geopandas will also install numpy, pandas, shapely, fiona, and pyproj -geopandas>=0.14.0 +geopandas>=1.0.1 # rasterio will also install affine -rasterio>=1.3.8 +rasterio>=1.3.10 # pyvista also install pooch and matplotlib -pyvista>=0.42.2 \ No newline at end of file +pyvista>=0.44.1 \ No newline at end of file From 3bd4087c10f831c50c84117103c26ec797c410ed Mon Sep 17 00:00:00 2001 From: AlexanderJuestel Date: Sat, 20 Jul 2024 19:59:36 +0200 Subject: [PATCH 02/63] Fix #333 --- gemgis/visualization.py | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/gemgis/visualization.py b/gemgis/visualization.py index a95719ab..5f0e55f7 100644 --- a/gemgis/visualization.py +++ b/gemgis/visualization.py @@ -1617,7 +1617,7 @@ def create_depth_maps_from_gempy(geo_model, .. versionadded:: 1.0.x - .. versionchanged:: 1.1.8 + .. versionchanged:: 1.2 Ensure compatibility with GemPy>=3 Example @@ -1625,7 +1625,7 @@ def create_depth_maps_from_gempy(geo_model, >>> # Loading Libraries and creating depth map >>> import gemgis as gg - >>> dict_sand1 = gg.visualization.create_depth_maps(geo_model=geo_model, surfaces='Sand1') + >>> dict_sand1 = gg.visualization.create_depth_maps_from_gempy(geo_model=geo_model, surfaces='Sand1') >>> dict_sand1 {'Sand1': [PolyData (0x2dd0f46c820) N Cells: 4174 @@ -1714,13 +1714,20 @@ def create_depth_maps_from_gempy(geo_model, surfaces_poly = {} for index in list_indices: - surf = pv.PolyData(geo_model.solutions.raw_arrays.vertices[index], - np.insert(geo_model.solutions.raw_arrays.edges[index], 0, 3, axis=1).ravel()) - # Append depth to PolyData - surf['Depth [m]'] = geo_model.solutions.raw_arrays.vertices[index][:, 2] + # Extracting vertices + vertices = geo_model.input_transform.apply_inverse(geo_model.solutions.raw_arrays.vertices[index]) - # Store mesh, depth values and color values in dict + # Extracting faces + faces = np.insert(geo_model.solutions.raw_arrays.edges[index], 0, 3, axis=1).ravel() + + # Creating PolyData from vertices and faces + surf = pv.PolyData(vertices, faces) + + # Appending depth to PolyData + surf['Depth [m]'] = geo_model.input_transform.apply_inverse(geo_model.solutions.raw_arrays.vertices[index])[:, 2] + + # Storing mesh, depth values and color values in dict surfaces_poly[list_surfaces[index]] = [surf, geo_model.structural_frame.elements_colors[index]] return surfaces_poly From 6676046da307058539460b48d0c8eb0b70a453ff Mon Sep 17 00:00:00 2001 From: AlexanderJuestel Date: Sat, 20 Jul 2024 20:16:05 +0200 Subject: [PATCH 03/63] Fix #332 --- gemgis/vector.py | 14 ++++++++++ gemgis/web.py | 69 +++++++++++++++++++++++++++++++++++------------- 2 files changed, 65 insertions(+), 18 deletions(-) diff --git a/gemgis/vector.py b/gemgis/vector.py index 0f8105e6..4470cc62 100644 --- a/gemgis/vector.py +++ b/gemgis/vector.py @@ -7191,6 +7191,10 @@ def load_gpx(path: str, load_gpx_as_dict : Loading a GPX file as dict load_gpx_as_geometry : Loading a GPX file as Shapely BaseGeometry + .. versionadded:: 1.0.x + + .. versionchanged:: 1.2 + """ # Trying to import fiona but returning error if fiona is not installed @@ -7246,6 +7250,8 @@ def load_gpx_as_dict(path: str, .. versionadded:: 1.0.x + .. versionchanged:: 1.2 + Example _______ @@ -7279,6 +7285,10 @@ def load_gpx_as_dict(path: str, load_gpx_as : Loading a GPX file as Collection load_gpx_as_geometry : Loading a GPX file as Shapely BaseGeometry + .. versionadded:: 1.0.x + + .. versionchanged:: 1.2 + """ # Trying to import fiona but returning error if fiona is not installed @@ -7337,6 +7347,8 @@ def load_gpx_as_geometry(path: str, .. versionadded:: 1.0.x + .. versionchanged:: 1.2 + Example _______ @@ -7353,6 +7365,8 @@ def load_gpx_as_geometry(path: str, load_gpx : Loading a GPX file as Collection load_gpx_as_dict : Loading a GPX file as dict + .. versionchanged:: 1.2 + """ # Trying to import fiona but returning error if fiona is not installed diff --git a/gemgis/web.py b/gemgis/web.py index 80fb94b0..127dc3b8 100644 --- a/gemgis/web.py +++ b/gemgis/web.py @@ -33,14 +33,18 @@ ############################### -def load_wms(url: str): # -> owslib.wms.WebMapService: +def load_wms(url: str, + version: str = '1.3.0'): # -> owslib.wms.WebMapService: """Loading a WMS Service by URL Parameters __________ url : str - Link of the WMS Service, e.g. ``url='https://ows.terrestris.de/osm/service?'`` + Link of the WMS Service, e.g. ``url='https://ows.terrestris.de/osm/service?'``. + + version : str, default: ``'1.3.0'`` + Version of the WMS Service, e.g. ``version='1.3.0'``. Returns _______ @@ -50,6 +54,8 @@ def load_wms(url: str): # -> owslib.wms.WebMapService: .. versionadded:: 1.0.x + .. versionchanged:: 1.2 + Example _______ @@ -86,9 +92,14 @@ def load_wms(url: str): # -> owslib.wms.WebMapService: if not isinstance(url, str): raise TypeError('URL must be of type string') + # Checking that the version is provided as string + if not isinstance(version, str): + raise TypeError('The WMS Service version must be provided as string') + # Requesting the WMS Service or returning an error if a module may be missing try: - wms = owslib.wms.WebMapService(url) + wms = owslib.wms.WebMapService(url, + version=version) return wms except requests.exceptions.SSLError: @@ -97,6 +108,7 @@ def load_wms(url: str): # -> owslib.wms.WebMapService: def load_as_map(url: str, + version: str, layer: str, style: str, crs: Union[str, dict], @@ -116,6 +128,9 @@ def load_as_map(url: str, url : str Link of the WMS Service, e.g. ``url='https://ows.terrestris.de/osm/service?'`` + version : str, default: ``'1.3.0'`` + Version of the WMS Service, e.g. ``version='1.3.0'``. + layer : str Name of layer to be requested, e.g. ``layer='OSM-WMS'`` @@ -153,7 +168,6 @@ def load_as_map(url: str, Variable to create a new directory of directory does not exist Options include: ``True`` or ``False``, default set to ``False`` - Returns _______ @@ -162,6 +176,8 @@ def load_as_map(url: str, .. versionadded:: 1.0.x + .. versionchanged:: 1.2 + Example _______ @@ -195,6 +211,10 @@ def load_as_map(url: str, if not isinstance(url, str): raise TypeError('URL must be of type string') + # Checking that the version is provided as string + if not isinstance(version, str): + raise TypeError('The WMS Service version must be provided as string') + # Checking if the layer name is of type string if not isinstance(layer, str): raise TypeError('Layers must be of type string') @@ -264,7 +284,7 @@ def load_as_map(url: str, "The file already exists. Pass overwrite_file=True to overwrite the existing file") # Loading WMS Service - wms = load_wms(url) + wms = load_wms(url, version=version) # Creating map object wms_map = wms.getmap(layers=[layer], styles=[style], srs=crs, bbox=tuple([bbox[0], bbox[2], bbox[1], bbox[3]]), @@ -287,6 +307,7 @@ def load_as_map(url: str, def load_as_array(url: str, + version: str, layer: str, style: str, crs: Union[str, dict], @@ -304,44 +325,47 @@ def load_as_array(url: str, __________ url : str - Link of the WMS Service, e.g. ``url='https://ows.terrestris.de/osm/service?'`` + Link of the WMS Service, e.g. ``url='https://ows.terrestris.de/osm/service?'``. + + version : str, default: ``'1.3.0'`` + Version of the WMS Service, e.g. ``version='1.3.0'``. layer : str - Name of layer to be requested, e.g. ``layer='OSM-WMS'`` + Name of layer to be requested, e.g. ``layer='OSM-WMS'``. style : str - Name of style of the layer, e.g. ``style='default'`` + Name of style of the layer, e.g. ``style='default'``. crs : str - String or dict containing the CRS, e.g. ``crs='EPSG:4647'`` + String or dict containing the CRS, e.g. ``crs='EPSG:4647'``. bbox : List[Union[float,int]] - List of bounding box coordinates, e.g. ``bbox=[0, 972, 0, 1069]`` + List of bounding box coordinates, e.g. ``bbox=[0, 972, 0, 1069]``. size : List[int] - List of x and y values defining the size of the image, e.g. ``size=[1000,1000]`` + List of x and y values defining the size of the image, e.g. ``size=[1000,1000]``. filetype : str - String of the image type to be downloaded, e.g. 'filetype='image/png'`` + String of the image type to be downloaded, e.g. 'filetype='image/png'``. transparent : bool Variable if layer is transparent. - Options include: ``True`` or ``False``, default set to ``True`` + Options include: ``True`` or ``False``, default set to ``True``. save_image : bool Variable to save image. - Options include: ``True`` or ``False``, default set to ``False`` + Options include: ``True`` or ``False``, default set to ``False``. path : str - Path and file name of the file to be saved, e.g. ``path=map.tif`` + Path and file name of the file to be saved, e.g. ``path=map.tif``. overwrite_file : bool Variable to overwrite an already existing file. - Options include: ``True`` or ``False``, default set to ``False`` + Options include: ``True`` or ``False``, default set to ``False``. create_directory : bool - Variable to create a new directory of directory does not exist - Options include: ``True`` or ``False``, default set to ``False`` + Variable to create a new directory of directory does not exist. + Options include: ``True`` or ``False``, default set to ``False``. Returns _______ @@ -372,6 +396,10 @@ def load_as_array(url: str, load_wms : Load WMS Service load_as_map : Load Map from WMS Service + .. versionadded:: 1.0.x + + .. versionchanged:: 1.2 + """ # Trying to import owslib but returning error if owslib is not installed @@ -396,6 +424,10 @@ def load_as_array(url: str, if not isinstance(url, str): raise TypeError('URL must be of type string') + # Checking that the version is provided as string + if not isinstance(version, str): + raise TypeError('The WMS Service version must be provided as string') + # Checking if the layer name is of type string if not isinstance(layer, str): raise TypeError('Layers must be of type string') @@ -466,6 +498,7 @@ def load_as_array(url: str, # Creating WMS map object wms_map = load_as_map(url=url, + version=version, layer=layer, style=style, crs=crs, From cbc9f31bc36358b832b3861face4395e9640e29b Mon Sep 17 00:00:00 2001 From: AlexanderJuestel Date: Sat, 20 Jul 2024 20:32:19 +0200 Subject: [PATCH 04/63] Fix #337 --- gemgis/vector.py | 44 +++++++++++++++++++++++++------------------- 1 file changed, 25 insertions(+), 19 deletions(-) diff --git a/gemgis/vector.py b/gemgis/vector.py index 4470cc62..77c0b3ab 100644 --- a/gemgis/vector.py +++ b/gemgis/vector.py @@ -6068,8 +6068,8 @@ def calculate_orientation_for_three_point_problem(gdf: gpd.geodataframe.GeoDataF ######################################### -def intersection_polygon_polygon(polygon1: shapely.geometry.polygon.Polygon, - polygon2: shapely.geometry.polygon.Polygon) \ +def intersect_polygon_polygon(polygon1: shapely.geometry.polygon.Polygon, + polygon2: shapely.geometry.polygon.Polygon) \ -> Union[shapely.geometry.linestring.LineString, shapely.geometry.polygon.Polygon]: """Calculating the intersection between to Shapely Polygons @@ -6092,6 +6092,8 @@ def intersection_polygon_polygon(polygon1: shapely.geometry.polygon.Polygon, .. versionadded:: 1.0.x + .. versionchanged:: 1.2 + Example _______ @@ -6108,15 +6110,15 @@ def intersection_polygon_polygon(polygon1: shapely.geometry.polygon.Polygon, 'POLYGON ((10 0, 20 0, 20 10, 10 10, 10 0))' >>> # Calculating the intersection between two polygons - >>> intersection = gg.vector.intersection_polygon_polygon(polygon1=polygon1, polygon2=polygon2) + >>> intersection = gg.vector.intersect_polygon_polygon(polygon1=polygon1, polygon2=polygon2) >>> intersection.wkt 'LINESTRING (10 0, 10 10)' See Also ________ - intersections_polygon_polygons : Intersecting a polygon with mutiple polygons - intersections_polygons_polygons : Intersecting multiple polygons with multiple polygons + intersect_polygon_polygons : Intersecting a polygon with mutiple polygons + intersect_polygons_polygons : Intersecting multiple polygons with multiple polygons extract_xy_from_polygon_intersections : Extracting intersections between multiple polygons """ @@ -6151,9 +6153,9 @@ def intersection_polygon_polygon(polygon1: shapely.geometry.polygon.Polygon, return intersection -def intersections_polygon_polygons(polygon1: shapely.geometry.polygon.Polygon, - polygons2: Union[ - gpd.geodataframe.GeoDataFrame, List[shapely.geometry.polygon.Polygon]]) \ +def intersect_polygon_polygons(polygon1: shapely.geometry.polygon.Polygon, + polygons2: Union[ + gpd.geodataframe.GeoDataFrame, List[shapely.geometry.polygon.Polygon]]) \ -> List[shapely.geometry.base.BaseGeometry]: """Calculating the intersections between one polygon and a list of polygons @@ -6175,6 +6177,8 @@ def intersections_polygon_polygons(polygon1: shapely.geometry.polygon.Polygon, .. versionadded:: 1.0.x + .. versionchanged:: 1.2 + Example _______ @@ -6210,8 +6214,8 @@ def intersections_polygon_polygons(polygon1: shapely.geometry.polygon.Polygon, See Also ________ - intersection_polygon_polygon : Intersecting a polygon with a polygon - intersections_polygons_polygons : Intersecting multiple polygons with multiple polygons + intersect_polygon_polygon : Intersecting a polygon with a polygon + intersect_polygons_polygons : Intersecting multiple polygons with multiple polygons extract_xy_from_polygon_intersections : Extracting intersections between multiple polygons """ @@ -6250,13 +6254,13 @@ def intersections_polygon_polygons(polygon1: shapely.geometry.polygon.Polygon, raise TypeError('None of the geometry elements of polygons2 must be empty') # Creating the list of intersection geometries - intersections = [intersection_polygon_polygon(polygon1=polygon1, - polygon2=polygon) for polygon in polygons2] + intersections = [intersect_polygon_polygon(polygon1=polygon1, + polygon2=polygon) for polygon in polygons2] return intersections -def intersections_polygons_polygons( +def intersect_polygons_polygons( polygons1: Union[gpd.geodataframe.GeoDataFrame, List[shapely.geometry.polygon.Polygon]], polygons2: Union[gpd.geodataframe.GeoDataFrame, List[shapely.geometry.polygon.Polygon]]) \ -> List[shapely.geometry.base.BaseGeometry]: @@ -6279,6 +6283,8 @@ def intersections_polygons_polygons( .. versionadded:: 1.0.x + .. versionchanged:: 1.2 + Example _______ @@ -6301,7 +6307,7 @@ def intersections_polygons_polygons( >>> polygons2 = [polygon2, polygon2] >>> # Calculating intersections between polygons and polygons - >>> intersection = gg.vector.intersections_polygons_polygons(polygons1=polygons1, polygons2=polygons2) + >>> intersection = gg.vector.intersect_polygons_polygons(polygons1=polygons1, polygons2=polygons2) >>> intersection [, , @@ -6327,8 +6333,8 @@ def intersections_polygons_polygons( See Also ________ - intersection_polygon_polygon : Intersecting a polygon with a polygon - intersections_polygon_polygons : Intersecting a polygons with multiple polygons + intersect_polygon_polygon : Intersecting a polygon with a polygon + intersect_polygon_polygons : Intersecting a polygons with multiple polygons extract_xy_from_polygon_intersections : Extracting intersections between multiple polygons """ @@ -6378,8 +6384,8 @@ def intersections_polygons_polygons( raise TypeError('None of the geometry elements of polygons2 must be empty') # Creating list with lists of intersections - intersections = [intersections_polygon_polygons(polygon1=polygon, - polygons2=polygons2) for polygon in polygons1] + intersections = [intersect_polygon_polygons(polygon1=polygon, + polygons2=polygons2) for polygon in polygons1] # Creating single list from list of lists intersections = [intersections[i][j] for i in range(len(intersections)) for j in range(len(intersections[i]))] @@ -6465,7 +6471,7 @@ def extract_xy_from_polygon_intersections(gdf: gpd.geodataframe.GeoDataFrame, gdf = gdf[gdf.geometry.is_valid].reset_index(drop=True) # Creating a list of GeoDataFrames with intersections - intersections = [intersections_polygons_polygons( + intersections = [intersect_polygons_polygons( polygons1=gdf[gdf['formation'].isin([gdf['formation'].unique().tolist()[i]])], polygons2=gdf[gdf['formation'].isin(gdf['formation'].unique().tolist()[i + 1:])]) for i in range(len(gdf['formation'].unique().tolist()))] From 218341b095700ac5cd95a7af2f77407085329e35 Mon Sep 17 00:00:00 2001 From: AlexanderJuestel Date: Sat, 20 Jul 2024 20:38:23 +0200 Subject: [PATCH 05/63] Fix tests #337 --- tests/test_vector.py | 35 +++++++++++++++++------------------ 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/tests/test_vector.py b/tests/test_vector.py index 9798d2e1..81f4475d 100644 --- a/tests/test_vector.py +++ b/tests/test_vector.py @@ -4910,17 +4910,17 @@ def test_extract_orientations_from_cross_sections(): assert {'X', 'Y', 'Z', 'dip', 'azimuth', 'polarity', 'geometry'}.issubset(orientation.columns) -# Testing intersection_polygon_polygon +# Testing intersect_polygon_polygon ########################################################## -def test_intersection_polygon_polygon(): - from gemgis.vector import intersection_polygon_polygon +def test_intersect_polygon_polygon(): + from gemgis.vector import intersect_polygon_polygon polygon1 = Polygon([(0, 0), (10, 0), (10, 10), (0, 10)]) polygon2 = Polygon([(10, 0), (20, 0), (20, 10), (10, 10)]) - intersection = intersection_polygon_polygon(polygon1=polygon1, - polygon2=polygon2) + intersection = intersect_polygon_polygon(polygon1=polygon1, + polygon2=polygon2) assert isinstance(intersection, LineString) assert intersection.wkt == 'LINESTRING (10 0, 10 10)' @@ -4929,8 +4929,8 @@ def test_intersection_polygon_polygon(): polygon2 = Polygon([(5, 0), (15, 0), (15, 10), (5, 10)]) - intersection = intersection_polygon_polygon(polygon1=polygon1, - polygon2=polygon2) + intersection = intersect_polygon_polygon(polygon1=polygon1, + polygon2=polygon2) assert isinstance(intersection, Polygon) try: @@ -4938,20 +4938,19 @@ def test_intersection_polygon_polygon(): except AssertionError: assert intersection.wkt == 'POLYGON ((5 0, 5 10, 10 10, 10 0, 5 0))' - # Testing intersection_polygon_polygons - +# Testing intersect_polygon_polygons ########################################################## -def test_intersections_polygon_polygons(): - from gemgis.vector import intersections_polygon_polygons +def test_intersect_polygon_polygons(): + from gemgis.vector import intersect_polygon_polygons polygon1 = Polygon([(0, 0), (10, 0), (10, 10), (0, 10)]) polygons2 = [Polygon([(10, 0), (20, 0), (20, 10), (10, 10)]), Polygon([(5, 0), (15, 0), (15, 10), (5, 10)])] - intersections = intersections_polygon_polygons(polygon1=polygon1, - polygons2=polygons2) + intersections = intersect_polygon_polygons(polygon1=polygon1, + polygons2=polygons2) assert isinstance(intersections, list) assert len(intersections) == 2 @@ -4965,17 +4964,17 @@ def test_intersections_polygon_polygons(): assert intersections[1].wkt == 'POLYGON ((5 0, 5 10, 10 10, 10 0, 5 0))' -# Testing intersection_polygon_polygons +# Testing intersect_polygon_polygons ########################################################## -def test_intersections_polygons_polygons(): - from gemgis.vector import intersections_polygons_polygons +def test_intersect_polygons_polygons(): + from gemgis.vector import intersect_polygons_polygons polygons = [Polygon([(0, 0), (10, 0), (10, 10), (0, 10)]), Polygon([(10, 0), (20, 0), (20, 10), (10, 10)]), Polygon([(5, 0), (15, 0), (15, 10), (5, 10)])] - intersections = intersections_polygons_polygons(polygons1=polygons, - polygons2=polygons) + intersections = intersect_polygons_polygons(polygons1=polygons, + polygons2=polygons) assert isinstance(intersections, list) assert len(intersections) == 9 From ed518bddaf898aeddba2b52a6eac4d54f98a8e3f Mon Sep 17 00:00:00 2001 From: AlexanderJuestel Date: Sun, 21 Jul 2024 11:16:42 +0200 Subject: [PATCH 06/63] Fix #341 --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 448972e3..094dabad 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ +# GemGIS - Spatial data and information processing for geomodeling and subsurface data +

-> Spatial data and information processing for geomodeling and subsurface data [![PyPI](https://img.shields.io/badge/python-3-blue.svg)](https://www.python.org/downloads/) From 21c03724fad7b11451587be3f99140a599146b72 Mon Sep 17 00:00:00 2001 From: AlexanderJuestel Date: Sun, 21 Jul 2024 12:02:29 +0200 Subject: [PATCH 07/63] Fix #348 --- gemgis/download_gemgis_data.py | 1 - gemgis/gemgis.py | 1 - gemgis/postprocessing.py | 42 +++++++++++++++++----------------- gemgis/raster.py | 3 +-- gemgis/utils.py | 15 ++++++------ gemgis/web.py | 35 ++++------------------------ 6 files changed, 33 insertions(+), 64 deletions(-) diff --git a/gemgis/download_gemgis_data.py b/gemgis/download_gemgis_data.py index a96c4d98..a999445b 100644 --- a/gemgis/download_gemgis_data.py +++ b/gemgis/download_gemgis_data.py @@ -82,7 +82,6 @@ def download_tutorial_data(filename: str, create_pooch : Create pooch class to fetch files from a website. """ try: - import pooch from pooch import HTTPDownloader download = HTTPDownloader(progressbar=False) except ModuleNotFoundError: diff --git a/gemgis/gemgis.py b/gemgis/gemgis.py index ebef8c08..c5d544a6 100644 --- a/gemgis/gemgis.py +++ b/gemgis/gemgis.py @@ -22,7 +22,6 @@ import numpy as np # import scooby import pandas as pd -from pandas.core import frame import rasterio import geopandas as gpd import rasterio.transform diff --git a/gemgis/postprocessing.py b/gemgis/postprocessing.py index 28bc19d2..e0086fbd 100644 --- a/gemgis/postprocessing.py +++ b/gemgis/postprocessing.py @@ -63,12 +63,12 @@ def extract_lithologies(geo_model, raise ModuleNotFoundError( 'Matplotlib package is not installed. Use pip install matplotlib to install the latest version') - # Trying to import gempy but returning error if gempy is not installed - try: - import gempy as gp - except ModuleNotFoundError: - raise ModuleNotFoundError( - 'GemPy package is not installed. Use pip install gempy to install the latest version') + ## Trying to import gempy but returning error if gempy is not installed + #try: + # import gempy as gp + #except ModuleNotFoundError: + # raise ModuleNotFoundError( + # 'GemPy package is not installed. Use pip install gempy to install the latest version') shape = geo_model._grid.topography.values_2d[:, :, 2].shape @@ -502,11 +502,11 @@ def crop_block_to_topography(geo_model) -> pv.core.pointset.UnstructuredGrid: """ # Trying to import GemPy - try: - import gempy as gp - except ModuleNotFoundError: - raise ModuleNotFoundError( - 'GemPy package is not installed. Use pip install gempy to install the latest version') + #try: + # import gempy as gp + #except ModuleNotFoundError: + # raise ModuleNotFoundError( + # 'GemPy package is not installed. Use pip install gempy to install the latest version') # Trying to import PVGeo try: @@ -880,7 +880,7 @@ def save_qgis_qml_file(gdf: gpd.geodataframe.GeoDataFrame, raise TypeError('gdf must be a GeoDataFrame') # Checking that the geometry column is present in the gdf - if not 'geometry' in gdf: + if 'geometry' not in gdf: raise ValueError('geometry column not present in GeoDataFrame') # Checking that all geometries are Polygons @@ -892,7 +892,7 @@ def save_qgis_qml_file(gdf: gpd.geodataframe.GeoDataFrame, raise TypeError('value column name must be of type string') # Checking that the value column is present in the gdf - if not value in gdf: + if value not in gdf: raise ValueError('"%s" not in gdf. Please provide a valid column name.' % value) # Checking that the color column is of type string @@ -900,7 +900,7 @@ def save_qgis_qml_file(gdf: gpd.geodataframe.GeoDataFrame, raise TypeError('color column name must be of type string') # Checking that the value column is present in the gdf - if not color in gdf: + if color not in gdf: raise ValueError('"%s" not in gdf. Please provide a valid column name.' % color) # Creating RGBA column from hex colors @@ -1058,11 +1058,11 @@ def clip_fault_of_gempy_model(geo_model, """ # Trying to import gempy but returning error if gempy is not installed - try: - import gempy as gp - except ModuleNotFoundError: - raise ModuleNotFoundError( - 'GemPy package is not installed. Use pip install gempy to install the latest version') + #try: + # import gempy as gp + #except ModuleNotFoundError: + # raise ModuleNotFoundError( + # 'GemPy package is not installed. Use pip install gempy to install the latest version') # Checking that the fault is provided as string if not isinstance(fault, str): @@ -1070,7 +1070,7 @@ def clip_fault_of_gempy_model(geo_model, # Checking that the fault is a fault of the geo_model if isinstance(fault, str): - if not fault in geo_model.surfaces.df['surface'][geo_model.surfaces.df['isFault'] == True].tolist(): + if fault not in geo_model.surfaces.df['surface'][geo_model.surfaces.df['isFault'] == True].tolist(): raise ValueError('Fault is not part of the GemPy geo_model') # Getting the fault DataFrames @@ -1087,7 +1087,7 @@ def clip_fault_of_gempy_model(geo_model, # Checking that the correct values are provided for the parameter which if isinstance(which, str): - if not which in ['first', 'last', 'both']: + if which not in ['first', 'last', 'both']: raise ValueError('The options for the parameter "which" include "first", "last", or "both"') # Checking that the i size is of type int or float diff --git a/gemgis/raster.py b/gemgis/raster.py index 8f619bec..20ed37cb 100644 --- a/gemgis/raster.py +++ b/gemgis/raster.py @@ -28,8 +28,7 @@ import pandas as pd import geopandas as gpd from typing import Union, List, Sequence, Optional, Iterable, Dict, Tuple -from rasterio.mask import mask -from shapely.geometry import box, Polygon, LineString +from shapely.geometry import Polygon, LineString import shapely from pathlib import Path import affine diff --git a/gemgis/utils.py b/gemgis/utils.py index 596b3e60..5afe457c 100644 --- a/gemgis/utils.py +++ b/gemgis/utils.py @@ -541,11 +541,11 @@ def show_number_of_data_points(geo_model): """ # Trying to import gempy but returning error if gempy is not installed - try: - import gempy as gp - except ModuleNotFoundError: - raise ModuleNotFoundError( - 'GemPy package is not installed. Use pip install gempy to install the latest version') + #try: + # import gempy as gp + #except ModuleNotFoundError: + # raise ModuleNotFoundError( + # 'GemPy package is not installed. Use pip install gempy to install the latest version') # Create empty lists to store values no_int = [] @@ -2405,7 +2405,6 @@ def rotate_gempy_input_data(extent: Union[np.ndarray, shapely.geometry.Polygon, list(extent_polygon.centroid.coords)[0][1])) # Creating Bounding Box - bbox = box(*extent_rotated.bounds) extent = [extent_rotated.bounds[0], extent_rotated.bounds[2], extent_rotated.bounds[1], @@ -2636,7 +2635,7 @@ def get_cdp_linestring_of_seismic_data(path_in: str, """ # Trying to import segysak but returning error if segysak is not installed try: - from segysak.segy import segy_loader, segy_writer + from segysak.segy import segy_loader except ModuleNotFoundError: raise ModuleNotFoundError( 'segysak package is not installed. Use pip install segysak to install the latest version') @@ -2720,7 +2719,7 @@ def get_cdp_points_of_seismic_data(path_in: str, """ # Trying to import segysak but returning error if segysak is not installed try: - from segysak.segy import segy_loader, segy_writer + from segysak.segy import segy_loader except ModuleNotFoundError: raise ModuleNotFoundError( 'segysak package is not installed. Use pip install segysak to install the latest version') diff --git a/gemgis/web.py b/gemgis/web.py index 127dc3b8..e4056526 100644 --- a/gemgis/web.py +++ b/gemgis/web.py @@ -98,8 +98,8 @@ def load_wms(url: str, # Requesting the WMS Service or returning an error if a module may be missing try: - wms = owslib.wms.WebMapService(url, - version=version) + wms = WebMapService(url, + version=version) return wms except requests.exceptions.SSLError: @@ -197,11 +197,7 @@ def load_as_map(url: str, # Trying to import owslib but returning error if owslib is not installed try: - import owslib from owslib import util - from owslib.wms import WebMapService - from owslib.wfs import WebFeatureService - from owslib.wcs import WebCoverageService __all__ = [util] except ModuleNotFoundError: raise ModuleNotFoundError( @@ -404,11 +400,7 @@ def load_as_array(url: str, # Trying to import owslib but returning error if owslib is not installed try: - import owslib from owslib import util - from owslib.wms import WebMapService - from owslib.wfs import WebFeatureService - from owslib.wcs import WebCoverageService __all__ = [util] except ModuleNotFoundError: raise ModuleNotFoundError( @@ -557,9 +549,7 @@ def load_wfs(url: str): # -> owslib.wfs.WebFeatureService: try: import owslib from owslib import util - from owslib.wms import WebMapService from owslib.wfs import WebFeatureService - from owslib.wcs import WebCoverageService __all__ = [util] except ModuleNotFoundError: raise ModuleNotFoundError( @@ -578,7 +568,7 @@ def load_wfs(url: str): # -> owslib.wfs.WebFeatureService: # Requesting the WMS Service or returning an error if a module may be missing try: - wfs = owslib.wfs.WebFeatureService(url) + wfs = WebFeatureService(url) return wfs @@ -634,9 +624,6 @@ def load_as_gpd(url: str, try: import owslib from owslib import util - from owslib.wms import WebMapService - from owslib.wfs import WebFeatureService - from owslib.wcs import WebCoverageService __all__ = [util] except ModuleNotFoundError: raise ModuleNotFoundError( @@ -731,8 +718,6 @@ def load_wcs(url: str): # -> owslib.wcs.WebCoverageService: try: import owslib from owslib import util - from owslib.wms import WebMapService - from owslib.wfs import WebFeatureService from owslib.wcs import WebCoverageService __all__ = [util] except ModuleNotFoundError: @@ -744,7 +729,7 @@ def load_wcs(url: str): # -> owslib.wcs.WebCoverageService: raise TypeError('URL must be of type string') # Loading the WCS Layer - wcs = owslib.wcs.WebCoverageService(url) + wcs = WebCoverageService(url) return wcs @@ -810,11 +795,7 @@ def create_request(wcs_url: str, # Trying to import owslib but returning error if owslib is not installed try: - import owslib from owslib import util - from owslib.wms import WebMapService - from owslib.wfs import WebFeatureService - from owslib.wcs import WebCoverageService __all__ = [util] except ModuleNotFoundError: raise ModuleNotFoundError( @@ -904,11 +885,7 @@ def load_as_file(url: str, # Trying to import owslib but returning error if owslib is not installed try: - import owslib from owslib import util - from owslib.wms import WebMapService - from owslib.wfs import WebFeatureService - from owslib.wcs import WebCoverageService __all__ = [util] except ModuleNotFoundError: raise ModuleNotFoundError( @@ -1018,11 +995,7 @@ def load_as_files(wcs_url: str, # Trying to import owslib but returning error if owslib is not installed try: - import owslib from owslib import util - from owslib.wms import WebMapService - from owslib.wfs import WebFeatureService - from owslib.wcs import WebCoverageService __all__ = [util] except ModuleNotFoundError: raise ModuleNotFoundError( From 609ec9c7c92288ed4fce8b6da7820e640877acd9 Mon Sep 17 00:00:00 2001 From: AlexanderJuestel Date: Sun, 21 Jul 2024 12:33:16 +0200 Subject: [PATCH 08/63] Fix #344 --- README.md | 36 +++++++++++++++++++++++++++++------- 1 file changed, 29 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 094dabad..cf8f3971 100644 --- a/README.md +++ b/README.md @@ -56,13 +56,35 @@ The Contribution Guidelines for GemGIS can be found here: [Contribution Guidelin We welcome issue reports, questions, ideas for new features and pull-requests to fix issues or even add new features to the software. Once a pull-request is opened, we will guide through the review process. + +## Citation + +If you use GemGIS for any published work, please cite it using the reference below: + +Jüstel, A., Endlein Correira, A., Pischke, M., de la Varga, M., Wellmann, F. (2022). GemGIS - Spatial Data Processing for Geomodeling. Journal of Open Source Software, 7(73), 3709, https://doi.org/10.21105/joss.03709. + +``` +@article{Jüstel2022, +doi = {10.21105/joss.03709}, +url = {https://doi.org/10.21105/joss.03709}, +year = {2022}, +publisher = {The Open Journal}, +volume = {7}, +number = {73}, +pages = {3709}, +author = {Alexander Jüstel and Arthur Endlein Correira and Marius Pischke and Miguel de la Varga and Florian Wellmann}, +title = {GemGIS - Spatial Data Processing for Geomodeling}, +journal = {Journal of Open Source Software} +} +``` + ## References -* Jüstel et al., (2023). From Maps to Models - Tutorials for structural geological modeling using GemPy and GemGIS. Journal of Open Source Education, 6(66), 185, https://doi.org/10.21105/jose.00185 -* Jüstel et al., (2022). GemGIS - Spatial Data Processing for Geomodeling. Journal of Open Source Software, 7(73), 3709, https://doi.org/10.21105/joss.03709 -* Jüstel, A., Endlein Correira, A., Wellmann, F. and Pischke, M.: GemGIS – GemPy Geographic: Open-Source Spatial Data Processing for Geological Modeling. EGU General Assembly 2021, https://doi.org/10.5194/egusphere-egu21-4613, 2021 -* Jüstel, A.: 3D Probabilistic Modeling and Data Analysis of the Aachen-Weisweiler Area: Implications for Deep Geothermal Energy Exploration, unpublished Master Thesis at RWTH Aachen University, 2020 -* de la Varga, M., Schaaf, A., and Wellmann, F.: GemPy 1.0: open-source stochastic geological modeling and inversion, Geosci. Model Dev., 12, 1-32, https://doi.org/10.5194/gmd-12-1-2019, 2019 -* Powell, D.: Interpretation of Geological Structures Through Maps: An Introductory Practical Manual, Longman, pp. 192, 1992 -* Bennison, G.M.: An Introduction to Geological Structures and Maps, Hodder Education Publication, pp. 78, 1990 +* Jüstel, A. et al.: From Maps to Models - Tutorials for structural geological modeling using GemPy and GemGIS. Journal of Open Source Education, 6(66), 185, https://doi.org/10.21105/jose.00185, 2023. +* Jüstel, A. et al.: GemGIS - Spatial Data Processing for Geomodeling. Journal of Open Source Software, 7(73), 3709, https://doi.org/10.21105/joss.03709, 2022. +* Jüstel, A., Endlein Correira, A., Wellmann, F. and Pischke, M.: GemGIS – GemPy Geographic: Open-Source Spatial Data Processing for Geological Modeling. EGU General Assembly 2021, https://doi.org/10.5194/egusphere-egu21-4613, 2021. +* Jüstel, A.: 3D Probabilistic Modeling and Data Analysis of the Aachen-Weisweiler Area: Implications for Deep Geothermal Energy Exploration, unpublished Master Thesis at RWTH Aachen University, 2020. +* de la Varga, M., Schaaf, A., and Wellmann, F.: GemPy 1.0: open-source stochastic geological modeling and inversion, Geosci. Model Dev., 12, 1-32, https://doi.org/10.5194/gmd-12-1-2019, 2019. +* Powell, D.: Interpretation of Geological Structures Through Maps: An Introductory Practical Manual, Longman, pp. 192, 1992. +* Bennison, G.M.: An Introduction to Geological Structures and Maps, Hodder Education Publication, pp. 78, 1990. From 8000aaa1cd98a2fc6747cd0309af9f727023dd67 Mon Sep 17 00:00:00 2001 From: AlexanderJuestel Date: Sun, 21 Jul 2024 12:34:07 +0200 Subject: [PATCH 09/63] Fix #344 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index cf8f3971..bee3d694 100644 --- a/README.md +++ b/README.md @@ -61,7 +61,7 @@ We welcome issue reports, questions, ideas for new features and pull-requests to If you use GemGIS for any published work, please cite it using the reference below: -Jüstel, A., Endlein Correira, A., Pischke, M., de la Varga, M., Wellmann, F. (2022). GemGIS - Spatial Data Processing for Geomodeling. Journal of Open Source Software, 7(73), 3709, https://doi.org/10.21105/joss.03709. +Jüstel, A., Endlein Correira, A., Pischke, M., de la Varga, M., Wellmann, F.: GemGIS - Spatial Data Processing for Geomodeling. Journal of Open Source Software, 7(73), 3709, https://doi.org/10.21105/joss.03709, 2022. ``` @article{Jüstel2022, From 15ca7b6f6287a63028f49d2f43a61b60c51c7a6d Mon Sep 17 00:00:00 2001 From: AlexanderJuestel Date: Sun, 21 Jul 2024 12:38:43 +0200 Subject: [PATCH 10/63] Fix #348 --- gemgis/utils.py | 2 +- gemgis/vector.py | 2 +- gemgis/visualization.py | 4 ++-- gemgis/web.py | 4 ---- tests/test_gemgis.py | 1 - tests/test_misc.py | 8 ++++---- tests/test_utils.py | 2 +- tests/test_web.py | 4 ++-- 8 files changed, 11 insertions(+), 16 deletions(-) diff --git a/gemgis/utils.py b/gemgis/utils.py index 5afe457c..43884219 100644 --- a/gemgis/utils.py +++ b/gemgis/utils.py @@ -2203,7 +2203,7 @@ def chunks(x, n): for j in chunks(i, nodes_per_line): j_fmt = "0.{}f".format(decimal_places) j_fmt = "{0:" + j_fmt + "}" - j = [j_fmt.format(float(x)) if not x is np.nan else j_fmt.format(float(nodata)) for x in j] + j = [j_fmt.format(float(x)) if x is not np.nan else j_fmt.format(float(nodata)) for x in j] line = "{:>" + "{}".format(field_width) + "}" lines.append("".join([line] * len(j)).format(*tuple(j))) diff --git a/gemgis/vector.py b/gemgis/vector.py index 77c0b3ab..303ff910 100644 --- a/gemgis/vector.py +++ b/gemgis/vector.py @@ -8179,7 +8179,7 @@ def create_voronoi_polygons(gdf: gpd.geodataframe.GeoDataFrame) -> gpd.geodatafr vor = Voronoi(points) # Filtering invalid Voronoi regions - regions = [region for region in vor.regions if not -1 in region] + regions = [region for region in vor.regions if -1 not in region] # Creating Polygons from Voronoi regions polygons = [geometry.Polygon(vor.vertices[regions[i]]) for i in range(len(regions))] diff --git a/gemgis/visualization.py b/gemgis/visualization.py index 5f0e55f7..05f844ad 100644 --- a/gemgis/visualization.py +++ b/gemgis/visualization.py @@ -108,7 +108,7 @@ def create_lines_3d_polydata(gdf: gpd.geodataframe.GeoDataFrame) -> pv.core.poin vertices_list = [list(gdf.geometry[i].coords) for i in range(len(gdf))] # Extracting Z values of all points if gdf has no Z but Z value is provided for each LineString in an additional column - if (all(gdf.has_z == False)) and ('Z' in gdf.columns): + if (not all(gdf.has_z)) and ('Z' in gdf.columns): vertices_list_z = [[vertices_list[j][i] + tuple([gdf['Z'].loc[j]]) for i in range(len(vertices_list[j]))] for j in range(len(vertices_list))] vertices_list = vertices_list_z @@ -1667,7 +1667,7 @@ def create_depth_maps_from_gempy(geo_model, raise TypeError('geo_model must be a GemPy geo_model') # Checking that the model was computed - if all(pd.isna(geo_model.surfaces.df.vertices)) == True and all(pd.isna(geo_model.surfaces.df.edges)) == True: + if all(pd.isna(geo_model.surfaces.df.vertices)) and all(pd.isna(geo_model.surfaces.df.edges)): raise ValueError('Model must be created before depth map extraction') # Extracting surface data_df for all surfaces diff --git a/gemgis/web.py b/gemgis/web.py index e4056526..fd74dedf 100644 --- a/gemgis/web.py +++ b/gemgis/web.py @@ -75,7 +75,6 @@ def load_wms(url: str, """ # Trying to import owslib but returning error if owslib is not installed try: - import owslib from owslib.wms import WebMapService except ModuleNotFoundError: raise ModuleNotFoundError( @@ -547,7 +546,6 @@ def load_wfs(url: str): # -> owslib.wfs.WebFeatureService: # Trying to import owslib but returning error if owslib is not installed try: - import owslib from owslib import util from owslib.wfs import WebFeatureService __all__ = [util] @@ -622,7 +620,6 @@ def load_as_gpd(url: str, # Trying to import owslib but returning error if owslib is not installed try: - import owslib from owslib import util __all__ = [util] except ModuleNotFoundError: @@ -716,7 +713,6 @@ def load_wcs(url: str): # -> owslib.wcs.WebCoverageService: # Trying to import owslib but returning error if owslib is not installed try: - import owslib from owslib import util from owslib.wcs import WebCoverageService __all__ = [util] diff --git a/tests/test_gemgis.py b/tests/test_gemgis.py index a00061a9..2677a8fc 100644 --- a/tests/test_gemgis.py +++ b/tests/test_gemgis.py @@ -27,7 +27,6 @@ import pandas as pd from shapely import geometry import geopandas as gpd -import gempy as gp import gemgis as gg gg.download_gemgis_data.download_tutorial_data(filename='test_gemgis.zip', diff --git a/tests/test_misc.py b/tests/test_misc.py index 11ca3b12..98fc92a9 100644 --- a/tests/test_misc.py +++ b/tests/test_misc.py @@ -370,7 +370,7 @@ def test_stratigraphic_table_list_comprehension(): try: pdf = load_pdf('../docs/getting_started/tutorial/data/test_misc/test_pdf.pdf') - assert type(pdf) == str + assert type(pdf) is str df = get_stratigraphic_data_df(data=pdf, name='Test', @@ -378,7 +378,7 @@ def test_stratigraphic_table_list_comprehension(): formations=formations, return_gdf=False) - assert type(df) == pd.DataFrame + assert type(df) is pd.DataFrame assert len(df) == 7 assert df.loc[0]['Depth'] == 1242 assert df.loc[4]['Depth'] == 1135 @@ -398,7 +398,7 @@ def test_stratigraphic_table_list_comprehension(): try: pdf = load_pdf('../docs/getting_started/tutorial/data/test_misc/test_pdf.pdf') - assert type(pdf) == str + assert type(pdf) is str df = get_stratigraphic_data_df(data=pdf, name='Test', @@ -407,7 +407,7 @@ def test_stratigraphic_table_list_comprehension(): remove_last=True, return_gdf=False) - assert type(df) == pd.DataFrame + assert type(df) is pd.DataFrame assert len(df) == 5 assert df.loc[0]['Depth'] == 1242 assert df.loc[4]['Depth'] == 1135 diff --git a/tests/test_utils.py b/tests/test_utils.py index 15de00bf..76c21d01 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -688,7 +688,7 @@ def test_get_nearest_neighbor(): index = get_nearest_neighbor(x=x, y=np.array([0, 0])) - assert type(index) == np.int64 + assert type(index) is np.int64 assert index == 0 diff --git a/tests/test_web.py b/tests/test_web.py index 254221c5..baa4ba85 100644 --- a/tests/test_web.py +++ b/tests/test_web.py @@ -478,7 +478,7 @@ def test_load_wfs(): wfs = load_wfs(url='https://nibis.lbeg.de/net3/public/ogc.ashx?NodeId=287&Service=WFS&Request=GetCapabilities&') - assert type(wfs) == owslib.feature.wfs100.WebFeatureService_1_0_0 + assert type(wfs) is owslib.feature.wfs100.WebFeatureService_1_0_0 assert wfs.version == '1.0.0' assert wfs.identification.version == '1.0.0' assert wfs.identification.type == 'LBEG' @@ -608,7 +608,7 @@ def test_create_request(): identifier='nw_dgm', form='image/tiff', extent=[292000, 298000, 5626000, 5632000]) - assert type(url) == str + assert type(url) is str assert url == 'https://www.wcs.nrw.de/geobasis/wcs_nw_dgm?REQUEST=GetCoverage&SERVICE=WCS&VERSION=2.0.1&COVERAGEID=nw_dgm&FORMAT=image/tiff&SUBSET=x(292000,298000)&SUBSET=y(5626000,5632000)&OUTFILE=test.tif' with pytest.raises(TypeError): From 654b9acc7e861160b9f0c070033432a263cf3904 Mon Sep 17 00:00:00 2001 From: AlexanderJuestel Date: Mon, 22 Jul 2024 20:11:22 +0200 Subject: [PATCH 11/63] Fix Dependencies --- environment.yml | 6 ++++++ environment_dev.yml | 10 ++++++++-- pyproject.toml | 6 ++++++ requirements.txt | 10 ++++++++-- 4 files changed, 28 insertions(+), 4 deletions(-) diff --git a/environment.yml b/environment.yml index a58f9ddc..65fc12a2 100644 --- a/environment.yml +++ b/environment.yml @@ -6,8 +6,14 @@ dependencies: - python>=3.11 # geopandas will also install numpy, pandas, shapely, pyogrio, and pyproj - geopandas>=1.0.1 + - shapely>=2.0.5 + - pandas>=2.2.2 + - numpy>=2.0.1 + - affine>=2.4.0 + - pyproj>=3.6.1 # rasterio will also install affine - rasterio>=1.3.10 # pyvista also install pooch and matplotlib - pyvista>=0.44.1 + - matplotlib>=3.9.1 - gemgis>=1.1 diff --git a/environment_dev.yml b/environment_dev.yml index c01a36dc..526ebcf8 100644 --- a/environment_dev.yml +++ b/environment_dev.yml @@ -5,10 +5,16 @@ dependencies: - python>=3.11 # geopandas will also install numpy, pandas, shapely, pyogrio, and pyproj - geopandas>=1.0.1 + - shapely>=2.0.5 + - pandas>=2.2.2 + - numpy>=2.0.1 + - affine>=2.4.0 + - pyproj>=3.6.1 # rasterio will also install affine - #- rasterio>=1.3.8 will be installed through rioxarray + - rasterio>=1.3.10 # pyvista also install pooch and matplotlib - #- pyvista>=0.42.2 will be installed through pvgeo + - pyvista>=0.44.1 + - matplotlib>=3.9.1 - gemgis>=1.1 - rioxarray - scipy diff --git a/pyproject.toml b/pyproject.toml index 4d80497d..cd0ee32e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,8 +38,14 @@ classifiers = [ # These dependencies will automatically install other packages like numpy, pandas or matplotlib dependencies = [ 'geopandas', + 'shapely' + 'pandas', + 'numpy', + 'affine', + 'pyproj', 'rasterio', 'pyvista', + 'matplotlib', ] [project.optional-dependencies] diff --git a/requirements.txt b/requirements.txt index 03afcaf4..f37d8f01 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,10 +1,16 @@ # Requirements as of July 2024 -# geopandas will also install numpy, pandas, shapely, fiona, and pyproj +# geopandas will also install numpy, pandas, shapely, pyogrio, and pyproj geopandas>=1.0.1 +shapely>=2.0.5 +pandas>=2.2.2 +numpy>=2.0.1 +pyproj>=3.6.1 # rasterio will also install affine rasterio>=1.3.10 +affine>=2.4.0 # pyvista also install pooch and matplotlib -pyvista>=0.44.1 \ No newline at end of file +pyvista>=0.44.1 +matplotlib>=3.9.1 \ No newline at end of file From 1e9d0d50a8497e85aca4a4e85730932fda601cab Mon Sep 17 00:00:00 2001 From: AlexanderJuestel Date: Mon, 22 Jul 2024 20:19:03 +0200 Subject: [PATCH 12/63] Fix #343 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index cd0ee32e..4a186b5e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,10 +27,10 @@ classifiers = [ 'Development Status :: 5 - Production/Stable', 'Intended Audience :: Science/Research', 'Topic :: Scientific/Engineering :: Information Analysis', - 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.11', + 'Programming Language :: Python :: 3.12', 'License :: OSI Approved :: GNU General Public License v3 (GPLv3)', 'Operating System :: OS Independent', ] From e211542100e7674f9ad56fa527ec321d77614243 Mon Sep 17 00:00:00 2001 From: AlexanderJuestel Date: Mon, 22 Jul 2024 20:32:20 +0200 Subject: [PATCH 13/63] Fix pyproject.toml --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 4a186b5e..8c7c83e4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,7 +38,7 @@ classifiers = [ # These dependencies will automatically install other packages like numpy, pandas or matplotlib dependencies = [ 'geopandas', - 'shapely' + 'shapely', 'pandas', 'numpy', 'affine', From c5fdc1cff53ce9b80da7844ed37a7ff62f00bf75 Mon Sep 17 00:00:00 2001 From: AlexanderJuestel Date: Mon, 22 Jul 2024 20:36:08 +0200 Subject: [PATCH 14/63] Format web.py --- gemgis/web.py | 456 +++++++++++++++++++++++++++++++------------------- 1 file changed, 280 insertions(+), 176 deletions(-) diff --git a/gemgis/web.py b/gemgis/web.py index fd74dedf..96b20a09 100644 --- a/gemgis/web.py +++ b/gemgis/web.py @@ -33,8 +33,7 @@ ############################### -def load_wms(url: str, - version: str = '1.3.0'): # -> owslib.wms.WebMapService: +def load_wms(url: str, version: str = "1.3.0"): # -> owslib.wms.WebMapService: """Loading a WMS Service by URL Parameters @@ -78,47 +77,52 @@ def load_wms(url: str, from owslib.wms import WebMapService except ModuleNotFoundError: raise ModuleNotFoundError( - 'owslib package is not installed. Use pip install owslib to install the latest version') + "owslib package is not installed. Use pip install owslib to install the latest version" + ) # Trying to import requests but returning error if requests is not installed try: import requests except ModuleNotFoundError: raise ModuleNotFoundError( - 'requests package is not installed. Use pip install requests to install the latest version') + "requests package is not installed. Use pip install requests to install the latest version" + ) # Checking if url is of type string if not isinstance(url, str): - raise TypeError('URL must be of type string') + raise TypeError("URL must be of type string") # Checking that the version is provided as string if not isinstance(version, str): - raise TypeError('The WMS Service version must be provided as string') + raise TypeError("The WMS Service version must be provided as string") # Requesting the WMS Service or returning an error if a module may be missing try: - wms = WebMapService(url, - version=version) + wms = WebMapService(url, version=version) return wms except requests.exceptions.SSLError: - print("GemGIS: SSL Error, potentially related to missing module - try:\n\n pip install -U openssl \n\n") + print( + "GemGIS: SSL Error, potentially related to missing module - try:\n\n pip install -U openssl \n\n" + ) raise -def load_as_map(url: str, - version: str, - layer: str, - style: str, - crs: Union[str, dict], - bbox: List[Union[float, int]], - size: List[int], - filetype: str, - transparent: bool = True, - save_image: bool = False, - path: str = None, - overwrite_file: bool = False, - create_directory: bool = False): # -> owslib.util.ResponseWrapper: +def load_as_map( + url: str, + version: str, + layer: str, + style: str, + crs: Union[str, dict], + bbox: List[Union[float, int]], + size: List[int], + filetype: str, + transparent: bool = True, + save_image: bool = False, + path: str = None, + overwrite_file: bool = False, + create_directory: bool = False, +): # -> owslib.util.ResponseWrapper: """Loading a portion of a WMS as array Parameters @@ -197,62 +201,66 @@ def load_as_map(url: str, # Trying to import owslib but returning error if owslib is not installed try: from owslib import util + __all__ = [util] except ModuleNotFoundError: raise ModuleNotFoundError( - 'owslib package is not installed. Use pip install owslib to install the latest version') + "owslib package is not installed. Use pip install owslib to install the latest version" + ) # Checking if the url is of type string if not isinstance(url, str): - raise TypeError('URL must be of type string') + raise TypeError("URL must be of type string") # Checking that the version is provided as string if not isinstance(version, str): - raise TypeError('The WMS Service version must be provided as string') + raise TypeError("The WMS Service version must be provided as string") # Checking if the layer name is of type string if not isinstance(layer, str): - raise TypeError('Layers must be of type string') + raise TypeError("Layers must be of type string") # Checking if the style is of type string if not isinstance(style, str): - raise TypeError('Style must be of type string') + raise TypeError("Style must be of type string") # Checking if the crs is of type string or dict if not isinstance(crs, (str, dict)): - raise TypeError('CRS must be of type str or dict') + raise TypeError("CRS must be of type str or dict") # Checking if bbox is of type list if not isinstance(bbox, list): - raise TypeError('Bbox must be of type list') + raise TypeError("Bbox must be of type list") # Checking the length of the bbox list if len(bbox) != 4: - raise ValueError('Provide minx, maxx, miny, and maxy values for the bounding box') + raise ValueError( + "Provide minx, maxx, miny, and maxy values for the bounding box" + ) # Checking if size is of type list if not isinstance(size, list): - raise TypeError('Size must be of type list') + raise TypeError("Size must be of type list") # Checking the length of the size list if len(size) != 2: - raise ValueError('Provide only a x- and y-value for the size') + raise ValueError("Provide only a x- and y-value for the size") # Checking if file type is of type string if not isinstance(filetype, str): - raise TypeError('File type must be of type string') + raise TypeError("File type must be of type string") # Checking if the transparency is of type book if not isinstance(transparent, bool): - raise TypeError('transparent must be of type bool') + raise TypeError("transparent must be of type bool") # Checking if save_image is of type bool if not isinstance(save_image, bool): - raise TypeError('Save_image must be of type bool') + raise TypeError("Save_image must be of type bool") # Checking is path is of type string if not isinstance(path, (str, type(None))): - raise TypeError('Path must be of type string') + raise TypeError("Path must be of type string") if isinstance(path, str): # Getting the absolute path @@ -271,49 +279,60 @@ def load_as_map(url: str, if create_directory: os.makedirs(path_dir) else: - raise LookupError('Directory not found. Pass create_directory=True to create a new directory') + raise LookupError( + "Directory not found. Pass create_directory=True to create a new directory" + ) if not overwrite_file: if os.path.exists(path): raise FileExistsError( - "The file already exists. Pass overwrite_file=True to overwrite the existing file") + "The file already exists. Pass overwrite_file=True to overwrite the existing file" + ) # Loading WMS Service wms = load_wms(url, version=version) # Creating map object - wms_map = wms.getmap(layers=[layer], styles=[style], srs=crs, bbox=tuple([bbox[0], bbox[2], bbox[1], bbox[3]]), - size=tuple(size), format=filetype, - transparent=transparent) + wms_map = wms.getmap( + layers=[layer], + styles=[style], + srs=crs, + bbox=tuple([bbox[0], bbox[2], bbox[1], bbox[3]]), + size=tuple(size), + format=filetype, + transparent=transparent, + ) # Saving an image if save_image is true and a path is provided if save_image: if isinstance(path, str): - out = open(path, 'wb') + out = open(path, "wb") out.write(wms_map.read()) out.close() else: - raise ValueError('Path is missing') + raise ValueError("Path is missing") else: if isinstance(path, str): - raise ValueError('Save_image was set to False') + raise ValueError("Save_image was set to False") return wms_map -def load_as_array(url: str, - version: str, - layer: str, - style: str, - crs: Union[str, dict], - bbox: List[Union[float, int]], - size: List[int], - filetype: str, - transparent: bool = True, - save_image: bool = False, - path: str = None, - overwrite_file: bool = False, - create_directory: bool = False) -> np.ndarray: +def load_as_array( + url: str, + version: str, + layer: str, + style: str, + crs: Union[str, dict], + bbox: List[Union[float, int]], + size: List[int], + filetype: str, + transparent: bool = True, + save_image: bool = False, + path: str = None, + overwrite_file: bool = False, + create_directory: bool = False, +) -> np.ndarray: """Loading a portion of a WMS as array Parameters @@ -400,68 +419,74 @@ def load_as_array(url: str, # Trying to import owslib but returning error if owslib is not installed try: from owslib import util + __all__ = [util] except ModuleNotFoundError: raise ModuleNotFoundError( - 'owslib package is not installed. Use pip install owslib to install the latest version') + "owslib package is not installed. Use pip install owslib to install the latest version" + ) # Trying to import matplotlib but returning error if matplotlib is not installed try: import matplotlib.pyplot as plt except ModuleNotFoundError: - raise ModuleNotFoundError('Matplotlib package is not installed. Use pip install matplotlib to install the latest version') + raise ModuleNotFoundError( + "Matplotlib package is not installed. Use pip install matplotlib to install the latest version" + ) # Checking if the url is of type string if not isinstance(url, str): - raise TypeError('URL must be of type string') + raise TypeError("URL must be of type string") # Checking that the version is provided as string if not isinstance(version, str): - raise TypeError('The WMS Service version must be provided as string') + raise TypeError("The WMS Service version must be provided as string") # Checking if the layer name is of type string if not isinstance(layer, str): - raise TypeError('Layers must be of type string') + raise TypeError("Layers must be of type string") # Checking if the style is of type string if not isinstance(style, str): - raise TypeError('Style must be of type string') + raise TypeError("Style must be of type string") # Checking if the crs is of type string or dict if not isinstance(crs, (str, dict)): - raise TypeError('CRS must be of type str or dict') + raise TypeError("CRS must be of type str or dict") # Checking if bbox is of type list if not isinstance(bbox, list): - raise TypeError('Bbox must be of type list') + raise TypeError("Bbox must be of type list") # Checking the length of the bbox list if len(bbox) != 4: - raise ValueError('Provide minx, maxx, miny and maxy values for the bounding box') + raise ValueError( + "Provide minx, maxx, miny and maxy values for the bounding box" + ) # Checking if size is of type list if not isinstance(size, list): - raise TypeError('Size must be of type list') + raise TypeError("Size must be of type list") # Checking the length of the size list if len(size) != 2: - raise ValueError('Provide only a x- and y-value for the size') + raise ValueError("Provide only a x- and y-value for the size") # Checking if file type is of type string if not isinstance(filetype, str): - raise TypeError('File type must be of type string') + raise TypeError("File type must be of type string") # Checking if the transparency is of type book if not isinstance(transparent, bool): - raise TypeError('transparent must be of type bool') + raise TypeError("transparent must be of type bool") # Checking if save_image is of type bool if not isinstance(save_image, bool): - raise TypeError('Save_image must be of type bool') + raise TypeError("Save_image must be of type bool") # Checking is path is of type string if not isinstance(path, (str, type(None))): - raise TypeError('Path must be of type string') + raise TypeError("Path must be of type string") if isinstance(path, str): # Getting the absolute path @@ -480,25 +505,30 @@ def load_as_array(url: str, if create_directory: os.makedirs(path_dir) else: - raise LookupError('Directory not found. Pass create_directory=True to create a new directory') + raise LookupError( + "Directory not found. Pass create_directory=True to create a new directory" + ) if not overwrite_file: if os.path.exists(path): raise FileExistsError( - "The file already exists. Pass overwrite_file=True to overwrite the existing file") + "The file already exists. Pass overwrite_file=True to overwrite the existing file" + ) # Creating WMS map object - wms_map = load_as_map(url=url, - version=version, - layer=layer, - style=style, - crs=crs, - bbox=bbox, - size=size, - filetype=filetype, - transparent=transparent, - save_image=save_image, - path=path) + wms_map = load_as_map( + url=url, + version=version, + layer=layer, + style=style, + crs=crs, + bbox=bbox, + size=size, + filetype=filetype, + transparent=transparent, + save_image=save_image, + path=path, + ) # Converting WMS map object to array maps = io.BytesIO(wms_map.read()) @@ -548,21 +578,24 @@ def load_wfs(url: str): # -> owslib.wfs.WebFeatureService: try: from owslib import util from owslib.wfs import WebFeatureService + __all__ = [util] except ModuleNotFoundError: raise ModuleNotFoundError( - 'owslib package is not installed. Use pip install owslib to install the latest version') + "owslib package is not installed. Use pip install owslib to install the latest version" + ) # Trying to import requests but returning error if requests is not installed try: import requests except ModuleNotFoundError: raise ModuleNotFoundError( - 'requests package is not installed. Use pip install requests to install the latest version') + "requests package is not installed. Use pip install requests to install the latest version" + ) # Checking if url is of type string if not isinstance(url, str): - raise TypeError('URL must be of type string') + raise TypeError("URL must be of type string") # Requesting the WMS Service or returning an error if a module may be missing try: @@ -571,14 +604,15 @@ def load_wfs(url: str): # -> owslib.wfs.WebFeatureService: return wfs except requests.exceptions.SSLError: - print("GemGIS: SSL Error, potentially related to missing module - try:\n\n pip install -U openssl \n\n") + print( + "GemGIS: SSL Error, potentially related to missing module - try:\n\n pip install -U openssl \n\n" + ) raise -def load_as_gpd(url: str, - typename: str = None, - outputformat: str = None - ) -> gpd.geodataframe.GeoDataFrame: +def load_as_gpd( + url: str, typename: str = None, outputformat: str = None +) -> gpd.geodataframe.GeoDataFrame: """Requesting data from a WFS Service Parameters @@ -621,29 +655,32 @@ def load_as_gpd(url: str, # Trying to import owslib but returning error if owslib is not installed try: from owslib import util + __all__ = [util] except ModuleNotFoundError: raise ModuleNotFoundError( - 'owslib package is not installed. Use pip install owslib to install the latest version') + "owslib package is not installed. Use pip install owslib to install the latest version" + ) # Trying to import requests but returning error if requests is not installed try: import requests except ModuleNotFoundError: raise ModuleNotFoundError( - 'requests package is not installed. Use pip install requests to install the latest version') + "requests package is not installed. Use pip install requests to install the latest version" + ) # Checking that the url is of type string if not isinstance(url, str): - raise TypeError('URL must be of type string') + raise TypeError("URL must be of type string") # Checking that the typename is of type string or None if not isinstance(typename, (str, type(None))): - raise TypeError('Name of the feature must be of type string') + raise TypeError("Name of the feature must be of type string") # Checking that the outputformat is of type string if not isinstance(outputformat, (str, type(None))): - raise TypeError('The output format must be of type string') + raise TypeError("The output format must be of type string") # Loading the wfs layer wfs = load_wfs(url=url) @@ -652,19 +689,26 @@ def load_as_gpd(url: str, if not typename: layer = list(wfs.contents)[0] else: - raise ValueError('No layer available') + raise ValueError("No layer available") # If the output format is not provided, take the last if not outputformat: - if wfs.getOperationByName('GetFeature').formatOptions == ['{http://www.opengis.net/wfs}GML2']: - outputformat = 'xml/gml2' + if wfs.getOperationByName("GetFeature").formatOptions == [ + "{http://www.opengis.net/wfs}GML2" + ]: + outputformat = "xml/gml2" # Specify the parameters for fetching the data - params = dict(service='WFS', version=wfs.version, request='GetFeature', - typeName=layer, outputFormat=outputformat) + params = dict( + service="WFS", + version=wfs.version, + request="GetFeature", + typeName=layer, + outputFormat=outputformat, + ) # Parse the URL with parameters - q = requests.Request('GET', url, params=params).prepare().url + q = requests.Request("GET", url, params=params).prepare().url # Read data from request feature = gpd.read_file(q) @@ -715,14 +759,16 @@ def load_wcs(url: str): # -> owslib.wcs.WebCoverageService: try: from owslib import util from owslib.wcs import WebCoverageService + __all__ = [util] except ModuleNotFoundError: raise ModuleNotFoundError( - 'owslib package is not installed. Use pip install owslib to install the latest version') + "owslib package is not installed. Use pip install owslib to install the latest version" + ) # Checking if URL is of type string if not isinstance(url, str): - raise TypeError('URL must be of type string') + raise TypeError("URL must be of type string") # Loading the WCS Layer wcs = WebCoverageService(url) @@ -730,12 +776,14 @@ def load_wcs(url: str): # -> owslib.wcs.WebCoverageService: return wcs -def create_request(wcs_url: str, - version: str, - identifier: str, - form: str, - extent: List[Union[float, int]], - name: str = 'test.tif') -> str: +def create_request( + wcs_url: str, + version: str, + identifier: str, + form: str, + extent: List[Union[float, int]], + name: str = "test.tif", +) -> str: """Creating URL to request data from WCS Server Parameters @@ -792,48 +840,76 @@ def create_request(wcs_url: str, # Trying to import owslib but returning error if owslib is not installed try: from owslib import util + __all__ = [util] except ModuleNotFoundError: raise ModuleNotFoundError( - 'owslib package is not installed. Use pip install owslib to install the latest version') + "owslib package is not installed. Use pip install owslib to install the latest version" + ) # Checking that the URL is of type string if not isinstance(wcs_url, str): - raise TypeError('URL must be of type string') + raise TypeError("URL must be of type string") # Checking that the version number is of type string if not isinstance(version, str): - raise TypeError('WCS Version must be of type string') + raise TypeError("WCS Version must be of type string") # Checking that the identifier is of type string if not isinstance(identifier, str): - raise TypeError('Layer Name/Identifier must be of type string') + raise TypeError("Layer Name/Identifier must be of type string") # Checking that the format is of type string if not isinstance(form, str): - raise TypeError('Download format must be of type string') + raise TypeError("Download format must be of type string") # Checking that the extent is of type list if not isinstance(extent, list): - raise TypeError('Extent must be provided as list of minx, maxx, miny, maxy') + raise TypeError("Extent must be provided as list of minx, maxx, miny, maxy") # Checking the length of the extent if len(extent) != 4: - raise ValueError('Extent must be provided as list of minx, maxx, miny, maxy') + raise ValueError("Extent must be provided as list of minx, maxx, miny, maxy") # Create URL for Request - url = wcs_url + '?' + 'REQUEST=GetCoverage' + '&' + 'SERVICE=WCS' + '&' + 'VERSION=' + str(version) + '&' + \ - 'COVERAGEID=' + identifier + '&' + 'FORMAT=' + form + '&' + \ - 'SUBSET=x(' + str(extent[0]) + ',' + str(extent[1]) + ')' + '&' + \ - 'SUBSET=y(' + str(extent[2]) + ',' + str(extent[3]) + ')' + '&' + 'OUTFILE=' + name + url = ( + wcs_url + + "?" + + "REQUEST=GetCoverage" + + "&" + + "SERVICE=WCS" + + "&" + + "VERSION=" + + str(version) + + "&" + + "COVERAGEID=" + + identifier + + "&" + + "FORMAT=" + + form + + "&" + + "SUBSET=x(" + + str(extent[0]) + + "," + + str(extent[1]) + + ")" + + "&" + + "SUBSET=y(" + + str(extent[2]) + + "," + + str(extent[3]) + + ")" + + "&" + + "OUTFILE=" + + name + ) return url -def load_as_file(url: str, - path: str, - overwrite_file: bool = False, - create_directory: bool = False): +def load_as_file( + url: str, path: str, overwrite_file: bool = False, create_directory: bool = False +): """Executing WCS request and downloading file into specified folder Parameters @@ -882,24 +958,28 @@ def load_as_file(url: str, # Trying to import owslib but returning error if owslib is not installed try: from owslib import util + __all__ = [util] except ModuleNotFoundError: raise ModuleNotFoundError( - 'owslib package is not installed. Use pip install owslib to install the latest version') + "owslib package is not installed. Use pip install owslib to install the latest version" + ) # Trying to import urllib but returning error if urllib is not installed try: import urllib except ModuleNotFoundError: - raise ModuleNotFoundError('urllib package is not installed. Use pip install urllib to install the latest version') + raise ModuleNotFoundError( + "urllib package is not installed. Use pip install urllib to install the latest version" + ) # Checking that the url is of type string if not isinstance(url, str): - raise TypeError('URL must be of type string') + raise TypeError("URL must be of type string") # Checking that the path is of type string if not isinstance(path, str): - raise TypeError('Path must be of type string') + raise TypeError("Path must be of type string") # Getting the absolute path path = os.path.abspath(path=path) @@ -916,25 +996,30 @@ def load_as_file(url: str, if create_directory: os.makedirs(path_dir) else: - raise LookupError('Directory not found. Pass create_directory=True to create a new directory') + raise LookupError( + "Directory not found. Pass create_directory=True to create a new directory" + ) if not overwrite_file: if os.path.exists(path): raise FileExistsError( - "The file already exists. Pass overwrite_file=True to overwrite the existing file") + "The file already exists. Pass overwrite_file=True to overwrite the existing file" + ) # Executing request and downloading files to the specified folder urllib.request.urlretrieve(url, path) -def load_as_files(wcs_url: str, - version: str, - identifier: str, - form: str, - extent: List[Union[float, int]], - size: int, - path: str = '', - create_directory: bool = False): +def load_as_files( + wcs_url: str, + version: str, + identifier: str, + form: str, + extent: List[Union[float, int]], + size: int, + path: str = "", + create_directory: bool = False, +): """Executing WCS requests and downloading files into specified folder Parameters @@ -992,44 +1077,48 @@ def load_as_files(wcs_url: str, # Trying to import owslib but returning error if owslib is not installed try: from owslib import util + __all__ = [util] except ModuleNotFoundError: raise ModuleNotFoundError( - 'owslib package is not installed. Use pip install owslib to install the latest version') + "owslib package is not installed. Use pip install owslib to install the latest version" + ) # Trying to import tqdm but returning error if tqdm is not installed try: from tqdm import tqdm except ModuleNotFoundError: - raise ModuleNotFoundError('tqdm package is not installed. Use pip install tqdm to install the latest version') + raise ModuleNotFoundError( + "tqdm package is not installed. Use pip install tqdm to install the latest version" + ) # Checking that the URL is of type string if not isinstance(wcs_url, str): - raise TypeError('URL must be of type string') + raise TypeError("URL must be of type string") # Checking that the version number is of type string if not isinstance(version, str): - raise TypeError('WCS Version must be of type string') + raise TypeError("WCS Version must be of type string") # Checking that the identifier is of type string if not isinstance(identifier, str): - raise TypeError('Layer Name/Identifier must be of type string') + raise TypeError("Layer Name/Identifier must be of type string") # Checking that the format is of type string if not isinstance(form, str): - raise TypeError('Download format must be of type string') + raise TypeError("Download format must be of type string") # Checking that the extent is of type list if not isinstance(extent, list): - raise TypeError('Extent must be provided as list of minx, maxx, miny, maxy') + raise TypeError("Extent must be provided as list of minx, maxx, miny, maxy") # Checking the length of the extent if len(extent) != 4: - raise ValueError('Extent must be provided as list of minx, maxx, miny, maxy') + raise ValueError("Extent must be provided as list of minx, maxx, miny, maxy") # Checking that the provided size of each tile is of type int if not isinstance(size, int): - raise TypeError('Tile size must be provided as int') + raise TypeError("Tile size must be provided as int") # Calculating the x Extent x = extent[1] - extent[0] @@ -1038,41 +1127,56 @@ def load_as_files(wcs_url: str, y = extent[3] - extent[2] # Printing the extent and number of tiles that are going to be downloaded - print('Extent X: ', x, ' m') - print('Extent Y: ', y, ' m') - print('Number of tiles in X directions: ', int(x / size)) - print('Number of tiles in Y directions: ', int(y / size)) - print('Total Number of Tiles: ', int(x / size) * int(y / size)) + print("Extent X: ", x, " m") + print("Extent Y: ", y, " m") + print("Number of tiles in X directions: ", int(x / size)) + print("Number of tiles in Y directions: ", int(y / size)) + print("Total Number of Tiles: ", int(x / size) * int(y / size)) # Loop through each tile and download data for i in tqdm(range(int(x / size))): for j in range(int(y / size)): # Download data only if the tile does not exist yet - if not os.path.exists(path + 'tile_%d_%d_%d_%d.tif' % - (extent[0] + i * size, - extent[0] + (i + 1) * size, - extent[2] + j * size, - extent[2] + (j + 1) * size)): + if not os.path.exists( + path + + "tile_%d_%d_%d_%d.tif" + % ( + extent[0] + i * size, + extent[0] + (i + 1) * size, + extent[2] + j * size, + extent[2] + (j + 1) * size, + ) + ): # Create URL request - url = create_request(wcs_url=wcs_url, - version=version, - identifier=identifier, - form=form, - extent=[extent[0] + i * size, - extent[0] + (i + 1) * size, - extent[2] + j * size, - extent[2] + (j + 1) * size], - name=path) + url = create_request( + wcs_url=wcs_url, + version=version, + identifier=identifier, + form=form, + extent=[ + extent[0] + i * size, + extent[0] + (i + 1) * size, + extent[2] + j * size, + extent[2] + (j + 1) * size, + ], + name=path, + ) print(url) # Load file - load_as_file(url=url, - path=path + 'tile_%d_%d_%d_%d.tif' % (extent[0] + i * size, - extent[0] + (i + 1) * size, - extent[2] + j * size, - extent[2] + (j + 1) * size), - create_directory=create_directory) + load_as_file( + url=url, + path=path + + "tile_%d_%d_%d_%d.tif" + % ( + extent[0] + i * size, + extent[0] + (i + 1) * size, + extent[2] + j * size, + extent[2] + (j + 1) * size, + ), + create_directory=create_directory, + ) else: - print('All tiles have already been downloaded') + print("All tiles have already been downloaded") pass From c13378cd3b323652e6d6900a040399f8d0bf1d27 Mon Sep 17 00:00:00 2001 From: AlexanderJuestel Date: Mon, 22 Jul 2024 20:44:44 +0200 Subject: [PATCH 15/63] Format visualization.py --- gemgis/visualization.py | 3023 ++++++++++++++++++++++----------------- 1 file changed, 1684 insertions(+), 1339 deletions(-) diff --git a/gemgis/visualization.py b/gemgis/visualization.py index 05f844ad..b1aff416 100644 --- a/gemgis/visualization.py +++ b/gemgis/visualization.py @@ -45,7 +45,9 @@ ############################################################## -def create_lines_3d_polydata(gdf: gpd.geodataframe.GeoDataFrame) -> pv.core.pointset.PolyData: +def create_lines_3d_polydata( + gdf: gpd.geodataframe.GeoDataFrame, +) -> pv.core.pointset.PolyData: """Creating lines with z-component for the plotting with PyVista Parameters @@ -98,19 +100,24 @@ def create_lines_3d_polydata(gdf: gpd.geodataframe.GeoDataFrame) -> pv.core.poin # Checking that the contour lines are a GeoDataFrame if not isinstance(gdf, gpd.geodataframe.GeoDataFrame): - raise TypeError('Line Object must be of type GeoDataFrame') + raise TypeError("Line Object must be of type GeoDataFrame") # Checking that all elements of the GeoDataFrame are of geom_type LineString if not all(shapely.get_type_id(gdf.geometry) == 1): - raise TypeError('All Shapely objects of the GeoDataFrame must be LineStrings') + raise TypeError("All Shapely objects of the GeoDataFrame must be LineStrings") # Creating list of points vertices_list = [list(gdf.geometry[i].coords) for i in range(len(gdf))] # Extracting Z values of all points if gdf has no Z but Z value is provided for each LineString in an additional column - if (not all(gdf.has_z)) and ('Z' in gdf.columns): - vertices_list_z = [[vertices_list[j][i] + tuple([gdf['Z'].loc[j]]) for i in range(len(vertices_list[j]))] for j - in range(len(vertices_list))] + if (not all(gdf.has_z)) and ("Z" in gdf.columns): + vertices_list_z = [ + [ + vertices_list[j][i] + tuple([gdf["Z"].loc[j]]) + for i in range(len(vertices_list[j])) + ] + for j in range(len(vertices_list)) + ] vertices_list = vertices_list_z # Creating array of points @@ -131,9 +138,11 @@ def create_lines_3d_polydata(gdf: gpd.geodataframe.GeoDataFrame) -> pv.core.poin return poly -def create_lines_3d_linestrings(gdf: gpd.geodataframe.GeoDataFrame, - dem: Union[rasterio.io.DatasetReader, np.ndarray], - extent: List[Union[int, float]] = None) -> gpd.geodataframe.GeoDataFrame: +def create_lines_3d_linestrings( + gdf: gpd.geodataframe.GeoDataFrame, + dem: Union[rasterio.io.DatasetReader, np.ndarray], + extent: List[Union[int, float]] = None, +) -> gpd.geodataframe.GeoDataFrame: """Creating lines with z-component (LineString Z) Parameters @@ -194,43 +203,45 @@ def create_lines_3d_linestrings(gdf: gpd.geodataframe.GeoDataFrame, # Checking that gdf is of type GepDataFrame if not isinstance(gdf, gpd.geodataframe.GeoDataFrame): - raise TypeError('Loaded object is not a GeoDataFrame') + raise TypeError("Loaded object is not a GeoDataFrame") # Check that all entries of the gdf are of type Point if not all(shapely.get_type_id(gdf.geometry) == 1): - raise TypeError('All GeoDataFrame entries must be of geom_type LineString') + raise TypeError("All GeoDataFrame entries must be of geom_type LineString") # Checking that the dem is a np.ndarray or rasterio object if not isinstance(dem, (np.ndarray, rasterio.io.DatasetReader)): - raise TypeError('DEM must be a numpy.ndarray or rasterio object') + raise TypeError("DEM must be a numpy.ndarray or rasterio object") # Checking that the extent is of type list if isinstance(dem, np.ndarray) and not isinstance(extent, list): - raise TypeError('Extent must be of type list') + raise TypeError("Extent must be of type list") # Add index to line for later merging again - gdf['index_lines'] = gdf.index + gdf["index_lines"] = gdf.index # Extracting X,Y,Z coordinates from LineStrings - gdf_xyz = extract_xyz(gdf=gdf, - dem=dem, - extent=extent) + gdf_xyz = extract_xyz(gdf=gdf, dem=dem, extent=extent) # Creating list of LineStrings with Z component - list_linestrings = [LineString(gdf_xyz[gdf_xyz['index_lines'] == i][['X', 'Y', 'Z']].values) for i in - gdf_xyz['index_lines'].unique()] + list_linestrings = [ + LineString(gdf_xyz[gdf_xyz["index_lines"] == i][["X", "Y", "Z"]].values) + for i in gdf_xyz["index_lines"].unique() + ] # Creating GeoDataFrame with LineStrings - gdf_3d = gpd.GeoDataFrame(geometry=list_linestrings, - data=gdf, - crs=gdf.crs).drop('index_lines', axis=1) + gdf_3d = gpd.GeoDataFrame(geometry=list_linestrings, data=gdf, crs=gdf.crs).drop( + "index_lines", axis=1 + ) return gdf_3d -def create_dem_3d(dem: Union[rasterio.io.DatasetReader, np.ndarray], - extent: List[Union[int, float]] = None, - res: int = 1) -> pv.core.pointset.StructuredGrid: +def create_dem_3d( + dem: Union[rasterio.io.DatasetReader, np.ndarray], + extent: List[Union[int, float]] = None, + res: int = 1, +) -> pv.core.pointset.StructuredGrid: """Plotting the dem in 3D with PyVista Parameters @@ -290,11 +301,11 @@ def create_dem_3d(dem: Union[rasterio.io.DatasetReader, np.ndarray], # Checking if dem is a rasterio object or NumPy array if not isinstance(dem, (rasterio.io.DatasetReader, np.ndarray)): - raise TypeError('DEM must be a rasterio object') + raise TypeError("DEM must be a rasterio object") # Checking if the extent is of type list if not isinstance(extent, (list, type(None))): - raise TypeError('Extent must be of type list') + raise TypeError("Extent must be of type list") # Converting rasterio object to array if isinstance(dem, rasterio.io.DatasetReader): @@ -307,11 +318,11 @@ def create_dem_3d(dem: Union[rasterio.io.DatasetReader, np.ndarray], # Checking if the extent is of type list if not isinstance(extent, list): - raise TypeError('Extent must be of type list') + raise TypeError("Extent must be of type list") # Checking that all values are either ints or floats if not all(isinstance(n, (int, float)) for n in extent): - raise TypeError('Bound values must be of type int or float') + raise TypeError("Bound values must be of type int or float") # Creating arrays for meshgrid creation x = np.arange(extent[0], extent[1], res) @@ -382,18 +393,18 @@ def create_points_3d(gdf: gpd.geodataframe.GeoDataFrame) -> pv.core.pointset.Pol # Checking if points is of type GeoDataFrame if not isinstance(gdf, (gpd.geodataframe.GeoDataFrame, pd.DataFrame)): - raise TypeError('Points must be of type GeoDataFrame or DataFrame') + raise TypeError("Points must be of type GeoDataFrame or DataFrame") # Checking if all necessary columns are in the GeoDataFrame - if not {'X', 'Y', 'Z'}.issubset(gdf.columns): - raise ValueError('Points are missing columns, XYZ needed') + if not {"X", "Y", "Z"}.issubset(gdf.columns): + raise ValueError("Points are missing columns, XYZ needed") # Checking that all elements of the GeoDataFrame are of geom_type Point if not all(shapely.get_type_id(gdf.geometry) == 0): - raise TypeError('All Shapely objects of the GeoDataFrame must be Points') + raise TypeError("All Shapely objects of the GeoDataFrame must be Points") # Creating PyVista PolyData - points_mesh = pv.PolyData(gdf[['X', 'Y', 'Z']].to_numpy()) + points_mesh = pv.PolyData(gdf[["X", "Y", "Z"]].to_numpy()) return points_mesh @@ -402,9 +413,11 @@ def create_points_3d(gdf: gpd.geodataframe.GeoDataFrame) -> pv.core.pointset.Pol ################################## -def create_mesh_from_cross_section(linestring: shapely.geometry.linestring.LineString, - zmax: Union[float, int], - zmin: Union[float, int]) -> pv.core.pointset.PolyData: +def create_mesh_from_cross_section( + linestring: shapely.geometry.linestring.LineString, + zmax: Union[float, int], + zmin: Union[float, int], +) -> pv.core.pointset.PolyData: """Creating a PyVista Mesh from one cross section Parameters @@ -462,15 +475,15 @@ def create_mesh_from_cross_section(linestring: shapely.geometry.linestring.LineS # Checking that the LineString is a Shapely LineString if not isinstance(linestring, shapely.geometry.linestring.LineString): - raise TypeError('Profile Trace must be provided as Shapely LineString') + raise TypeError("Profile Trace must be provided as Shapely LineString") # Checking that zmax is an int or float if not isinstance(zmax, (int, float, np.int64)): - raise TypeError('Maximum vertical extent zmax must be provided as int or float') + raise TypeError("Maximum vertical extent zmax must be provided as int or float") # Checking that zmax is an int or float if not isinstance(zmin, (int, float, np.int64)): - raise TypeError('Minimum vertical extent zmax must be provided as int or float') + raise TypeError("Minimum vertical extent zmax must be provided as int or float") # Getting the number of vertices of the LineString n = len(list(linestring.coords)) @@ -492,14 +505,16 @@ def create_mesh_from_cross_section(linestring: shapely.geometry.linestring.LineS # i --- i+1 faces = np.array( - [[3, i, i + 1, i + n] for i in range(n - 1)] + [[3, i + n + 1, i + n, i + 1] for i in range(n - 1)]) + [[3, i, i + 1, i + n] for i in range(n - 1)] + + [[3, i + n + 1, i + n, i + 1] for i in range(n - 1)] + ) # L should be the normalized to 1 cumulative sum of the segment lengths data = np.linalg.norm(coords[1:] - coords[:-1], axis=1).cumsum() data /= data[-1] uv = np.zeros((2 * n, 2)) uv[1:n, 0] = data - uv[n + 1:, 0] = data + uv[n + 1 :, 0] = data uv[:, 1] = np.repeat([0, 1], n) # Creating PyVista PolyData @@ -513,12 +528,16 @@ def create_mesh_from_cross_section(linestring: shapely.geometry.linestring.LineS else: surface.active_texture_coordinates = uv except AttributeError: - raise ImportError("Please make sure you are using a compatible version of PyVista") + raise ImportError( + "Please make sure you are using a compatible version of PyVista" + ) return surface -def create_meshes_from_cross_sections(gdf: gpd.geodataframe.GeoDataFrame) -> List[pv.core.pointset.PolyData]: +def create_meshes_from_cross_sections( + gdf: gpd.geodataframe.GeoDataFrame, +) -> List[pv.core.pointset.PolyData]: """Creating PyVista Meshes from multiple cross section Parameters @@ -574,24 +593,29 @@ def create_meshes_from_cross_sections(gdf: gpd.geodataframe.GeoDataFrame) -> Lis # Checking that the data is provided as GeoDataFrame if not isinstance(gdf, gpd.geodataframe.GeoDataFrame): - raise TypeError('Data must be provided as GeoDataFrame') + raise TypeError("Data must be provided as GeoDataFrame") # Checking that all elements of the GeoDataFrame are Shapely LineStrings if not all(shapely.get_type_id(gdf.geometry) == 1): - raise TypeError('All elements must be of type LineString') + raise TypeError("All elements must be of type LineString") # Checking that zmax is in the gdf - if 'zmax' not in gdf: - raise ValueError('zmax is not in the gdf') + if "zmax" not in gdf: + raise ValueError("zmax is not in the gdf") # Checking that zmin is in the gdf - if 'zmin' not in gdf: - raise ValueError('zmin is not in the gdf') + if "zmin" not in gdf: + raise ValueError("zmin is not in the gdf") # Creating the meshes - meshes = [create_mesh_from_cross_section(linestring=gdf.loc[i].geometry, - zmax=gdf.loc[i]['zmax'], - zmin=gdf.loc[i]['zmin']) for i in range(len(gdf))] + meshes = [ + create_mesh_from_cross_section( + linestring=gdf.loc[i].geometry, + zmax=gdf.loc[i]["zmax"], + zmin=gdf.loc[i]["zmin"], + ) + for i in range(len(gdf)) + ] return meshes @@ -600,9 +624,9 @@ def create_meshes_from_cross_sections(gdf: gpd.geodataframe.GeoDataFrame) -> Lis ###################################################### -def read_raster(path=str, - nodata_val: Union[float, int] = None, - name: str = 'Elevation [m]') -> pv.core.pointset.PolyData: +def read_raster( + path=str, nodata_val: Union[float, int] = None, name: str = "Elevation [m]" +) -> pv.core.pointset.PolyData: """Reading a raster and returning a mesh Parameters @@ -661,11 +685,12 @@ def read_raster(path=str, import rioxarray as rxr except ModuleNotFoundError: raise ModuleNotFoundError( - 'rioxarray package is not installed. Use pip install rioxarray to install the latest version') + "rioxarray package is not installed. Use pip install rioxarray to install the latest version" + ) # Checking that the path is of type string if not isinstance(path, str): - raise TypeError('Path must be of type string') + raise TypeError("Path must be of type string") # Getting the absolute path path = os.path.abspath(path=path) @@ -676,15 +701,15 @@ def read_raster(path=str, # Checking that the file exists if not os.path.exists(path): - raise FileNotFoundError('File not found') + raise FileNotFoundError("File not found") # Checking that the nodata value is of type float or int if not isinstance(nodata_val, (float, int, type(None))): - raise TypeError('Nodata_val must be of type float or int') + raise TypeError("Nodata_val must be of type float or int") # Checking that the name of the array is provided as string if not isinstance(name, str): - raise TypeError('The name of the data array must be provided as string') + raise TypeError("The name of the data array must be provided as string") # Reading in the data data = rxr.open_rasterio(path) @@ -711,7 +736,7 @@ def read_raster(path=str, values[nans] = np.nan # Creating meshgrid - xx, yy = np.meshgrid(data['x'], data['y']) + xx, yy = np.meshgrid(data["x"], data["y"]) # Setting zz values zz = np.zeros_like(xx) @@ -720,7 +745,7 @@ def read_raster(path=str, mesh = pv.StructuredGrid(xx, yy, zz) # Assign Elevation Values - mesh[name] = values.ravel(order='F') + mesh[name] = values.ravel(order="F") return mesh @@ -793,19 +818,23 @@ def convert_to_rgb(array: np.ndarray) -> np.ndarray: # Checking that the array is a NumPy nd.array if not isinstance(array, np.ndarray): - raise TypeError('Input data must be of type NumPy nd.array') + raise TypeError("Input data must be of type NumPy nd.array") # Converting the array values to RGB values - array_stacked = (np.dstack((array[:, :, 0], array[:, :, 1], array[:, :, 2])) * 255.999).astype(np.uint8) + array_stacked = ( + np.dstack((array[:, :, 0], array[:, :, 1], array[:, :, 2])) * 255.999 + ).astype(np.uint8) return array_stacked -def drape_array_over_dem(array: np.ndarray, - dem: Union[rasterio.io.DatasetReader, np.ndarray], - extent: List[Union[float, int]] = None, - zmax: Union[float, int] = 10000, - resize_array: bool =True): +def drape_array_over_dem( + array: np.ndarray, + dem: Union[rasterio.io.DatasetReader, np.ndarray], + extent: List[Union[float, int]] = None, + zmax: Union[float, int] = 10000, + resize_array: bool = True, +): """Creating grid and texture to drape array over a digital elevation model Parameters @@ -907,19 +936,25 @@ def drape_array_over_dem(array: np.ndarray, # Checking that the map data is of type np.ndarray if not isinstance(array, np.ndarray): - raise TypeError('Map data must be provided as NumPy array') + raise TypeError("Map data must be provided as NumPy array") # Checking that the digital elevation model is a rasterio object or a NumPy array if not isinstance(dem, (rasterio.io.DatasetReader, np.ndarray)): - raise TypeError('The digital elevation model must be provided as rasterio object oder NumPy array') + raise TypeError( + "The digital elevation model must be provided as rasterio object oder NumPy array" + ) # Checking that the extent is of type list if the digital elevation model is provided as array if isinstance(dem, np.ndarray) and not isinstance(extent, list): - raise TypeError('The extent must be provided as list if the digital elevation model is a NumPy array') + raise TypeError( + "The extent must be provided as list if the digital elevation model is a NumPy array" + ) # Checking that all elements of the extent are of type float or int if the digital elevation model is an array - if isinstance(dem, np.ndarray) and not all(isinstance(n, (float, int)) for n in extent): - raise TypeError('All elements of the extent must be of type float or int') + if isinstance(dem, np.ndarray) and not all( + isinstance(n, (float, int)) for n in extent + ): + raise TypeError("All elements of the extent must be of type float or int") # Resizing array or DEM if the shapes do not match if dem.shape != array.shape: @@ -928,18 +963,21 @@ def drape_array_over_dem(array: np.ndarray, from skimage.transform import resize except ModuleNotFoundError: raise ModuleNotFoundError( - 'Scikit Image package is not installed. Use pip install scikit-image to install the latest version') + "Scikit Image package is not installed. Use pip install scikit-image to install the latest version" + ) if resize_array: - array = resize(image=array, - preserve_range=True, - output_shape=(dem.shape[0], - dem.shape[1])) + array = resize( + image=array, + preserve_range=True, + output_shape=(dem.shape[0], dem.shape[1]), + ) else: - dem = resize(image=dem, - preserve_range=True, - output_shape=(array.shape[0], - array.shape[1])) + dem = resize( + image=dem, + preserve_range=True, + output_shape=(array.shape[0], array.shape[1]), + ) # Creating Meshgrid from input data x = np.linspace(dem.bounds[0], dem.bounds[2], array.shape[1]) @@ -1026,31 +1064,34 @@ def create_polydata_from_msh(data: Dict[str, np.ndarray]) -> pv.core.pointset.Po # Checking that the data is a dict if not isinstance(data, dict): - raise TypeError('Data must be provided as dict') + raise TypeError("Data must be provided as dict") # Checking that the faces and vertices are in the dictionary - if 'Tri' not in data: - raise ValueError('Triangles are not in data. Check your input') - if 'Location' not in data: - raise ValueError('Vertices are not in data. Check your input') + if "Tri" not in data: + raise ValueError("Triangles are not in data. Check your input") + if "Location" not in data: + raise ValueError("Vertices are not in data. Check your input") # Creating faces for PyVista PolyData - faces = np.hstack(np.pad(data['Tri'], ((0, 0), (1, 0)), 'constant', constant_values=3)) + faces = np.hstack( + np.pad(data["Tri"], ((0, 0), (1, 0)), "constant", constant_values=3) + ) # Creating vertices for PyVista Polydata - vertices = data['Location'] + vertices = data["Location"] # Creating PolyData polydata = pv.PolyData(vertices, faces) # Adding depth scalars - polydata['Depth [m]'] = polydata.points[:, 2] + polydata["Depth [m]"] = polydata.points[:, 2] return polydata -def create_polydata_from_ts(data: Tuple[list, list], - concat: bool = False) -> pv.core.pointset.PolyData: +def create_polydata_from_ts( + data: Tuple[list, list], concat: bool = False +) -> pv.core.pointset.PolyData: """Converting loaded GoCAD mesh to PyVista PolyData Parameters @@ -1114,17 +1155,17 @@ def create_polydata_from_ts(data: Tuple[list, list], # Checking that the data is a tuple if not isinstance(data, tuple): - raise TypeError('Data must be provided as tuple of lists') + raise TypeError("Data must be provided as tuple of lists") # Checking that the concat parameter is provided as bool if not isinstance(concat, bool): - raise TypeError('Concat parameter must either be True or False') + raise TypeError("Concat parameter must either be True or False") # Checking that the faces and vertices are of the correct type if not isinstance(data[0], list): - raise TypeError('The vertices are in the wrong format. Check your input data') + raise TypeError("The vertices are in the wrong format. Check your input data") if not isinstance(data[1], list): - raise TypeError('The faces are in the wrong format. Check your input data') + raise TypeError("The faces are in the wrong format. Check your input data") if concat: @@ -1133,32 +1174,36 @@ def create_polydata_from_ts(data: Tuple[list, list], faces_list = np.vstack(data[1]) # Creating faces for PyVista PolyData - faces = np.hstack(np.pad(faces_list, ((0, 0), (1, 0)), 'constant', constant_values=3)) + faces = np.hstack( + np.pad(faces_list, ((0, 0), (1, 0)), "constant", constant_values=3) + ) # Creating vertices for PyVista Polydata - vertices = vertices_list[['X', 'Y', 'Z']].values + vertices = vertices_list[["X", "Y", "Z"]].values # Creating PolyData polydata = pv.PolyData(vertices, faces) # Adding depth scalars - polydata['Depth [m]'] = polydata.points[:, 2] + polydata["Depth [m]"] = polydata.points[:, 2] else: mesh_list = [] for i in range(len(data[0])): # Creating faces for PyVista PolyData - faces = np.hstack(np.pad(data[1][i], ((0, 0), (1, 0)), 'constant', constant_values=3)) + faces = np.hstack( + np.pad(data[1][i], ((0, 0), (1, 0)), "constant", constant_values=3) + ) # Creating vertices for PyVista Polydata - vertices = data[0][i][['X', 'Y', 'Z']].values + vertices = data[0][i][["X", "Y", "Z"]].values # Creating PolyData mesh = pv.PolyData(vertices, faces) # Adding depth scalars - mesh['Depth [m]'] = mesh.points[:, 2] + mesh["Depth [m]"] = mesh.points[:, 2] mesh_list.append(mesh) @@ -1167,7 +1212,9 @@ def create_polydata_from_ts(data: Tuple[list, list], return polydata -def create_polydata_from_dxf(gdf: gpd.geodataframe.GeoDataFrame) -> pv.core.pointset.PolyData: +def create_polydata_from_dxf( + gdf: gpd.geodataframe.GeoDataFrame, +) -> pv.core.pointset.PolyData: """Converting loaded DXF object to PyVista PolyData Parameters @@ -1223,33 +1270,35 @@ def create_polydata_from_dxf(gdf: gpd.geodataframe.GeoDataFrame) -> pv.core.poin # Checking that the input data is a GeoDataFrame if not isinstance(gdf, gpd.geodataframe.GeoDataFrame): - raise TypeError('The gdf must be provided as GeoDataFrame') + raise TypeError("The gdf must be provided as GeoDataFrame") # Checking that all elements of the gdf are LineStrings if not all(shapely.get_type_id(gdf.geometry) == 3): - raise TypeError('All geometries must be of geom_type Polygon') + raise TypeError("All geometries must be of geom_type Polygon") # Checking that all Shapely Objects are valid if not all(shapely.is_valid(gdf.geometry)): - raise ValueError('Not all Shapely Objects are valid objects') + raise ValueError("Not all Shapely Objects are valid objects") # Checking that no empty Shapely Objects are present if any(shapely.is_empty(gdf.geometry)): - raise ValueError('One or more Shapely objects are empty') + raise ValueError("One or more Shapely objects are empty") # Extracting XYZ gdf_lines = extract_xy(gdf=gdf) # Assigning vertices - vertices = gdf_lines[['X', 'Y', 'Z']].values + vertices = gdf_lines[["X", "Y", "Z"]].values # Assigning faces faces = np.pad( - np.arange(0, - len(gdf_lines[['X', 'Y', 'Z']].values)).reshape(int(len(gdf_lines[['X', 'Y', 'Z']].values) / 4), 4), + np.arange(0, len(gdf_lines[["X", "Y", "Z"]].values)).reshape( + int(len(gdf_lines[["X", "Y", "Z"]].values) / 4), 4 + ), ((0, 0), (1, 0)), - 'constant', - constant_values=4) + "constant", + constant_values=4, + ) # Creating PolyData dataset polydata = pv.PolyData(vertices, faces) @@ -1309,26 +1358,26 @@ def create_structured_grid_from_asc(data: dict) -> pv.core.pointset.StructuredGr # Checking that the input data is of type dict if not isinstance(data, dict): - raise TypeError('Input data must be a dict') + raise TypeError("Input data must be a dict") # Creating arrays for meshgrid - x = np.arange(data['Extent'][0], data['Extent'][1], data['Resolution']) - y = np.arange(data['Extent'][2], data['Extent'][3], data['Resolution']) + x = np.arange(data["Extent"][0], data["Extent"][1], data["Resolution"]) + y = np.arange(data["Extent"][2], data["Extent"][3], data["Resolution"]) # Creating meshgrid x, y = np.fliplr(np.meshgrid(x, y)) # Copying array data - data_nan = np.copy(data['Data']) + data_nan = np.copy(data["Data"]) # Replacing nodata_vals with np.nans for better visualization - data_nan[data_nan == data['Nodata_val']] = np.nan + data_nan[data_nan == data["Nodata_val"]] = np.nan # Creating StructuredGrid from Meshgrid - grid = pv.StructuredGrid(x, y, data['Data']) + grid = pv.StructuredGrid(x, y, data["Data"]) # Assign depth scalar with replaced nodata_vals - grid['Depth [m]'] = data_nan.ravel(order='F') + grid["Depth [m]"] = data_nan.ravel(order="F") return grid @@ -1385,32 +1434,41 @@ def create_structured_grid_from_zmap(data: dict) -> pv.core.pointset.StructuredG # Checking that the input data is of type dict if not isinstance(data, dict): - raise TypeError('Input data must be a dict') + raise TypeError("Input data must be a dict") # Creating arrays for meshgrid - x = np.arange(data['Extent'][0], data['Extent'][1] + data['Resolution'][0], data['Resolution'][0]) - y = np.arange(data['Extent'][2], data['Extent'][3] + data['Resolution'][1], data['Resolution'][1]) + x = np.arange( + data["Extent"][0], + data["Extent"][1] + data["Resolution"][0], + data["Resolution"][0], + ) + y = np.arange( + data["Extent"][2], + data["Extent"][3] + data["Resolution"][1], + data["Resolution"][1], + ) # Creating meshgrid x, y = np.fliplr(np.meshgrid(x, y)) # Copying array data - data_nan = np.copy(data['Data']) + data_nan = np.copy(data["Data"]) # Replacing nodata_vals with np.nans for better visualization - data_nan[data_nan == data['Nodata_val']] = np.nan + data_nan[data_nan == data["Nodata_val"]] = np.nan # Creating StructuredGrid from Meshgrid - grid = pv.StructuredGrid(x, y, data['Data']) + grid = pv.StructuredGrid(x, y, data["Data"]) # Assign depth scalar with replaced nodata_vals - grid['Depth [m]'] = data_nan.ravel(order='F') + grid["Depth [m]"] = data_nan.ravel(order="F") return grid -def create_delaunay_mesh_from_gdf(gdf: gpd.geodataframe.GeoDataFrame, - z: str = 'Z') -> pv.core.pointset.PolyData: +def create_delaunay_mesh_from_gdf( + gdf: gpd.geodataframe.GeoDataFrame, z: str = "Z" +) -> pv.core.pointset.PolyData: """Creating a delaunay triangulated mesh from surface contour lines Parameters @@ -1471,46 +1529,48 @@ def create_delaunay_mesh_from_gdf(gdf: gpd.geodataframe.GeoDataFrame, from scipy.spatial import Delaunay except ModuleNotFoundError: raise ModuleNotFoundError( - 'SciPy package is not installed. Use pip install scipy to install the latest version') + "SciPy package is not installed. Use pip install scipy to install the latest version" + ) # Checking that the gdf is a GeoDataFrame if not isinstance(gdf, gpd.geodataframe.GeoDataFrame): - raise TypeError('The gdf must be provided as GeoDataFrame') + raise TypeError("The gdf must be provided as GeoDataFrame") # Checking that all elements of the gdf are LineStrings if not all(shapely.get_type_id(gdf.geometry) == 1): - raise TypeError('All geometries must be of geom_type LineString') + raise TypeError("All geometries must be of geom_type LineString") # Checking that all Shapely Objects are valid if not all(shapely.is_valid(gdf.geometry)): - raise ValueError('Not all Shapely Objects are valid objects') + raise ValueError("Not all Shapely Objects are valid objects") # Checking that no empty Shapely Objects are present if any(shapely.is_empty(gdf.geometry)): - raise ValueError('One or more Shapely objects are empty') + raise ValueError("One or more Shapely objects are empty") # Checking that a Z column is present in the GeoDataFrame if z not in gdf: - raise ValueError('A valid column name for Z values must be provided') + raise ValueError("A valid column name for Z values must be provided") # Extracting X and Y values from LineStrings - gdf_xy = extract_xy(gdf=gdf, - reset_index=True) + gdf_xy = extract_xy(gdf=gdf, reset_index=True) # Creating Delaunay tessellation - tri = Delaunay(gdf_xy[['X', 'Y']].values) + tri = Delaunay(gdf_xy[["X", "Y"]].values) # Creating vertices - vertices = gdf_xy[['X', 'Y', 'Z']].values + vertices = gdf_xy[["X", "Y", "Z"]].values # Creating faces - faces = np.hstack(np.pad(tri.simplices, ((0, 0), (1, 0)), 'constant', constant_values=3)) + faces = np.hstack( + np.pad(tri.simplices, ((0, 0), (1, 0)), "constant", constant_values=3) + ) # Creating PyVista PolyData poly = pv.PolyData(vertices, faces) # Creating array with depth values - poly['Depth [m]'] = gdf_xy['Z'].values + poly["Depth [m]"] = gdf_xy["Z"].values return poly @@ -1518,8 +1578,10 @@ def create_delaunay_mesh_from_gdf(gdf: gpd.geodataframe.GeoDataFrame, # Creating Depth and Temperature Maps ##################################### -def create_depth_map(mesh: pv.core.pointset.PolyData, - name: str = 'Depth [m]') -> pv.core.pointset.PolyData: + +def create_depth_map( + mesh: pv.core.pointset.PolyData, name: str = "Depth [m]" +) -> pv.core.pointset.PolyData: """Extracting the depth values of the vertices and add them as scalars to the mesh Parameters @@ -1581,11 +1643,11 @@ def create_depth_map(mesh: pv.core.pointset.PolyData, # Checking that the mesh is a PyVista PolyData dataset if not isinstance(mesh, pv.core.pointset.PolyData): - raise TypeError('Mesh must be a PyVista PolyData dataset') + raise TypeError("Mesh must be a PyVista PolyData dataset") # Checking that the name is of type string if not isinstance(name, str): - raise TypeError('The provided name for the scalar must be of type string') + raise TypeError("The provided name for the scalar must be of type string") # Adding the depths values as data array to the mesh mesh[name] = mesh.points[:, 2] @@ -1593,9 +1655,9 @@ def create_depth_map(mesh: pv.core.pointset.PolyData, return mesh -def create_depth_maps_from_gempy(geo_model, - surfaces: Union[str, List[str]]) \ - -> Dict[str, List[Union[pv.core.pointset.PolyData, np.ndarray, List[str]]]]: +def create_depth_maps_from_gempy( + geo_model, surfaces: Union[str, List[str]] +) -> Dict[str, List[Union[pv.core.pointset.PolyData, np.ndarray, List[str]]]]: """Creating depth map of model surfaces, adapted from https://github.com/cgre-aachen/gempy/blob/20550fffdd1ccb3c6a9a402bc162e7eed3dd7352/gempy/plot/vista.py#L440-L477 @@ -1650,11 +1712,12 @@ def create_depth_maps_from_gempy(geo_model, import gempy as gp except ModuleNotFoundError: raise ModuleNotFoundError( - 'GemPy package is not installed. Use pip install gempy to install the latest version') + "GemPy package is not installed. Use pip install gempy to install the latest version" + ) # Checking if surface is of type string if not isinstance(surfaces, (str, list)): - raise TypeError('Surface name must be of type string') + raise TypeError("Surface name must be of type string") # Converting string to list if only one surface is provided if isinstance(surfaces, str): @@ -1664,39 +1727,49 @@ def create_depth_maps_from_gempy(geo_model, try: # GemPy<3 if not isinstance(geo_model, gp.core.model.Project): - raise TypeError('geo_model must be a GemPy geo_model') + raise TypeError("geo_model must be a GemPy geo_model") # Checking that the model was computed - if all(pd.isna(geo_model.surfaces.df.vertices)) and all(pd.isna(geo_model.surfaces.df.edges)): - raise ValueError('Model must be created before depth map extraction') + if all(pd.isna(geo_model.surfaces.df.vertices)) and all( + pd.isna(geo_model.surfaces.df.edges) + ): + raise ValueError("Model must be created before depth map extraction") # Extracting surface data_df for all surfaces data_df = geo_model.surfaces.df.copy(deep=True) # Checking that surfaces are valid if not all(item in data_df.surface.unique().tolist() for item in surfaces): - raise ValueError('One or more invalid surface names provided') + raise ValueError("One or more invalid surface names provided") # Extracting geometric data of selected surfaces - geometric_data = pd.concat([data_df.groupby('surface').get_group(group) for group in surfaces]) + geometric_data = pd.concat( + [data_df.groupby("surface").get_group(group) for group in surfaces] + ) # Creating empty dict to store data surfaces_poly = {} - for idx, val in geometric_data[['vertices', 'edges', 'color', 'surface', 'id']].dropna().iterrows(): + for idx, val in ( + geometric_data[["vertices", "edges", "color", "surface", "id"]] + .dropna() + .iterrows() + ): # Creating PolyData from each surface - surf = pv.PolyData(val['vertices'][0], np.insert(val['edges'][0], 0, 3, axis=1).ravel()) + surf = pv.PolyData( + val["vertices"][0], np.insert(val["edges"][0], 0, 3, axis=1).ravel() + ) # Append depth to PolyData - surf['Depth [m]'] = val['vertices'][0][:, 2] + surf["Depth [m]"] = val["vertices"][0][:, 2] # Store mesh, depth values and color values in dict - surfaces_poly[val['surface']] = [surf, val['color']] + surfaces_poly[val["surface"]] = [surf, val["color"]] except AttributeError: # GemPy>=3 if not isinstance(geo_model, gp.core.data.geo_model.GeoModel): - raise TypeError('geo_model must be a GemPy geo_model') + raise TypeError("geo_model must be a GemPy geo_model") # TODO Add check that arrays are not empty @@ -1705,7 +1778,7 @@ def create_depth_maps_from_gempy(geo_model, # Checking that surfaces are valid if not all(item in list_surfaces for item in surfaces): - raise ValueError('One or more invalid surface names provided') + raise ValueError("One or more invalid surface names provided") # Getting indices of provided surfaces list_indices = [list_surfaces.index(surface) for surface in surfaces] @@ -1716,25 +1789,35 @@ def create_depth_maps_from_gempy(geo_model, for index in list_indices: # Extracting vertices - vertices = geo_model.input_transform.apply_inverse(geo_model.solutions.raw_arrays.vertices[index]) + vertices = geo_model.input_transform.apply_inverse( + geo_model.solutions.raw_arrays.vertices[index] + ) # Extracting faces - faces = np.insert(geo_model.solutions.raw_arrays.edges[index], 0, 3, axis=1).ravel() + faces = np.insert( + geo_model.solutions.raw_arrays.edges[index], 0, 3, axis=1 + ).ravel() # Creating PolyData from vertices and faces surf = pv.PolyData(vertices, faces) # Appending depth to PolyData - surf['Depth [m]'] = geo_model.input_transform.apply_inverse(geo_model.solutions.raw_arrays.vertices[index])[:, 2] + surf["Depth [m]"] = geo_model.input_transform.apply_inverse( + geo_model.solutions.raw_arrays.vertices[index] + )[:, 2] # Storing mesh, depth values and color values in dict - surfaces_poly[list_surfaces[index]] = [surf, geo_model.structural_frame.elements_colors[index]] + surfaces_poly[list_surfaces[index]] = [ + surf, + geo_model.structural_frame.elements_colors[index], + ] return surfaces_poly -def create_thickness_maps(top_surface: pv.core.pointset.PolyData, - base_surface: pv.core.pointset.PolyData) -> pv.core.pointset.PolyData: +def create_thickness_maps( + top_surface: pv.core.pointset.PolyData, base_surface: pv.core.pointset.PolyData +) -> pv.core.pointset.PolyData: """Creating a thickness map using https://docs.pyvista.org/examples/01-filter/distance-between-surfaces.html#sphx-glr-examples-01-filter-distance-between-surfaces-py Parameters @@ -1787,16 +1870,16 @@ def create_thickness_maps(top_surface: pv.core.pointset.PolyData, # Checking that the top_surface is a PyVista pv.core.pointset.PolyData if not isinstance(top_surface, pv.core.pointset.PolyData): - raise TypeError('Top Surface must be a PyVista PolyData set') + raise TypeError("Top Surface must be a PyVista PolyData set") # Checking that the base_surface is a PyVista pv.core.pointset.PolyData if not isinstance(base_surface, pv.core.pointset.PolyData): - raise TypeError('Base Surface must be a PyVista PolyData set') + raise TypeError("Base Surface must be a PyVista PolyData set") # Computing normals of lower surface - base_surface_normals = base_surface.compute_normals(point_normals=True, - cell_normals=False, - auto_orient_normals=True) + base_surface_normals = base_surface.compute_normals( + point_normals=True, cell_normals=False, auto_orient_normals=True + ) # Travel along normals to the other surface and compute the thickness on each vector base_surface_normals["Thickness [m]"] = np.empty(base_surface.n_points) @@ -1816,12 +1899,14 @@ def create_thickness_maps(top_surface: pv.core.pointset.PolyData, return base_surface_normals -def create_temperature_map(dem: rasterio.io.DatasetReader, - mesh: pv.core.pointset.PolyData, - name: str = 'Thickness [m]', - apply_threshold: bool = True, - tsurface: Union[float, int] = 10, - gradient: Union[float, int] = 0.03) -> pv.core.pointset.PolyData: +def create_temperature_map( + dem: rasterio.io.DatasetReader, + mesh: pv.core.pointset.PolyData, + name: str = "Thickness [m]", + apply_threshold: bool = True, + tsurface: Union[float, int] = 10, + gradient: Union[float, int] = 0.03, +) -> pv.core.pointset.PolyData: """Creating a temperature map for a surface at depth taking the topography into account Parameters @@ -1900,15 +1985,17 @@ def create_temperature_map(dem: rasterio.io.DatasetReader, # Checking that the raster is a rasterio object if not isinstance(dem, rasterio.io.DatasetReader): - raise TypeError('Provided Digital Elevation Model must be provided as rasterio object') + raise TypeError( + "Provided Digital Elevation Model must be provided as rasterio object" + ) # Checking that the mesh is PyVista PolyData dataset if not isinstance(mesh, pv.core.pointset.PolyData): - raise TypeError('Mesh must be a PyVista PolyData dataset') + raise TypeError("Mesh must be a PyVista PolyData dataset") # Checking that apply_threshold is of type bool if not isinstance(apply_threshold, bool): - raise TypeError('Variable to apply the threshold must be of type bool') + raise TypeError("Variable to apply the threshold must be of type bool") # Getting the x coordinates of the mesh vertices vertices_x = mesh.points[:, 0] @@ -1920,9 +2007,7 @@ def create_temperature_map(dem: rasterio.io.DatasetReader, vertices_z = mesh.points[:, 2] # Sampling the raster at the vertices locations - raster_z = sample_from_rasterio(raster=dem, - point_x=vertices_x, - point_y=vertices_y) + raster_z = sample_from_rasterio(raster=dem, point_x=vertices_x, point_y=vertices_y) # Calculating the thickness of the layer thickness = (vertices_z - raster_z) * (-1) @@ -1935,7 +2020,7 @@ def create_temperature_map(dem: rasterio.io.DatasetReader, mesh = mesh.threshold([0, mesh[name].max()]) # Calculating the temperature and adding it as data array to the mesh - mesh['Temperature [°C]'] = mesh[name] * gradient + tsurface + mesh["Temperature [°C]"] = mesh[name] * gradient + tsurface return mesh @@ -1943,6 +2028,7 @@ def create_temperature_map(dem: rasterio.io.DatasetReader, # Visualizing Boreholes in 3D ############################# + def group_borehole_dataframe(df: pd.DataFrame) -> List[pd.DataFrame]: """Grouping Borehole DataFrame by Index @@ -1974,14 +2060,14 @@ def group_borehole_dataframe(df: pd.DataFrame) -> List[pd.DataFrame]: # Checking that the input data is a (Geo-)DataFrame if not isinstance(df, (pd.DataFrame, gpd.geodataframe.GeoDataFrame)): - raise TypeError('Input data must be a (Geo-)DataFrame') + raise TypeError("Input data must be a (Geo-)DataFrame") # Checking that the index column is in the (Geo-)DataFrame - if 'Index' not in df: - raise ValueError('Index column not in (Geo-)DataFrame') + if "Index" not in df: + raise ValueError("Index column not in (Geo-)DataFrame") # Grouping df by Index - grouped = df.groupby(['Index']) + grouped = df.groupby(["Index"]) # Getting single (Geo-)DataFrames df_groups = [grouped.get_group(x) for x in grouped.groups] @@ -2043,30 +2129,35 @@ def add_row_to_boreholes(df_groups: List[pd.DataFrame]) -> List[pd.DataFrame]: try: from tqdm import tqdm except ModuleNotFoundError: - raise ModuleNotFoundError('tqdm package is not installed. Use pip install tqdm to install the latest version') + raise ModuleNotFoundError( + "tqdm package is not installed. Use pip install tqdm to install the latest version" + ) # Checking that df_groups is a list if not isinstance(df_groups, list): - raise TypeError('df_groups must be a list containing Pandas DataFrames') + raise TypeError("df_groups must be a list containing Pandas DataFrames") # Checking that all elements of the list are of type DataFrame if not all(isinstance(i, pd.DataFrame) for i in df_groups): - raise TypeError('All elements of df_groups must be of type Pandas DataFrame') + raise TypeError("All elements of df_groups must be of type Pandas DataFrame") # Adding additional row to DataFrame for i in tqdm(range(len(df_groups))): - index = df_groups[i]['Index'].unique()[0] - name = df_groups[i]['Name'].unique()[0] - x = df_groups[i]['X'].unique()[0] - y = df_groups[i]['Y'].unique()[0] - z = df_groups[i]['Altitude'].unique()[0] - altitude = df_groups[i]['Altitude'].unique()[0] - depth = df_groups[i]['Depth'].unique()[0] - formation = '' + index = df_groups[i]["Index"].unique()[0] + name = df_groups[i]["Name"].unique()[0] + x = df_groups[i]["X"].unique()[0] + y = df_groups[i]["Y"].unique()[0] + z = df_groups[i]["Altitude"].unique()[0] + altitude = df_groups[i]["Altitude"].unique()[0] + depth = df_groups[i]["Depth"].unique()[0] + formation = "" data = [[index, name, x, y, z, altitude, depth, formation]] - row = pd.DataFrame(data=data, columns=['Index', 'Name', 'X', 'Y', 'Z', 'Altitude', 'Depth', 'formation']) + row = pd.DataFrame( + data=data, + columns=["Index", "Name", "X", "Y", "Z", "Altitude", "Depth", "formation"], + ) df_groups[i] = pd.concat([df_groups[i], row]) - df_groups[i] = df_groups[i].sort_values(by=['Z'], ascending=False) + df_groups[i] = df_groups[i].sort_values(by=["Z"], ascending=False) return df_groups @@ -2134,10 +2225,10 @@ def create_lines_from_points(df: pd.DataFrame) -> pv.core.pointset.PolyData: # Checking if df is of a pandas DataFrame if not isinstance(df, pd.DataFrame): - raise TypeError('Borehole data must be provided as Pandas DataFrame') + raise TypeError("Borehole data must be provided as Pandas DataFrame") # Deleting not needed columns - df_copy = df.copy(deep=True)[['X', 'Y', 'Z']] + df_copy = df.copy(deep=True)[["X", "Y", "Z"]] # Creating line data set poly = pv.PolyData(df_copy.to_numpy()) @@ -2149,9 +2240,9 @@ def create_lines_from_points(df: pd.DataFrame) -> pv.core.pointset.PolyData: return poly -def create_borehole_tube(df: pd.DataFrame, - line: pv.core.pointset.PolyData, - radius: Union[float, int]) -> pv.core.pointset.PolyData: +def create_borehole_tube( + df: pd.DataFrame, line: pv.core.pointset.PolyData, radius: Union[float, int] +) -> pv.core.pointset.PolyData: """Creating a tube from a line for the 3D visualization of boreholes Parameters @@ -2236,15 +2327,15 @@ def create_borehole_tube(df: pd.DataFrame, # Checking if df is of a pandas DataFrame if not isinstance(df, pd.DataFrame): - raise TypeError('Borehole data must be provided as Pandas DataFrame') + raise TypeError("Borehole data must be provided as Pandas DataFrame") # Checking that the line data is a PolyData object if not isinstance(line, pv.core.pointset.PolyData): - raise TypeError('Line data must be a PolyData object') + raise TypeError("Line data must be a PolyData object") # Checking that the radius is of type float if not isinstance(radius, (float, int)): - raise TypeError('Radius must be of type float') + raise TypeError("Radius must be of type float") # Deleting the first row which does not contain a formation (see above) df_cols = df.copy(deep=True) @@ -2257,14 +2348,14 @@ def create_borehole_tube(df: pd.DataFrame, tube = line.tube(radius=radius) # Adding depth scalars - tube['Depth'] = tube.points[:, 2] + tube["Depth"] = tube.points[:, 2] return tube -def create_borehole_tubes(df: pd.DataFrame, - min_length: Union[float, int], - radius: Union[int, float] = 10) -> Tuple[List[pv.core.pointset.PolyData], List[pd.DataFrame]]: +def create_borehole_tubes( + df: pd.DataFrame, min_length: Union[float, int], radius: Union[int, float] = 10 +) -> Tuple[List[pv.core.pointset.PolyData], List[pd.DataFrame]]: """Creating PyVista Tubes for plotting boreholes in 3D Parameters @@ -2331,40 +2422,47 @@ def create_borehole_tubes(df: pd.DataFrame, # Checking if df is of a pandas DataFrame if not isinstance(df, pd.DataFrame): - raise TypeError('Borehole data must be provided as Pandas DataFrame') + raise TypeError("Borehole data must be provided as Pandas DataFrame") # Checking that all necessary columns are present in the DataFrame - if not {'Index', 'Name', 'X', 'Y', 'Z', 'Altitude', 'Depth', 'formation'}.issubset(df.columns): - raise ValueError('[%s, %s, %s, %s, %s, %s, %s, %s] need to be columns in the provided DataFrame' % ( - 'Index', 'Name', 'X', 'Y', 'Z', 'Altitude', 'Depth', 'formation')) + if not {"Index", "Name", "X", "Y", "Z", "Altitude", "Depth", "formation"}.issubset( + df.columns + ): + raise ValueError( + "[%s, %s, %s, %s, %s, %s, %s, %s] need to be columns in the provided DataFrame" + % ("Index", "Name", "X", "Y", "Z", "Altitude", "Depth", "formation") + ) # Checking that the min_limit is of type float or int if not isinstance(min_length, (float, int)): - raise TypeError('Minimum length for boreholes must be of type float or int') + raise TypeError("Minimum length for boreholes must be of type float or int") # Checking that the radius is of type int or float if not isinstance(radius, (int, float)): - raise TypeError('The radius must be provided as int or float') + raise TypeError("The radius must be provided as int or float") # Limiting the length of boreholes withing the DataFrame to a minimum length - df = df[df['Depth'] >= min_length] + df = df[df["Depth"] >= min_length] # Group each borehole by its index and return groups within a list, each item in the list is a pd.DataFrame - grouped = df.groupby(['Index']) + grouped = df.groupby(["Index"]) df_groups = [grouped.get_group(x) for x in grouped.groups] # Add additional row to each borehole df_groups = add_row_to_boreholes(df_groups=df_groups) lines = [create_lines_from_points(df=i) for i in df_groups] - tubes = [create_borehole_tube(df=df_groups[i], - line=lines[i], - radius=radius) for i in range(len(df_groups))] + tubes = [ + create_borehole_tube(df=df_groups[i], line=lines[i], radius=radius) + for i in range(len(df_groups)) + ] return tubes, df_groups -def create_borehole_labels(df: Union[pd.DataFrame, gpd.geodataframe.GeoDataFrame]) -> pv.core.pointset.PolyData: +def create_borehole_labels( + df: Union[pd.DataFrame, gpd.geodataframe.GeoDataFrame] +) -> pv.core.pointset.PolyData: """Create labels for borehole plots. Parameters @@ -2420,36 +2518,51 @@ def create_borehole_labels(df: Union[pd.DataFrame, gpd.geodataframe.GeoDataFrame """ # Checking if df is of a pandas DataFrame if not isinstance(df, (pd.DataFrame, gpd.geodataframe.GeoDataFrame)): - raise TypeError('Borehole data must be provided as Pandas DataFrame or GeoPandas GeoDataFrame') + raise TypeError( + "Borehole data must be provided as Pandas DataFrame or GeoPandas GeoDataFrame" + ) # Checking that X, Y and the Altitude of the borehole are present - if not {'X', 'Y', 'Altitude'}.issubset(df.columns): - raise ValueError('X, Y and Altitude columns must be provided for label creation') + if not {"X", "Y", "Altitude"}.issubset(df.columns): + raise ValueError( + "X, Y and Altitude columns must be provided for label creation" + ) # Creating array with coordinates from each group (equals to one borehole) coordinates = np.rot90( np.array( - df.groupby(['Index', 'Name'])[['X', 'Y', 'Altitude']].apply(lambda x: list(np.unique(x))).values.tolist()), - 2) + df.groupby(["Index", "Name"])[["X", "Y", "Altitude"]] + .apply(lambda x: list(np.unique(x))) + .values.tolist() + ), + 2, + ) # Creating borehole location PyVista PolyData Object borehole_locations = pv.PolyData(coordinates) # Creating borehole_location labels - list_tuples = df.groupby(['Index', 'Name'])[['X', 'Y', 'Altitude']].apply( - lambda x: list(np.unique(x))).index.tolist()[::-1] + list_tuples = ( + df.groupby(["Index", "Name"])[["X", "Y", "Altitude"]] + .apply(lambda x: list(np.unique(x))) + .index.tolist()[::-1] + ) - borehole_locations['Labels'] = [''.join(char for char in i[1] if ord(char) < 128) for i in list_tuples] + borehole_locations["Labels"] = [ + "".join(char for char in i[1] if ord(char) < 128) for i in list_tuples + ] return borehole_locations -def create_boreholes_3d(df: pd.DataFrame, - min_length: Union[float, int], - color_dict: dict, - radius: Union[float, int] = 10) -> Tuple[List[pv.core.pointset.PolyData], - pv.core.pointset.PolyData, - List[pd.DataFrame]]: +def create_boreholes_3d( + df: pd.DataFrame, + min_length: Union[float, int], + color_dict: dict, + radius: Union[float, int] = 10, +) -> Tuple[ + List[pv.core.pointset.PolyData], pv.core.pointset.PolyData, List[pd.DataFrame] +]: """Plotting boreholes in 3D Parameters @@ -2532,29 +2645,37 @@ def create_boreholes_3d(df: pd.DataFrame, # Checking if df is of a pandas DataFrame if not isinstance(df, pd.DataFrame): - raise TypeError('Borehole data must be provided as Pandas DataFrame') + raise TypeError("Borehole data must be provided as Pandas DataFrame") # Checking that all necessary columns are present in the DataFrame - if not pd.Series(['Index', 'Name', 'X', 'Y', 'Z', 'Altitude', 'Depth', 'formation']).isin(df.columns).all(): - raise ValueError('[%s, %s, %s, %s, %s, %s, %s, %s] need to be columns in the provided DataFrame' % ( - 'Index', 'Name', 'X', 'Y', 'Z', 'Altitude', 'Depth', 'formation')) + if ( + not pd.Series( + ["Index", "Name", "X", "Y", "Z", "Altitude", "Depth", "formation"] + ) + .isin(df.columns) + .all() + ): + raise ValueError( + "[%s, %s, %s, %s, %s, %s, %s, %s] need to be columns in the provided DataFrame" + % ("Index", "Name", "X", "Y", "Z", "Altitude", "Depth", "formation") + ) # Checking that the min_limit is of type float or int if not isinstance(min_length, (float, int)): - raise TypeError('Minimum length for boreholes must be of type float or int') + raise TypeError("Minimum length for boreholes must be of type float or int") # Checking that the color_dict is of type dict if not isinstance(color_dict, dict): - raise TypeError('Surface color dictionary must be of type dict') + raise TypeError("Surface color dictionary must be of type dict") # Checking that the radius is of type int or float if not isinstance(radius, (int, float)): - raise TypeError('The radius must be provided as int or float') + raise TypeError("The radius must be provided as int or float") # Creating tubes for later plotting - tubes, df_groups = create_borehole_tubes(df=df, - min_length=min_length, - radius=radius) + tubes, df_groups = create_borehole_tubes( + df=df, min_length=min_length, radius=radius + ) # Creating MultiBlock Object containing the tubes tubes = pv.MultiBlock(tubes) @@ -2565,8 +2686,7 @@ def create_boreholes_3d(df: pd.DataFrame, return tubes, labels, df_groups -def calculate_vector(dip: Union[float, int], - azimuth: Union[float, int]) -> np.ndarray: +def calculate_vector(dip: Union[float, int], azimuth: Union[float, int]) -> np.ndarray: """Calculating the plunge vector of a borehole section Parameters @@ -2605,25 +2725,31 @@ def calculate_vector(dip: Union[float, int], # Checking that the dip is type float or int if not isinstance(dip, (float, int)): - raise TypeError('Dip value must be of type float or int') + raise TypeError("Dip value must be of type float or int") # Checking that the azimuth is type float or int if not isinstance(azimuth, (float, int)): - raise TypeError('Azimuth value must be of type float or int') + raise TypeError("Azimuth value must be of type float or int") # Calculating plunging vector - vector = np.array([[np.sin(dip) * np.cos(azimuth)], - [np.cos(dip) * np.cos(azimuth)], - [np.sin(azimuth)]]) + vector = np.array( + [ + [np.sin(dip) * np.cos(azimuth)], + [np.cos(dip) * np.cos(azimuth)], + [np.sin(azimuth)], + ] + ) return vector -def create_deviated_borehole_df(df_survey: pd.DataFrame, - position: Union[np.ndarray, shapely.geometry.point.Point], - depth: str = 'depth', - dip: str = 'dip', - azimuth: str = 'azimuth') -> pd.DataFrame: +def create_deviated_borehole_df( + df_survey: pd.DataFrame, + position: Union[np.ndarray, shapely.geometry.point.Point], + depth: str = "depth", + dip: str = "dip", + azimuth: str = "azimuth", +) -> pd.DataFrame: """Creating Pandas DataFrame containing parameters to create 3D boreholes Parameters @@ -2680,75 +2806,91 @@ def create_deviated_borehole_df(df_survey: pd.DataFrame, # Checking that the input DataFrame is a Pandas DataFrame if not isinstance(df_survey, pd.DataFrame): - raise TypeError('Survey Input Data must be a Pandas DataFrame') + raise TypeError("Survey Input Data must be a Pandas DataFrame") # Checking that the position of the well is either provided as np.ndarray or as Shapely point if not isinstance(position, (np.ndarray, shapely.geometry.point.Point)): - raise TypeError('Borehole position must be provides as NumPy array or Shapely Point') + raise TypeError( + "Borehole position must be provides as NumPy array or Shapely Point" + ) # Checking that the column name is of type string if not isinstance(depth, str): - raise TypeError('Depth column name must be provided as string') + raise TypeError("Depth column name must be provided as string") # Checking that the column name is of type string if not isinstance(dip, str): - raise TypeError('Dip column name must be provided as string') + raise TypeError("Dip column name must be provided as string") # Checking that the column name is of type string if not isinstance(azimuth, str): - raise TypeError('Azimuth column name must be provided as string') + raise TypeError("Azimuth column name must be provided as string") # Converting Shapely Point to array if isinstance(position, shapely.geometry.point.Point): position = np.array(position.coords) # Calculating the bottom depth of each borehole segment - df_survey['depth_bottom'] = pd.concat([df_survey[depth], pd.Series(np.nan,index=[len(df_survey[depth])])]) + df_survey["depth_bottom"] = pd.concat( + [df_survey[depth], pd.Series(np.nan, index=[len(df_survey[depth])])] + ) # Calculating the plunging vector for each borehole segment - df_survey['vector'] = df_survey.apply(lambda row: calculate_vector(row[dip], - row[azimuth]), axis=1) + df_survey["vector"] = df_survey.apply( + lambda row: calculate_vector(row[dip], row[azimuth]), axis=1 + ) # Calculating the length of each segment - depths = df_survey['depth'].values[:-1] - df_survey['depth'].values[1:] + depths = df_survey["depth"].values[:-1] - df_survey["depth"].values[1:] depths = np.append(depths, 0) - df_survey['segment_length'] = depths + df_survey["segment_length"] = depths # Calculating the coordinates of each segment - x = np.cumsum(df_survey['segment_length'].values * df_survey['vector'].values) + x = np.cumsum(df_survey["segment_length"].values * df_survey["vector"].values) # Adding the position of the borehole at the surface to each point - df_survey['points'] = np.array([(element.T + position)[0] for element in x]).tolist() + df_survey["points"] = np.array( + [(element.T + position)[0] for element in x] + ).tolist() # Adding point coordinates as X, Y and Z columns to work with `create_lines_from_points' function - df_survey[['X', 'Y', 'Z']] = df_survey['points'].values.tolist() + df_survey[["X", "Y", "Z"]] = df_survey["points"].values.tolist() # Creating coordinates for first row df_row0 = pd.DataFrame([position[0], position[1], position[2]]).T - df_row0['points'] = [position] - df_row0.columns = ['X', 'Y', 'Z', 'points'] + df_row0["points"] = [position] + df_row0.columns = ["X", "Y", "Z", "points"] # Creating first row - df_extra = pd.concat([pd.DataFrame(df_survey.loc[0].drop(['points', 'X', 'Y', 'Z'])).T, df_row0], axis=1) + df_extra = pd.concat( + [pd.DataFrame(df_survey.loc[0].drop(["points", "X", "Y", "Z"])).T, df_row0], + axis=1, + ) # Adding first row to DataFrame - df_survey = pd.concat([df_extra, df_survey]).drop(df_survey.tail(1).index).reset_index(drop=True) + df_survey = ( + pd.concat([df_extra, df_survey]) + .drop(df_survey.tail(1).index) + .reset_index(drop=True) + ) return df_survey -def create_deviated_boreholes_3d(df_collar: pd.DataFrame, - df_survey: pd.DataFrame, - min_length: Union[float, int], - # color_dict: dict, - radius: Union[float, int] = 10, - collar_depth: str = 'Depth', - survey_depth: str = 'Depth', - index: str = 'Index', - dip: str = 'dip', - azimuth: str = 'azimuth') -> Tuple[List[pv.core.pointset.PolyData], - pv.core.pointset.PolyData, - List[pd.DataFrame]]: +def create_deviated_boreholes_3d( + df_collar: pd.DataFrame, + df_survey: pd.DataFrame, + min_length: Union[float, int], + # color_dict: dict, + radius: Union[float, int] = 10, + collar_depth: str = "Depth", + survey_depth: str = "Depth", + index: str = "Index", + dip: str = "dip", + azimuth: str = "azimuth", +) -> Tuple[ + List[pv.core.pointset.PolyData], pv.core.pointset.PolyData, List[pd.DataFrame] +]: """Plotting boreholes in 3D Parameters @@ -2805,15 +2947,15 @@ def create_deviated_boreholes_3d(df_collar: pd.DataFrame, # Checking if df is of a pandas DataFrame if not isinstance(df_collar, pd.DataFrame): - raise TypeError('Borehole data must be provided as Pandas DataFrame') + raise TypeError("Borehole data must be provided as Pandas DataFrame") # Checking if df is of a pandas DataFrame if not isinstance(df_survey, pd.DataFrame): - raise TypeError('Borehole data must be provided as Pandas DataFrame') + raise TypeError("Borehole data must be provided as Pandas DataFrame") # Checking that the min_limit is of type float or int if not isinstance(min_length, (float, int)): - raise TypeError('Minimum length for boreholes must be of type float or int') + raise TypeError("Minimum length for boreholes must be of type float or int") # Checking that the color_dict is of type dict # if not isinstance(color_dict, dict): @@ -2821,27 +2963,27 @@ def create_deviated_boreholes_3d(df_collar: pd.DataFrame, # Checking that the radius is of type int or float if not isinstance(radius, (int, float)): - raise TypeError('The radius must be provided as int or float') + raise TypeError("The radius must be provided as int or float") # Checking that the column name is of type string if not isinstance(collar_depth, str): - raise TypeError('Collar depth column name must be provided as string') + raise TypeError("Collar depth column name must be provided as string") # Checking that the column name is of type string if not isinstance(survey_depth, str): - raise TypeError('Survey depth column name must be provided as string') + raise TypeError("Survey depth column name must be provided as string") # Checking that the column name is of type string if not isinstance(dip, str): - raise TypeError('Dip column name must be provided as string') + raise TypeError("Dip column name must be provided as string") # Checking that the column name is of type string if not isinstance(azimuth, str): - raise TypeError('Azimuth column name must be provided as string') + raise TypeError("Azimuth column name must be provided as string") # Checking that the column name is of type string if not isinstance(index, str): - raise TypeError('Index column name must be provided as string') + raise TypeError("Index column name must be provided as string") # Limiting the length of boreholes withing the DataFrame to a minimum length df_collar = df_collar[df_collar[collar_depth] >= min_length] @@ -2857,17 +2999,24 @@ def create_deviated_boreholes_3d(df_collar: pd.DataFrame, # Group each borehole by its index and return groups within a list, each item in the list is a pd.DataFrame grouped_survey = df_survey.groupby([index]) - df_groups_survey = [grouped_survey.get_group(x).reset_index() for x in grouped_survey.groups] + df_groups_survey = [ + grouped_survey.get_group(x).reset_index() for x in grouped_survey.groups + ] # Creating deviated borehole DataFrames df_groups = [ - create_deviated_borehole_df(df_survey=df_groups_survey[i], position=df_collar.loc[i][['x', 'y', 'z']].values) - for i in range(len(df_collar))] + create_deviated_borehole_df( + df_survey=df_groups_survey[i], + position=df_collar.loc[i][["x", "y", "z"]].values, + ) + for i in range(len(df_collar)) + ] lines = [create_lines_from_points(df=i) for i in df_groups] - tubes = [create_borehole_tube(df=df_groups[i], - line=lines[i], - radius=radius) for i in range(len(df_groups))] + tubes = [ + create_borehole_tube(df=df_groups[i], line=lines[i], radius=radius) + for i in range(len(df_groups)) + ] # Creating MultiBlock Object containing the tubes tubes = pv.MultiBlock(tubes) @@ -2878,14 +3027,17 @@ def create_deviated_boreholes_3d(df_collar: pd.DataFrame, # Misc ######## -def plot_orientations(gdf: Union[gpd.geodataframe.GeoDataFrame, pd.DataFrame], - show_planes: bool = True, - show_density_contours: bool = True, - show_density_contourf: bool = False, - formation: str = None, - method: str = 'exponential_kamb', - sigma: Union[float, int] = 1, - cmap: str = 'Blues_r'): + +def plot_orientations( + gdf: Union[gpd.geodataframe.GeoDataFrame, pd.DataFrame], + show_planes: bool = True, + show_density_contours: bool = True, + show_density_contourf: bool = False, + formation: str = None, + method: str = "exponential_kamb", + sigma: Union[float, int] = 1, + cmap: str = "Blues_r", +): """Plotting orientation values of a GeoDataFrame with mplstereonet Parameters @@ -2948,77 +3100,81 @@ def plot_orientations(gdf: Union[gpd.geodataframe.GeoDataFrame, pd.DataFrame], import mplstereonet except ModuleNotFoundError: raise ModuleNotFoundError( - 'mplstereonet package is not installed. Use pip install mplstereonet to install the latest version') + "mplstereonet package is not installed. Use pip install mplstereonet to install the latest version" + ) # Trying to import matplotlib but returning error if matplotlib is not installed try: import matplotlib.pyplot as plt except ModuleNotFoundError: raise ModuleNotFoundError( - 'Matplotlib package is not installed. Use pip install matplotlib to install the latest version') + "Matplotlib package is not installed. Use pip install matplotlib to install the latest version" + ) # Checking if gdf is of type GeoDataFrame or DataFrame if not isinstance(gdf, (gpd.geodataframe.GeoDataFrame, pd.DataFrame)): - raise TypeError('Object must be of type GeoDataFrame or DataFrame') + raise TypeError("Object must be of type GeoDataFrame or DataFrame") # Checking if the formation, dip and azimuth columns are present - if not {'formation', 'dip', 'azimuth'}.issubset(gdf.columns): - raise ValueError('GeoDataFrame/DataFrame is missing columns (formation, dip, azimuth)') + if not {"formation", "dip", "azimuth"}.issubset(gdf.columns): + raise ValueError( + "GeoDataFrame/DataFrame is missing columns (formation, dip, azimuth)" + ) # Checking that the provided formation for contourf is of type string if not isinstance(formation, (str, type(None))): - raise TypeError('The provided formation must be of type string') + raise TypeError("The provided formation must be of type string") # Checking that show_planes is of type bool if not isinstance(show_planes, bool): - raise TypeError('Variable to show planes must be of type bool') + raise TypeError("Variable to show planes must be of type bool") # Checking that show_density_contours is of type bool if not isinstance(show_density_contours, bool): - raise TypeError('Variable to show density contours must be of type bool') + raise TypeError("Variable to show density contours must be of type bool") # Checking that show_density_contourf is of type bool if not isinstance(show_density_contourf, bool): - raise TypeError('Variable to show density contourf must be of type bool') + raise TypeError("Variable to show density contourf must be of type bool") # Checking that the provided method is of type str if not isinstance(method, str): - raise TypeError('The provided method must be of type string') + raise TypeError("The provided method must be of type string") # Checking that the provided sigma is of type float or int if not isinstance(sigma, (float, int)): - raise TypeError('Sigma must be of type float or int') + raise TypeError("Sigma must be of type float or int") # Checking that the provided cmap is of type string if not isinstance(cmap, str): - raise TypeError('Colormap must be provided as string') + raise TypeError("Colormap must be provided as string") # Converting dips to floats - if 'dip' in gdf: - gdf['dip'] = gdf['dip'].astype(float) + if "dip" in gdf: + gdf["dip"] = gdf["dip"].astype(float) # Converting azimuths to floats - if 'azimuth' in gdf: - gdf['azimuth'] = gdf['azimuth'].astype(float) + if "azimuth" in gdf: + gdf["azimuth"] = gdf["azimuth"].astype(float) # Converting formations to string - if 'formation' in gdf: - gdf['formation'] = gdf['formation'].astype(str) + if "formation" in gdf: + gdf["formation"] = gdf["formation"].astype(str) # Checking that dips do not exceed 90 degrees - if (gdf['dip'] > 90).any(): - raise ValueError('dip values exceed 90 degrees') + if (gdf["dip"] > 90).any(): + raise ValueError("dip values exceed 90 degrees") # Checking that azimuth do not exceed 360 degrees - if (gdf['azimuth'] > 360).any(): - raise ValueError('azimuth values exceed 360 degrees') + if (gdf["azimuth"] > 360).any(): + raise ValueError("azimuth values exceed 360 degrees") # Get unique formations - formations = gdf['formation'].unique() + formations = gdf["formation"].unique() # Define figure fig = plt.figure(figsize=(11, 5)) - ax = fig.add_subplot(121, projection='stereonet') + ax = fig.add_subplot(121, projection="stereonet") # Creating a set of points and planes for each formation for j, form in enumerate(formations): @@ -3027,64 +3183,81 @@ def plot_orientations(gdf: Union[gpd.geodataframe.GeoDataFrame, pd.DataFrame], color = "#%06x" % np.random.randint(0, 0xFFFFFF) # Select rows of the dataframe - gdf_form = gdf[gdf['formation'] == form] + gdf_form = gdf[gdf["formation"] == form] # Plot poles and planes - for i in range(len(gdf_form[['azimuth', 'dip']])): + for i in range(len(gdf_form[["azimuth", "dip"]])): # Plotting poles - ax.pole(gdf_form[['azimuth', 'dip']].iloc[i][0] - 90, - gdf_form[['azimuth', 'dip']].iloc[i][1], - color=color, - markersize=4, - markeredgewidth=0.5, - markeredgecolor='black', - label=formations[j]) + ax.pole( + gdf_form[["azimuth", "dip"]].iloc[i][0] - 90, + gdf_form[["azimuth", "dip"]].iloc[i][1], + color=color, + markersize=4, + markeredgewidth=0.5, + markeredgecolor="black", + label=formations[j], + ) # Plotting planes if show_planes: - ax.plane(gdf_form[['azimuth', 'dip']].iloc[i][0] - 90, - gdf_form[['azimuth', 'dip']].iloc[i][1], - linewidth=0.5, - color=color) + ax.plane( + gdf_form[["azimuth", "dip"]].iloc[i][0] - 90, + gdf_form[["azimuth", "dip"]].iloc[i][1], + linewidth=0.5, + color=color, + ) # Creating legend handles, labels = ax.get_legend_handles_labels() by_label = OrderedDict(zip(labels, handles)) - ax.legend(by_label.values(), by_label.keys(), loc='upper left', bbox_to_anchor=(1.05, 1)) + ax.legend( + by_label.values(), + by_label.keys(), + loc="upper left", + bbox_to_anchor=(1.05, 1), + ) # Creating density contours if show_density_contours: - ax.density_contour(gdf_form['azimuth'].to_numpy() - 90, - gdf_form['dip'].to_numpy(), - measurement='poles', - sigma=sigma, - method=method, - cmap=cmap) + ax.density_contour( + gdf_form["azimuth"].to_numpy() - 90, + gdf_form["dip"].to_numpy(), + measurement="poles", + sigma=sigma, + method=method, + cmap=cmap, + ) # Creating density contourf if show_density_contourf and formation is not None: - ax.density_contourf(gdf[gdf['formation'] == formation]['azimuth'].to_numpy() - 90, - gdf[gdf['formation'] == formation]['dip'].to_numpy(), - measurement='poles', - sigma=sigma, - method=method, - cmap=cmap) + ax.density_contourf( + gdf[gdf["formation"] == formation]["azimuth"].to_numpy() - 90, + gdf[gdf["formation"] == formation]["dip"].to_numpy(), + measurement="poles", + sigma=sigma, + method=method, + cmap=cmap, + ) elif not show_density_contourf and formation is not None: - raise ValueError('Formation must not be provided if show_density_contourf is set to False') + raise ValueError( + "Formation must not be provided if show_density_contourf is set to False" + ) elif show_density_contourf and formation is None: - raise ValueError('Formation name needed to plot density contourf') + raise ValueError("Formation name needed to plot density contourf") else: pass ax.grid() - ax.set_title('n = %d' % (len(gdf)), y=1.1) + ax.set_title("n = %d" % (len(gdf)), y=1.1) -def create_meshes_hypocenters(gdf: gpd.geodataframe.GeoDataFrame, - magnitude: str = 'Magnitude', - magnitude_factor: int = 200, - year: str = 'Year') -> pv.core.composite.MultiBlock: +def create_meshes_hypocenters( + gdf: gpd.geodataframe.GeoDataFrame, + magnitude: str = "Magnitude", + magnitude_factor: int = 200, + year: str = "Year", +) -> pv.core.composite.MultiBlock: """Plotting earthquake hypocenters with PyVista Parameters @@ -3144,40 +3317,52 @@ def create_meshes_hypocenters(gdf: gpd.geodataframe.GeoDataFrame, # Checking that the gdf is a GeoDataFrame if not isinstance(gdf, gpd.geodataframe.GeoDataFrame): - raise TypeError('Input data must be a GeoDataFrame') + raise TypeError("Input data must be a GeoDataFrame") # Checking that all geometry objects are points if not all(shapely.get_type_id(gdf.geometry) == 0): - raise TypeError('All geometry objects must be Shapely Points') + raise TypeError("All geometry objects must be Shapely Points") # Checking that all Shapely Objects are valid if not all(shapely.is_valid(gdf.geometry)): - raise ValueError('Not all Shapely Objects are valid objects') + raise ValueError("Not all Shapely Objects are valid objects") # Checking that no empty Shapely Objects are present if any(shapely.is_empty(gdf.geometry)): - raise ValueError('One or more Shapely objects are empty') + raise ValueError("One or more Shapely objects are empty") # Checking that X, Y and Z columns are present - if not {'X', 'Y', 'Z'}.issubset(gdf.columns): + if not {"X", "Y", "Z"}.issubset(gdf.columns): gdf = extract_xy(gdf=gdf) # Creating the spheres - spheres = pv.MultiBlock([pv.Sphere(radius=gdf.loc[i][magnitude] * magnitude_factor, - center=gdf.loc[i][['X', 'Y', 'Z']].tolist()) for i in range(len(gdf))]) + spheres = pv.MultiBlock( + [ + pv.Sphere( + radius=gdf.loc[i][magnitude] * magnitude_factor, + center=gdf.loc[i][["X", "Y", "Z"]].tolist(), + ) + for i in range(len(gdf)) + ] + ) # Adding magnitude array to spheres for i in range(len(spheres.keys())): - spheres[spheres.keys()[i]][magnitude] = np.zeros(len(spheres[spheres.keys()[i]].points)) + \ - gdf.loc[i][magnitude] + spheres[spheres.keys()[i]][magnitude] = ( + np.zeros(len(spheres[spheres.keys()[i]].points)) + gdf.loc[i][magnitude] + ) if year in gdf: for i in range(len(spheres.keys())): - spheres[spheres.keys()[i]][year] = np.zeros(len(spheres[spheres.keys()[i]].points)) + gdf.loc[i][year] + spheres[spheres.keys()[i]][year] = ( + np.zeros(len(spheres[spheres.keys()[i]].points)) + gdf.loc[i][year] + ) return spheres -def plane_through_hypocenters(spheres: pv.core.composite.MultiBlock) -> pv.core.pointset.PolyData: +def plane_through_hypocenters( + spheres: pv.core.composite.MultiBlock, +) -> pv.core.pointset.PolyData: """Fitting a plane through the hypocenters of earthquakes using Eigenvector analysis Parameters @@ -3222,10 +3407,12 @@ def plane_through_hypocenters(spheres: pv.core.composite.MultiBlock) -> pv.core. # Checking that the input data is a PyVista PolyData dataset if not isinstance(spheres, pv.core.composite.MultiBlock): - raise TypeError('Input data must be of type PyVista PolyData') + raise TypeError("Input data must be of type PyVista PolyData") # Creating array of centers of the spheres - centers = np.array([spheres.GetBlock(block).center for block in range(spheres.GetNumberOfBlocks())]) + centers = np.array( + [spheres.GetBlock(block).center for block in range(spheres.GetNumberOfBlocks())] + ) # Defining origin of plane as mean of the location of all hypocenters center = [centers[:, 0].mean(), centers[:, 1].mean(), centers[:, 2].mean()] @@ -3246,22 +3433,24 @@ def plane_through_hypocenters(spheres: pv.core.composite.MultiBlock) -> pv.core. # TODO: Refactor when refactoring GemGIS Data Object -def plot_data(geo_data, - show_basemap: bool = False, - show_geolmap: bool = False, - show_topo: bool = False, - show_interfaces: bool = False, - show_orientations: bool = False, - show_customsections: bool = False, - show_wms: bool = False, - show_legend: bool = True, - show_hillshades: bool = False, - show_slope: bool = False, - show_aspect: bool = False, - show_contours: bool = False, - add_to_extent: float = 0, - hide_topo_left: bool = False, - **kwargs): +def plot_data( + geo_data, + show_basemap: bool = False, + show_geolmap: bool = False, + show_topo: bool = False, + show_interfaces: bool = False, + show_orientations: bool = False, + show_customsections: bool = False, + show_wms: bool = False, + show_legend: bool = True, + show_hillshades: bool = False, + show_slope: bool = False, + show_aspect: bool = False, + show_contours: bool = False, + add_to_extent: float = 0, + hide_topo_left: bool = False, + **kwargs, +): """Plotting Input Data Parameters @@ -3340,103 +3529,133 @@ def plot_data(geo_data, import matplotlib.pyplot as plt except ModuleNotFoundError: raise ModuleNotFoundError( - 'Matplotlib package is not installed. Use pip install matplotlib to install the latest version') + "Matplotlib package is not installed. Use pip install matplotlib to install the latest version" + ) # Trying to import matplotlib but returning error if matplotlib is not installed try: from matplotlib.colors import ListedColormap except ModuleNotFoundError: raise ModuleNotFoundError( - 'Matplotlib package is not installed. Use pip install matplotlib to install the latest version') + "Matplotlib package is not installed. Use pip install matplotlib to install the latest version" + ) # Converting GeoDataFrame extent to list extent if isinstance(geo_data.extent, gpd.geodataframe.GeoDataFrame): geo_data.extent = set_extent(gdf=geo_data.extent) # Getting and checking kwargs - cmap_basemap = kwargs.get('cmap_basemap', 'gray') + cmap_basemap = kwargs.get("cmap_basemap", "gray") if not isinstance(cmap_basemap, (str, type(None))): - raise TypeError('Colormap must be of type string') + raise TypeError("Colormap must be of type string") # Getting and checking kwargs - cmap_geolmap = kwargs.get('cmap_geolmap', 'gray') + cmap_geolmap = kwargs.get("cmap_geolmap", "gray") if not isinstance(cmap_geolmap, (str, type(None), list)): - raise TypeError('Colormap must be of type string') + raise TypeError("Colormap must be of type string") - cmap_topo = kwargs.get('cmap_topo', 'gist_earth') + cmap_topo = kwargs.get("cmap_topo", "gist_earth") if not isinstance(cmap_topo, (str, type(None))): - raise TypeError('Colormap must be of type string') + raise TypeError("Colormap must be of type string") - cmap_contours = kwargs.get('cmap_contours', 'gist_earth') + cmap_contours = kwargs.get("cmap_contours", "gist_earth") if not isinstance(cmap_contours, (str, type(None))): - raise TypeError('Colormap must be of type string') + raise TypeError("Colormap must be of type string") - cmap_hillshades = kwargs.get('cmap_hillshades', 'gray') + cmap_hillshades = kwargs.get("cmap_hillshades", "gray") if not isinstance(cmap_hillshades, (str, type(None))): - raise TypeError('Colormap must be of type string') + raise TypeError("Colormap must be of type string") - cmap_slope = kwargs.get('cmap_slope', 'RdYlBu_r') + cmap_slope = kwargs.get("cmap_slope", "RdYlBu_r") if not isinstance(cmap_slope, (str, type(None))): - raise TypeError('Colormap must be of type string') + raise TypeError("Colormap must be of type string") - cmap_aspect = kwargs.get('cmap_aspect', 'twilight_shifted') + cmap_aspect = kwargs.get("cmap_aspect", "twilight_shifted") if not isinstance(cmap_aspect, (str, type(None))): - raise TypeError('Colormap must be of type string') + raise TypeError("Colormap must be of type string") - cmap_interfaces = kwargs.get('cmap_interfaces', 'gray') + cmap_interfaces = kwargs.get("cmap_interfaces", "gray") if not isinstance(cmap_interfaces, (list, str, type(None))): - raise TypeError('Colormap must be of type string') + raise TypeError("Colormap must be of type string") - cmap_orientations = kwargs.get('cmap_orientations', 'gray') + cmap_orientations = kwargs.get("cmap_orientations", "gray") if not isinstance(cmap_orientations, (list, str, type(None))): - raise TypeError('Colormap must be of type string') + raise TypeError("Colormap must be of type string") - cmap_wms = kwargs.get('cmap_wms', None) + cmap_wms = kwargs.get("cmap_wms", None) if not isinstance(cmap_wms, (str, type(None))): - raise TypeError('Colormap must be of type string') + raise TypeError("Colormap must be of type string") # Creating figure and axes - fig, (ax1, ax2) = plt.subplots(ncols=2, sharex='all', sharey='all', figsize=(20, 10)) + fig, (ax1, ax2) = plt.subplots( + ncols=2, sharex="all", sharey="all", figsize=(20, 10) + ) # Plot basemap if show_basemap: if not isinstance(geo_data.basemap, type(None)): - ax1.imshow(np.flipud(geo_data.basemap), origin='lower', cmap=cmap_basemap, extent=geo_data.extent[:4]) + ax1.imshow( + np.flipud(geo_data.basemap), + origin="lower", + cmap=cmap_basemap, + extent=geo_data.extent[:4], + ) # Plot geological map if show_geolmap: if isinstance(geo_data.geolmap, np.ndarray): - ax1.imshow(np.flipud(geo_data.geolmap), origin='lower', cmap=cmap_geolmap, extent=geo_data.extent[:4]) + ax1.imshow( + np.flipud(geo_data.geolmap), + origin="lower", + cmap=cmap_geolmap, + extent=geo_data.extent[:4], + ) else: - geo_data.geolmap.plot(ax=ax1, column='formation', alpha=0.75, legend=True, - cmap=ListedColormap(cmap_geolmap), aspect='equal') + geo_data.geolmap.plot( + ax=ax1, + column="formation", + alpha=0.75, + legend=True, + cmap=ListedColormap(cmap_geolmap), + aspect="equal", + ) # Plot WMS Layer if show_wms: if not isinstance(geo_data.wms, type(None)): - ax1.imshow(np.flipud(geo_data.wms), origin='lower', cmap=cmap_wms, extent=geo_data.extent[:4]) + ax1.imshow( + np.flipud(geo_data.wms), + origin="lower", + cmap=cmap_wms, + extent=geo_data.extent[:4], + ) # Plot topography if show_topo: if not hide_topo_left: if not isinstance(geo_data.raw_dem, type(None)): if isinstance(geo_data.raw_dem, np.ndarray): - ax1.imshow(np.flipud(geo_data.raw_dem), origin='lower', cmap=cmap_topo, extent=geo_data.extent[:4], - alpha=0.5) + ax1.imshow( + np.flipud(geo_data.raw_dem), + origin="lower", + cmap=cmap_topo, + extent=geo_data.extent[:4], + alpha=0.5, + ) # Set labels, grid and limits - ax1.set_xlabel('X') - ax1.set_ylabel('Y') + ax1.set_xlabel("X") + ax1.set_ylabel("Y") ax1.grid() ax1.set_ylim(geo_data.extent[2] - add_to_extent, geo_data.extent[3] + add_to_extent) ax1.set_xlim(geo_data.extent[0] - add_to_extent, geo_data.extent[1] + add_to_extent) @@ -3444,78 +3663,161 @@ def plot_data(geo_data, # Plot basemap if show_basemap: if not isinstance(geo_data.basemap, type(None)): - ax2.imshow(np.flipud(geo_data.basemap), origin='lower', cmap=cmap_basemap, extent=geo_data.extent[:4]) + ax2.imshow( + np.flipud(geo_data.basemap), + origin="lower", + cmap=cmap_basemap, + extent=geo_data.extent[:4], + ) # Plot geolmap if show_geolmap: if isinstance(geo_data.geolmap, np.ndarray): - ax2.imshow(np.flipud(geo_data.geolmap), origin='lower', cmap=cmap_geolmap, extent=geo_data.extent[:4]) + ax2.imshow( + np.flipud(geo_data.geolmap), + origin="lower", + cmap=cmap_geolmap, + extent=geo_data.extent[:4], + ) else: - geo_data.geolmap.plot(ax=ax2, column='formation', alpha=0.75, legend=True, - cmap=ListedColormap(cmap_geolmap), aspect='equal') + geo_data.geolmap.plot( + ax=ax2, + column="formation", + alpha=0.75, + legend=True, + cmap=ListedColormap(cmap_geolmap), + aspect="equal", + ) # Plot topography if show_topo: if not isinstance(geo_data.raw_dem, type(None)): if isinstance(geo_data.raw_dem, np.ndarray): - ax2.imshow(np.flipud(geo_data.raw_dem), origin='lower', cmap=cmap_topo, extent=geo_data.extent[:4], - alpha=0.5) + ax2.imshow( + np.flipud(geo_data.raw_dem), + origin="lower", + cmap=cmap_topo, + extent=geo_data.extent[:4], + alpha=0.5, + ) else: - geo_data.raw_dem.plot(ax=ax2, column='Z', legend=False, linewidth=5, cmap=cmap_topo, aspect='equal') + geo_data.raw_dem.plot( + ax=ax2, + column="Z", + legend=False, + linewidth=5, + cmap=cmap_topo, + aspect="equal", + ) # Plot contours if show_contours: if not isinstance(geo_data.contours, type(None)): - geo_data.contours.plot(ax=ax2, column='Z', legend=False, linewidth=5, cmap=cmap_contours, aspect='equal') + geo_data.contours.plot( + ax=ax2, + column="Z", + legend=False, + linewidth=5, + cmap=cmap_contours, + aspect="equal", + ) # Plot WMS Layer if show_wms: if not isinstance(geo_data.wms, type(None)): - ax2.imshow(np.flipud(geo_data.wms), origin='lower', cmap=cmap_wms, extent=geo_data.extent[:4]) + ax2.imshow( + np.flipud(geo_data.wms), + origin="lower", + cmap=cmap_wms, + extent=geo_data.extent[:4], + ) # Plot hillshades if show_hillshades: if not isinstance(geo_data.hillshades, type(None)): - ax2.imshow(np.flipud(geo_data.hillshades), origin='lower', cmap=cmap_hillshades, extent=geo_data.extent[:4]) + ax2.imshow( + np.flipud(geo_data.hillshades), + origin="lower", + cmap=cmap_hillshades, + extent=geo_data.extent[:4], + ) # Plot slope if show_slope: if not isinstance(geo_data.slope, type(None)): - ax2.imshow(np.flipud(geo_data.slope), origin='lower', cmap=cmap_slope, extent=geo_data.extent[:4]) + ax2.imshow( + np.flipud(geo_data.slope), + origin="lower", + cmap=cmap_slope, + extent=geo_data.extent[:4], + ) # Plot aspect if show_aspect: if not isinstance(geo_data.aspect, type(None)): - ax2.imshow(np.flipud(geo_data.aspect), origin='lower', cmap=cmap_aspect, extent=geo_data.extent[:4]) + ax2.imshow( + np.flipud(geo_data.aspect), + origin="lower", + cmap=cmap_aspect, + extent=geo_data.extent[:4], + ) # Plot interfaces and orientations if show_interfaces: if not isinstance(geo_data.raw_i, type(None)): - if all(geo_data.raw_i.geom_type == 'Point'): - geo_data.raw_i.plot(ax=ax2, column='formation', legend=show_legend, s=200, aspect='equal') - elif all(geo_data.raw_i.geom_type == 'LineString'): - geo_data.raw_i.plot(ax=ax2, column='formation', legend=show_legend, linewidth=5, - cmap=cmap_interfaces, aspect='equal') + if all(geo_data.raw_i.geom_type == "Point"): + geo_data.raw_i.plot( + ax=ax2, + column="formation", + legend=show_legend, + s=200, + aspect="equal", + ) + elif all(geo_data.raw_i.geom_type == "LineString"): + geo_data.raw_i.plot( + ax=ax2, + column="formation", + legend=show_legend, + linewidth=5, + cmap=cmap_interfaces, + aspect="equal", + ) else: if not cmap_interfaces: - geo_data.raw_i.plot(ax=ax2, column='formation', legend=show_legend, aspect='equal') + geo_data.raw_i.plot( + ax=ax2, column="formation", legend=show_legend, aspect="equal" + ) else: - geo_data.raw_i.plot(ax=ax2, column='formation', legend=show_legend, - cmap=ListedColormap(cmap_interfaces), aspect='equal') + geo_data.raw_i.plot( + ax=ax2, + column="formation", + legend=show_legend, + cmap=ListedColormap(cmap_interfaces), + aspect="equal", + ) if show_orientations: if not isinstance(geo_data.raw_o, type(None)): - geo_data.raw_o.plot(ax=ax2, column='formation', legend=True, s=200, aspect='equal', cmap=cmap_orientations) + geo_data.raw_o.plot( + ax=ax2, + column="formation", + legend=True, + s=200, + aspect="equal", + cmap=cmap_orientations, + ) # Plot custom sections if show_customsections: if not isinstance(geo_data.customsections, type(None)): - geo_data.customsections.plot(ax=ax2, legend=show_legend, linewidth=5, color='red', aspect='equal') + geo_data.customsections.plot( + ax=ax2, legend=show_legend, linewidth=5, color="red", aspect="equal" + ) # Set labels, grid and limits - ax2.set_xlabel('X') - ax2.set_ylabel('Y') + ax2.set_xlabel("X") + ax2.set_ylabel("Y") ax2.grid() ax2.set_ylim(geo_data.extent[2] - add_to_extent, geo_data.extent[3] + add_to_extent) ax2.set_xlim(geo_data.extent[0] - add_to_extent, geo_data.extent[1] + add_to_extent) @@ -3523,9 +3825,11 @@ def plot_data(geo_data, return fig, ax1, ax2 -def clip_seismic_data(seismic_data, - cdp_start: Union[int, type(None)] = None, - cdp_end: Union[int, type(None)] = None) -> pd.DataFrame: +def clip_seismic_data( + seismic_data, + cdp_start: Union[int, type(None)] = None, + cdp_end: Union[int, type(None)] = None, +) -> pd.DataFrame: """Clipping seismic data loaded with segysak to CDP defined start and end CDP values Parameters @@ -3555,20 +3859,22 @@ def clip_seismic_data(seismic_data, import xarray except ModuleNotFoundError: raise ModuleNotFoundError( - 'xarray package is not installed. Use pip install xarray to install the latest version') + "xarray package is not installed. Use pip install xarray to install the latest version" + ) # Checking that the seismic data is provided as xarray Dataset if not isinstance(seismic_data, xarray.core.dataset.Dataset): raise TypeError( - 'The seismic data must be provided as xarray Dataset loaded ideally with segysak and its segy_loader') + "The seismic data must be provided as xarray Dataset loaded ideally with segysak and its segy_loader" + ) # Checking that cdp_start ist of type int or None if not isinstance(cdp_start, (int, type(None))): - raise TypeError('The start CDP must be provided as int') + raise TypeError("The start CDP must be provided as int") # Checking that cdp_end ist of type int or None if not isinstance(cdp_end, (int, type(None))): - raise TypeError('The end CDP must be provided as int') + raise TypeError("The end CDP must be provided as int") # Converting xarray DataSet to DataFrame df_seismic_data = seismic_data.to_dataframe() @@ -3587,10 +3893,12 @@ def clip_seismic_data(seismic_data, return df_seismic_data_selection -def seismic_to_array(seismic_data, - cdp_start: Union[int, type(None)] = None, - cdp_end: Union[int, type(None)] = None, - max_depth: Union[int, float, type(None)] = None) -> np.ndarray: +def seismic_to_array( + seismic_data, + cdp_start: Union[int, type(None)] = None, + cdp_end: Union[int, type(None)] = None, + max_depth: Union[int, float, type(None)] = None, +) -> np.ndarray: """Converting seismic data loaded with segysak to a NumPy array Parameters @@ -3623,24 +3931,28 @@ def seismic_to_array(seismic_data, import xarray except ModuleNotFoundError: raise ModuleNotFoundError( - 'xarray package is not installed. Use pip install xarray to install the latest version') + "xarray package is not installed. Use pip install xarray to install the latest version" + ) # Checking that the seismic data is provided as xarray Dataset if not isinstance(seismic_data, xarray.core.dataset.Dataset): raise TypeError( - 'The seismic data must be provided as xarray Dataset loaded ideally with segysak and its segy_loader') + "The seismic data must be provided as xarray Dataset loaded ideally with segysak and its segy_loader" + ) # Checking that cdp_start ist of type int or None if not isinstance(cdp_start, (int, type(None))): - raise TypeError('The start CDP must be provided as int') + raise TypeError("The start CDP must be provided as int") # Checking that cdp_end ist of type int or None if not isinstance(cdp_end, (int, type(None))): - raise TypeError('The end CDP must be provided as int') + raise TypeError("The end CDP must be provided as int") # Checking that the max_depth is of type int or float if not isinstance(max_depth, (int, float, type(None))): - raise TypeError('The maximum depth in m or TWT must be provided as int or float') + raise TypeError( + "The maximum depth in m or TWT must be provided as int or float" + ) # Converting xarray DataSet to DataFrame df_seismic_data = seismic_data.to_dataframe() @@ -3654,46 +3966,54 @@ def seismic_to_array(seismic_data, cdp_end = int(df_seismic_data.index[-1][0]) # Clipping the seismic data - df_seismic_data_selection = clip_seismic_data(seismic_data=seismic_data, - cdp_start=cdp_start, - cdp_end=cdp_end) + df_seismic_data_selection = clip_seismic_data( + seismic_data=seismic_data, cdp_start=cdp_start, cdp_end=cdp_end + ) # Getting the number of rows per CDP and number of cdps len_cdp = int(len(df_seismic_data_selection.loc[cdp_start])) num_cdp = int(len(df_seismic_data_selection) / len_cdp) # Getting the seismic data - df_seismic_data_values = df_seismic_data_selection['data'].values + df_seismic_data_values = df_seismic_data_selection["data"].values # Reshaping the array - df_seismic_data_values_reshaped = df_seismic_data_values.reshape(num_cdp, - len_cdp) + df_seismic_data_values_reshaped = df_seismic_data_values.reshape(num_cdp, len_cdp) # Getting the max_depth if it is not provided if not max_depth: max_depth = df_seismic_data_selection.loc[cdp_start].index[-1] # Getting the number of samples based on max_depth - num_indices = int((len_cdp - 1) / ( - df_seismic_data_selection.loc[cdp_start].index.max() - df_seismic_data_selection.loc[ - cdp_start].index.min()) * max_depth + 1) + num_indices = int( + (len_cdp - 1) + / ( + df_seismic_data_selection.loc[cdp_start].index.max() + - df_seismic_data_selection.loc[cdp_start].index.min() + ) + * max_depth + + 1 + ) # Selecting samples - df_seismic_data_values_reshaped_selected = df_seismic_data_values_reshaped[:, :num_indices] + df_seismic_data_values_reshaped_selected = df_seismic_data_values_reshaped[ + :, :num_indices + ] return df_seismic_data_values_reshaped_selected -def seismic_to_mesh(seismic_data, - cdp_start: Union[int, type(None)] = None, - cdp_end: Union[int, type(None)] = None, - max_depth: Union[int, float] = None, - sampling_rate: Union[int, type(None)] = None, - shift: Union[int, float] = 0, - source_crs: Union[str, pyproj.crs.crs.CRS] = None, - target_crs: Union[str, pyproj.crs.crs.CRS] = None, - cdp_coords=None, - ) -> pv.core.pointset.StructuredGrid: +def seismic_to_mesh( + seismic_data, + cdp_start: Union[int, type(None)] = None, + cdp_end: Union[int, type(None)] = None, + max_depth: Union[int, float] = None, + sampling_rate: Union[int, type(None)] = None, + shift: Union[int, float] = 0, + source_crs: Union[str, pyproj.crs.crs.CRS] = None, + target_crs: Union[str, pyproj.crs.crs.CRS] = None, + cdp_coords=None, +) -> pv.core.pointset.StructuredGrid: """Converting seismic data loaded with segysak to a PyVista Mesh Parameters @@ -3738,42 +4058,46 @@ def seismic_to_mesh(seismic_data, # Checking that the sampling_rate is provided if not isinstance(sampling_rate, (int, type(None))): - raise TypeError('The sampling rate must be provided as integer') + raise TypeError("The sampling rate must be provided as integer") # Checking that the shift is of type int if not isinstance(shift, int): - raise TypeError('The shift must be provided as integer') + raise TypeError("The shift must be provided as integer") # Checking that the target_crs is of type string if not isinstance(source_crs, (str, type(None), pyproj.crs.crs.CRS)): - raise TypeError('source_crs must be of type string or a pyproj object') + raise TypeError("source_crs must be of type string or a pyproj object") # Checking that the target_crs is of type string if not isinstance(target_crs, (str, type(None), pyproj.crs.crs.CRS)): - raise TypeError('target_crs must be of type string or a pyproj object') + raise TypeError("target_crs must be of type string or a pyproj object") # Getting the sampling rate if it is not provided if not sampling_rate: - sampling_rate = seismic_data.to_dataframe().reset_index()['twt'][1] - \ - seismic_data.to_dataframe().reset_index()['twt'][0] + sampling_rate = ( + seismic_data.to_dataframe().reset_index()["twt"][1] + - seismic_data.to_dataframe().reset_index()["twt"][0] + ) # Getting the seismic data as array - seismic_data_array = seismic_to_array(seismic_data=seismic_data, - cdp_start=cdp_start, - cdp_end=cdp_end, - max_depth=max_depth) + seismic_data_array = seismic_to_array( + seismic_data=seismic_data, + cdp_start=cdp_start, + cdp_end=cdp_end, + max_depth=max_depth, + ) # Getting the number of traces and samples (columns and rows) ntraces, nsamples = seismic_data_array.shape # Clipping the seismic data - seismic_data = clip_seismic_data(seismic_data=seismic_data, - cdp_start=cdp_start, - cdp_end=cdp_end) + seismic_data = clip_seismic_data( + seismic_data=seismic_data, cdp_start=cdp_start, cdp_end=cdp_end + ) # Getting the CDP coordinates try: - cdp_coordinates = seismic_data[['cdp_x', 'cdp_y']].drop_duplicates().values + cdp_coordinates = seismic_data[["cdp_x", "cdp_y"]].drop_duplicates().values cdp_x = cdp_coordinates[:, 0] cdp_y = cdp_coordinates[:, 1] @@ -3784,12 +4108,13 @@ def seismic_to_mesh(seismic_data, cdp_coordinates = cdp_coords # Converting the coordinates if target_crs and source_crs: - gdf_coords = gpd.GeoDataFrame(geometry=gpd.points_from_xy(x=cdp_x, - y=cdp_y), - crs=source_crs).to_crs(target_crs) + gdf_coords = gpd.GeoDataFrame( + geometry=gpd.points_from_xy(x=cdp_x, y=cdp_y), crs=source_crs + ).to_crs(target_crs) - cdp_coordinates = np.array([gdf_coords.geometry.x.values, - gdf_coords.geometry.y.values]).T + cdp_coordinates = np.array( + [gdf_coords.geometry.x.values, gdf_coords.geometry.y.values] + ).T # Creating the path seismic_path = np.c_[cdp_coordinates, np.zeros(len(cdp_coordinates))] @@ -3826,262 +4151,266 @@ def get_seismic_cmap() -> matplotlib.colors.ListedColormap: """ - seismic = np.array([[0.63137255, 1., 1.], - [0.62745098, 0.97647059, 0.99607843], - [0.61960784, 0.95686275, 0.98823529], - [0.61568627, 0.93333333, 0.98431373], - [0.60784314, 0.91372549, 0.98039216], - [0.60392157, 0.89019608, 0.97254902], - [0.59607843, 0.87058824, 0.96862745], - [0.59215686, 0.85098039, 0.96078431], - [0.58431373, 0.83137255, 0.95686275], - [0.58039216, 0.81568627, 0.94901961], - [0.57254902, 0.79607843, 0.94117647], - [0.56862745, 0.77647059, 0.9372549], - [0.56078431, 0.76078431, 0.92941176], - [0.55686275, 0.74509804, 0.92156863], - [0.54901961, 0.72941176, 0.91764706], - [0.54509804, 0.70980392, 0.90980392], - [0.5372549, 0.69411765, 0.90196078], - [0.53333333, 0.68235294, 0.89803922], - [0.5254902, 0.66666667, 0.89019608], - [0.52156863, 0.65098039, 0.88235294], - [0.51372549, 0.63921569, 0.87843137], - [0.50980392, 0.62352941, 0.87058824], - [0.50196078, 0.61176471, 0.8627451], - [0.49803922, 0.59607843, 0.85490196], - [0.49411765, 0.58431373, 0.85098039], - [0.48627451, 0.57254902, 0.84313725], - [0.48235294, 0.56078431, 0.83529412], - [0.47843137, 0.54901961, 0.82745098], - [0.4745098, 0.54117647, 0.82352941], - [0.46666667, 0.52941176, 0.81568627], - [0.4627451, 0.51764706, 0.80784314], - [0.45882353, 0.50980392, 0.8], - [0.45490196, 0.49803922, 0.79607843], - [0.45098039, 0.49019608, 0.78823529], - [0.44705882, 0.48235294, 0.78039216], - [0.44313725, 0.4745098, 0.77254902], - [0.43921569, 0.46666667, 0.76862745], - [0.43529412, 0.45882353, 0.76078431], - [0.43529412, 0.45098039, 0.75294118], - [0.43137255, 0.44313725, 0.74901961], - [0.42745098, 0.43529412, 0.74117647], - [0.42352941, 0.43137255, 0.7372549], - [0.42352941, 0.42352941, 0.72941176], - [0.41960784, 0.41960784, 0.72156863], - [0.41960784, 0.41176471, 0.71764706], - [0.41568627, 0.40784314, 0.70980392], - [0.41568627, 0.4, 0.70588235], - [0.41176471, 0.39607843, 0.69803922], - [0.41176471, 0.39215686, 0.69411765], - [0.41176471, 0.38823529, 0.68627451], - [0.40784314, 0.38431373, 0.68235294], - [0.40784314, 0.38039216, 0.67843137], - [0.40784314, 0.38039216, 0.67058824], - [0.40784314, 0.37647059, 0.66666667], - [0.40784314, 0.37254902, 0.6627451], - [0.40392157, 0.37254902, 0.65490196], - [0.40392157, 0.36862745, 0.65098039], - [0.40392157, 0.36862745, 0.64705882], - [0.40392157, 0.36470588, 0.64313725], - [0.40784314, 0.36470588, 0.63529412], - [0.40784314, 0.36470588, 0.63137255], - [0.40784314, 0.36078431, 0.62745098], - [0.40784314, 0.36078431, 0.62352941], - [0.40784314, 0.36078431, 0.61960784], - [0.40784314, 0.36078431, 0.61568627], - [0.41176471, 0.36078431, 0.61176471], - [0.41176471, 0.36078431, 0.60784314], - [0.41176471, 0.36470588, 0.60392157], - [0.41568627, 0.36470588, 0.60392157], - [0.41568627, 0.36470588, 0.6], - [0.41960784, 0.36862745, 0.59607843], - [0.41960784, 0.36862745, 0.59215686], - [0.42352941, 0.36862745, 0.59215686], - [0.42352941, 0.37254902, 0.58823529], - [0.42745098, 0.37647059, 0.58431373], - [0.43137255, 0.37647059, 0.58431373], - [0.43137255, 0.38039216, 0.58039216], - [0.43529412, 0.38431373, 0.58039216], - [0.43921569, 0.38823529, 0.57647059], - [0.44313725, 0.39215686, 0.57647059], - [0.44705882, 0.39607843, 0.57647059], - [0.44705882, 0.4, 0.57254902], - [0.45098039, 0.40392157, 0.57254902], - [0.45490196, 0.40784314, 0.57254902], - [0.45882353, 0.41176471, 0.57254902], - [0.4627451, 0.41568627, 0.57254902], - [0.46666667, 0.41960784, 0.57254902], - [0.47058824, 0.42745098, 0.57254902], - [0.4745098, 0.43137255, 0.57254902], - [0.48235294, 0.43529412, 0.57254902], - [0.48627451, 0.44313725, 0.57254902], - [0.49019608, 0.44705882, 0.57254902], - [0.49411765, 0.45490196, 0.57254902], - [0.50196078, 0.4627451, 0.57647059], - [0.50588235, 0.46666667, 0.57647059], - [0.50980392, 0.4745098, 0.58039216], - [0.51764706, 0.48235294, 0.58039216], - [0.52156863, 0.49019608, 0.58431373], - [0.52941176, 0.49803922, 0.58431373], - [0.53333333, 0.50196078, 0.58823529], - [0.54117647, 0.50980392, 0.59215686], - [0.54509804, 0.51764706, 0.59607843], - [0.55294118, 0.52941176, 0.59607843], - [0.56078431, 0.5372549, 0.6], - [0.56862745, 0.54509804, 0.60392157], - [0.57647059, 0.55294118, 0.61176471], - [0.58039216, 0.56078431, 0.61568627], - [0.58823529, 0.57254902, 0.61960784], - [0.59607843, 0.58039216, 0.62352941], - [0.60392157, 0.58823529, 0.63137255], - [0.61176471, 0.6, 0.63529412], - [0.62352941, 0.60784314, 0.64313725], - [0.63137255, 0.61960784, 0.64705882], - [0.63921569, 0.62745098, 0.65490196], - [0.64705882, 0.63921569, 0.6627451], - [0.65882353, 0.65098039, 0.67058824], - [0.66666667, 0.65882353, 0.67843137], - [0.67843137, 0.67058824, 0.68627451], - [0.68627451, 0.68235294, 0.69411765], - [0.69803922, 0.69411765, 0.70196078], - [0.70588235, 0.70588235, 0.71372549], - [0.71764706, 0.71372549, 0.72156863], - [0.72941176, 0.7254902, 0.73333333], - [0.74117647, 0.7372549, 0.74117647], - [0.75294118, 0.74901961, 0.75294118], - [0.76470588, 0.76470588, 0.76470588], - [0.77647059, 0.77647059, 0.77647059], - [0.78823529, 0.78823529, 0.78823529], - [0.79215686, 0.78823529, 0.78039216], - [0.78431373, 0.77254902, 0.76078431], - [0.77647059, 0.76078431, 0.74117647], - [0.76862745, 0.74901961, 0.7254902], - [0.76078431, 0.73333333, 0.70588235], - [0.75294118, 0.72156863, 0.68627451], - [0.74509804, 0.70980392, 0.67058824], - [0.74117647, 0.69803922, 0.65490196], - [0.73333333, 0.68627451, 0.63529412], - [0.72941176, 0.6745098, 0.61960784], - [0.72156863, 0.66666667, 0.60392157], - [0.71764706, 0.65490196, 0.58823529], - [0.70980392, 0.64313725, 0.57254902], - [0.70588235, 0.63137255, 0.55686275], - [0.70196078, 0.62352941, 0.54509804], - [0.69411765, 0.61176471, 0.52941176], - [0.69019608, 0.60392157, 0.51372549], - [0.68627451, 0.59215686, 0.50196078], - [0.68235294, 0.58431373, 0.48627451], - [0.67843137, 0.57647059, 0.4745098], - [0.6745098, 0.56470588, 0.4627451], - [0.67058824, 0.55686275, 0.45098039], - [0.66666667, 0.54901961, 0.43921569], - [0.66666667, 0.54117647, 0.42745098], - [0.6627451, 0.53333333, 0.41568627], - [0.65882353, 0.5254902, 0.40392157], - [0.65490196, 0.51764706, 0.39215686], - [0.65490196, 0.50980392, 0.38039216], - [0.65098039, 0.50196078, 0.36862745], - [0.64705882, 0.49803922, 0.36078431], - [0.64705882, 0.49019608, 0.34901961], - [0.64313725, 0.48235294, 0.34117647], - [0.64313725, 0.47843137, 0.32941176], - [0.64313725, 0.47058824, 0.32156863], - [0.63921569, 0.46666667, 0.31372549], - [0.63921569, 0.45882353, 0.30196078], - [0.63921569, 0.45490196, 0.29411765], - [0.63529412, 0.45098039, 0.28627451], - [0.63529412, 0.44313725, 0.27843137], - [0.63529412, 0.43921569, 0.27058824], - [0.63529412, 0.43529412, 0.2627451], - [0.63529412, 0.43137255, 0.25490196], - [0.63529412, 0.42745098, 0.24705882], - [0.63529412, 0.42352941, 0.23921569], - [0.63529412, 0.41960784, 0.23137255], - [0.63529412, 0.41568627, 0.22745098], - [0.63529412, 0.41176471, 0.21960784], - [0.63529412, 0.40784314, 0.21176471], - [0.63921569, 0.40392157, 0.20784314], - [0.63921569, 0.40392157, 0.2], - [0.63921569, 0.4, 0.19607843], - [0.63921569, 0.39607843, 0.18823529], - [0.64313725, 0.39607843, 0.18431373], - [0.64313725, 0.39607843, 0.17647059], - [0.64705882, 0.39215686, 0.17254902], - [0.64705882, 0.39215686, 0.16862745], - [0.65098039, 0.38823529, 0.16078431], - [0.65098039, 0.38823529, 0.15686275], - [0.65490196, 0.38823529, 0.15294118], - [0.65490196, 0.38823529, 0.14901961], - [0.65882353, 0.38823529, 0.14117647], - [0.6627451, 0.38823529, 0.1372549], - [0.66666667, 0.38823529, 0.13333333], - [0.66666667, 0.38823529, 0.12941176], - [0.67058824, 0.38823529, 0.1254902], - [0.6745098, 0.39215686, 0.12156863], - [0.67843137, 0.39215686, 0.11764706], - [0.68235294, 0.39215686, 0.11372549], - [0.68627451, 0.39607843, 0.10980392], - [0.69019608, 0.39607843, 0.10588235], - [0.69411765, 0.4, 0.10196078], - [0.69803922, 0.4, 0.09803922], - [0.70196078, 0.40392157, 0.09411765], - [0.70588235, 0.40784314, 0.09019608], - [0.70980392, 0.41176471, 0.08627451], - [0.71372549, 0.41568627, 0.08235294], - [0.71764706, 0.41960784, 0.07843137], - [0.7254902, 0.42352941, 0.0745098], - [0.72941176, 0.42745098, 0.07058824], - [0.73333333, 0.43137255, 0.06666667], - [0.7372549, 0.43529412, 0.0627451], - [0.74509804, 0.43921569, 0.05882353], - [0.74901961, 0.44705882, 0.05882353], - [0.75294118, 0.45098039, 0.05490196], - [0.76078431, 0.45882353, 0.05098039], - [0.76470588, 0.4627451, 0.04705882], - [0.77254902, 0.47058824, 0.04313725], - [0.77647059, 0.47843137, 0.03921569], - [0.78431373, 0.48627451, 0.03529412], - [0.78823529, 0.49411765, 0.03137255], - [0.79607843, 0.50196078, 0.02745098], - [0.8, 0.50980392, 0.02352941], - [0.80784314, 0.51764706, 0.01960784], - [0.81176471, 0.5254902, 0.01568627], - [0.81960784, 0.53333333, 0.01568627], - [0.82352941, 0.54509804, 0.01176471], - [0.83137255, 0.55294118, 0.00784314], - [0.83921569, 0.56078431, 0.00392157], - [0.84313725, 0.57254902, 0.00392157], - [0.85098039, 0.58431373, 0.], - [0.85490196, 0.59215686, 0.], - [0.8627451, 0.60392157, 0.], - [0.86666667, 0.61568627, 0.], - [0.8745098, 0.62745098, 0.], - [0.88235294, 0.63921569, 0.], - [0.88627451, 0.65098039, 0.], - [0.89411765, 0.6627451, 0.], - [0.89803922, 0.67843137, 0.], - [0.90588235, 0.69019608, 0.], - [0.91372549, 0.70196078, 0.], - [0.91764706, 0.71764706, 0.], - [0.9254902, 0.73333333, 0.], - [0.92941176, 0.74509804, 0.], - [0.93333333, 0.76078431, 0.], - [0.94117647, 0.77647059, 0.], - [0.94509804, 0.79215686, 0.], - [0.95294118, 0.80784314, 0.], - [0.95686275, 0.82352941, 0.], - [0.96078431, 0.83921569, 0.], - [0.96862745, 0.85490196, 0.], - [0.97254902, 0.87058824, 0.], - [0.97647059, 0.89019608, 0.], - [0.98039216, 0.90588235, 0.], - [0.98431373, 0.9254902, 0.], - [0.98823529, 0.94509804, 0.], - [0.99215686, 0.96078431, 0.], - [0.99607843, 0.98039216, 0.], - [1., 1., 0.]]) + seismic = np.array( + [ + [0.63137255, 1.0, 1.0], + [0.62745098, 0.97647059, 0.99607843], + [0.61960784, 0.95686275, 0.98823529], + [0.61568627, 0.93333333, 0.98431373], + [0.60784314, 0.91372549, 0.98039216], + [0.60392157, 0.89019608, 0.97254902], + [0.59607843, 0.87058824, 0.96862745], + [0.59215686, 0.85098039, 0.96078431], + [0.58431373, 0.83137255, 0.95686275], + [0.58039216, 0.81568627, 0.94901961], + [0.57254902, 0.79607843, 0.94117647], + [0.56862745, 0.77647059, 0.9372549], + [0.56078431, 0.76078431, 0.92941176], + [0.55686275, 0.74509804, 0.92156863], + [0.54901961, 0.72941176, 0.91764706], + [0.54509804, 0.70980392, 0.90980392], + [0.5372549, 0.69411765, 0.90196078], + [0.53333333, 0.68235294, 0.89803922], + [0.5254902, 0.66666667, 0.89019608], + [0.52156863, 0.65098039, 0.88235294], + [0.51372549, 0.63921569, 0.87843137], + [0.50980392, 0.62352941, 0.87058824], + [0.50196078, 0.61176471, 0.8627451], + [0.49803922, 0.59607843, 0.85490196], + [0.49411765, 0.58431373, 0.85098039], + [0.48627451, 0.57254902, 0.84313725], + [0.48235294, 0.56078431, 0.83529412], + [0.47843137, 0.54901961, 0.82745098], + [0.4745098, 0.54117647, 0.82352941], + [0.46666667, 0.52941176, 0.81568627], + [0.4627451, 0.51764706, 0.80784314], + [0.45882353, 0.50980392, 0.8], + [0.45490196, 0.49803922, 0.79607843], + [0.45098039, 0.49019608, 0.78823529], + [0.44705882, 0.48235294, 0.78039216], + [0.44313725, 0.4745098, 0.77254902], + [0.43921569, 0.46666667, 0.76862745], + [0.43529412, 0.45882353, 0.76078431], + [0.43529412, 0.45098039, 0.75294118], + [0.43137255, 0.44313725, 0.74901961], + [0.42745098, 0.43529412, 0.74117647], + [0.42352941, 0.43137255, 0.7372549], + [0.42352941, 0.42352941, 0.72941176], + [0.41960784, 0.41960784, 0.72156863], + [0.41960784, 0.41176471, 0.71764706], + [0.41568627, 0.40784314, 0.70980392], + [0.41568627, 0.4, 0.70588235], + [0.41176471, 0.39607843, 0.69803922], + [0.41176471, 0.39215686, 0.69411765], + [0.41176471, 0.38823529, 0.68627451], + [0.40784314, 0.38431373, 0.68235294], + [0.40784314, 0.38039216, 0.67843137], + [0.40784314, 0.38039216, 0.67058824], + [0.40784314, 0.37647059, 0.66666667], + [0.40784314, 0.37254902, 0.6627451], + [0.40392157, 0.37254902, 0.65490196], + [0.40392157, 0.36862745, 0.65098039], + [0.40392157, 0.36862745, 0.64705882], + [0.40392157, 0.36470588, 0.64313725], + [0.40784314, 0.36470588, 0.63529412], + [0.40784314, 0.36470588, 0.63137255], + [0.40784314, 0.36078431, 0.62745098], + [0.40784314, 0.36078431, 0.62352941], + [0.40784314, 0.36078431, 0.61960784], + [0.40784314, 0.36078431, 0.61568627], + [0.41176471, 0.36078431, 0.61176471], + [0.41176471, 0.36078431, 0.60784314], + [0.41176471, 0.36470588, 0.60392157], + [0.41568627, 0.36470588, 0.60392157], + [0.41568627, 0.36470588, 0.6], + [0.41960784, 0.36862745, 0.59607843], + [0.41960784, 0.36862745, 0.59215686], + [0.42352941, 0.36862745, 0.59215686], + [0.42352941, 0.37254902, 0.58823529], + [0.42745098, 0.37647059, 0.58431373], + [0.43137255, 0.37647059, 0.58431373], + [0.43137255, 0.38039216, 0.58039216], + [0.43529412, 0.38431373, 0.58039216], + [0.43921569, 0.38823529, 0.57647059], + [0.44313725, 0.39215686, 0.57647059], + [0.44705882, 0.39607843, 0.57647059], + [0.44705882, 0.4, 0.57254902], + [0.45098039, 0.40392157, 0.57254902], + [0.45490196, 0.40784314, 0.57254902], + [0.45882353, 0.41176471, 0.57254902], + [0.4627451, 0.41568627, 0.57254902], + [0.46666667, 0.41960784, 0.57254902], + [0.47058824, 0.42745098, 0.57254902], + [0.4745098, 0.43137255, 0.57254902], + [0.48235294, 0.43529412, 0.57254902], + [0.48627451, 0.44313725, 0.57254902], + [0.49019608, 0.44705882, 0.57254902], + [0.49411765, 0.45490196, 0.57254902], + [0.50196078, 0.4627451, 0.57647059], + [0.50588235, 0.46666667, 0.57647059], + [0.50980392, 0.4745098, 0.58039216], + [0.51764706, 0.48235294, 0.58039216], + [0.52156863, 0.49019608, 0.58431373], + [0.52941176, 0.49803922, 0.58431373], + [0.53333333, 0.50196078, 0.58823529], + [0.54117647, 0.50980392, 0.59215686], + [0.54509804, 0.51764706, 0.59607843], + [0.55294118, 0.52941176, 0.59607843], + [0.56078431, 0.5372549, 0.6], + [0.56862745, 0.54509804, 0.60392157], + [0.57647059, 0.55294118, 0.61176471], + [0.58039216, 0.56078431, 0.61568627], + [0.58823529, 0.57254902, 0.61960784], + [0.59607843, 0.58039216, 0.62352941], + [0.60392157, 0.58823529, 0.63137255], + [0.61176471, 0.6, 0.63529412], + [0.62352941, 0.60784314, 0.64313725], + [0.63137255, 0.61960784, 0.64705882], + [0.63921569, 0.62745098, 0.65490196], + [0.64705882, 0.63921569, 0.6627451], + [0.65882353, 0.65098039, 0.67058824], + [0.66666667, 0.65882353, 0.67843137], + [0.67843137, 0.67058824, 0.68627451], + [0.68627451, 0.68235294, 0.69411765], + [0.69803922, 0.69411765, 0.70196078], + [0.70588235, 0.70588235, 0.71372549], + [0.71764706, 0.71372549, 0.72156863], + [0.72941176, 0.7254902, 0.73333333], + [0.74117647, 0.7372549, 0.74117647], + [0.75294118, 0.74901961, 0.75294118], + [0.76470588, 0.76470588, 0.76470588], + [0.77647059, 0.77647059, 0.77647059], + [0.78823529, 0.78823529, 0.78823529], + [0.79215686, 0.78823529, 0.78039216], + [0.78431373, 0.77254902, 0.76078431], + [0.77647059, 0.76078431, 0.74117647], + [0.76862745, 0.74901961, 0.7254902], + [0.76078431, 0.73333333, 0.70588235], + [0.75294118, 0.72156863, 0.68627451], + [0.74509804, 0.70980392, 0.67058824], + [0.74117647, 0.69803922, 0.65490196], + [0.73333333, 0.68627451, 0.63529412], + [0.72941176, 0.6745098, 0.61960784], + [0.72156863, 0.66666667, 0.60392157], + [0.71764706, 0.65490196, 0.58823529], + [0.70980392, 0.64313725, 0.57254902], + [0.70588235, 0.63137255, 0.55686275], + [0.70196078, 0.62352941, 0.54509804], + [0.69411765, 0.61176471, 0.52941176], + [0.69019608, 0.60392157, 0.51372549], + [0.68627451, 0.59215686, 0.50196078], + [0.68235294, 0.58431373, 0.48627451], + [0.67843137, 0.57647059, 0.4745098], + [0.6745098, 0.56470588, 0.4627451], + [0.67058824, 0.55686275, 0.45098039], + [0.66666667, 0.54901961, 0.43921569], + [0.66666667, 0.54117647, 0.42745098], + [0.6627451, 0.53333333, 0.41568627], + [0.65882353, 0.5254902, 0.40392157], + [0.65490196, 0.51764706, 0.39215686], + [0.65490196, 0.50980392, 0.38039216], + [0.65098039, 0.50196078, 0.36862745], + [0.64705882, 0.49803922, 0.36078431], + [0.64705882, 0.49019608, 0.34901961], + [0.64313725, 0.48235294, 0.34117647], + [0.64313725, 0.47843137, 0.32941176], + [0.64313725, 0.47058824, 0.32156863], + [0.63921569, 0.46666667, 0.31372549], + [0.63921569, 0.45882353, 0.30196078], + [0.63921569, 0.45490196, 0.29411765], + [0.63529412, 0.45098039, 0.28627451], + [0.63529412, 0.44313725, 0.27843137], + [0.63529412, 0.43921569, 0.27058824], + [0.63529412, 0.43529412, 0.2627451], + [0.63529412, 0.43137255, 0.25490196], + [0.63529412, 0.42745098, 0.24705882], + [0.63529412, 0.42352941, 0.23921569], + [0.63529412, 0.41960784, 0.23137255], + [0.63529412, 0.41568627, 0.22745098], + [0.63529412, 0.41176471, 0.21960784], + [0.63529412, 0.40784314, 0.21176471], + [0.63921569, 0.40392157, 0.20784314], + [0.63921569, 0.40392157, 0.2], + [0.63921569, 0.4, 0.19607843], + [0.63921569, 0.39607843, 0.18823529], + [0.64313725, 0.39607843, 0.18431373], + [0.64313725, 0.39607843, 0.17647059], + [0.64705882, 0.39215686, 0.17254902], + [0.64705882, 0.39215686, 0.16862745], + [0.65098039, 0.38823529, 0.16078431], + [0.65098039, 0.38823529, 0.15686275], + [0.65490196, 0.38823529, 0.15294118], + [0.65490196, 0.38823529, 0.14901961], + [0.65882353, 0.38823529, 0.14117647], + [0.6627451, 0.38823529, 0.1372549], + [0.66666667, 0.38823529, 0.13333333], + [0.66666667, 0.38823529, 0.12941176], + [0.67058824, 0.38823529, 0.1254902], + [0.6745098, 0.39215686, 0.12156863], + [0.67843137, 0.39215686, 0.11764706], + [0.68235294, 0.39215686, 0.11372549], + [0.68627451, 0.39607843, 0.10980392], + [0.69019608, 0.39607843, 0.10588235], + [0.69411765, 0.4, 0.10196078], + [0.69803922, 0.4, 0.09803922], + [0.70196078, 0.40392157, 0.09411765], + [0.70588235, 0.40784314, 0.09019608], + [0.70980392, 0.41176471, 0.08627451], + [0.71372549, 0.41568627, 0.08235294], + [0.71764706, 0.41960784, 0.07843137], + [0.7254902, 0.42352941, 0.0745098], + [0.72941176, 0.42745098, 0.07058824], + [0.73333333, 0.43137255, 0.06666667], + [0.7372549, 0.43529412, 0.0627451], + [0.74509804, 0.43921569, 0.05882353], + [0.74901961, 0.44705882, 0.05882353], + [0.75294118, 0.45098039, 0.05490196], + [0.76078431, 0.45882353, 0.05098039], + [0.76470588, 0.4627451, 0.04705882], + [0.77254902, 0.47058824, 0.04313725], + [0.77647059, 0.47843137, 0.03921569], + [0.78431373, 0.48627451, 0.03529412], + [0.78823529, 0.49411765, 0.03137255], + [0.79607843, 0.50196078, 0.02745098], + [0.8, 0.50980392, 0.02352941], + [0.80784314, 0.51764706, 0.01960784], + [0.81176471, 0.5254902, 0.01568627], + [0.81960784, 0.53333333, 0.01568627], + [0.82352941, 0.54509804, 0.01176471], + [0.83137255, 0.55294118, 0.00784314], + [0.83921569, 0.56078431, 0.00392157], + [0.84313725, 0.57254902, 0.00392157], + [0.85098039, 0.58431373, 0.0], + [0.85490196, 0.59215686, 0.0], + [0.8627451, 0.60392157, 0.0], + [0.86666667, 0.61568627, 0.0], + [0.8745098, 0.62745098, 0.0], + [0.88235294, 0.63921569, 0.0], + [0.88627451, 0.65098039, 0.0], + [0.89411765, 0.6627451, 0.0], + [0.89803922, 0.67843137, 0.0], + [0.90588235, 0.69019608, 0.0], + [0.91372549, 0.70196078, 0.0], + [0.91764706, 0.71764706, 0.0], + [0.9254902, 0.73333333, 0.0], + [0.92941176, 0.74509804, 0.0], + [0.93333333, 0.76078431, 0.0], + [0.94117647, 0.77647059, 0.0], + [0.94509804, 0.79215686, 0.0], + [0.95294118, 0.80784314, 0.0], + [0.95686275, 0.82352941, 0.0], + [0.96078431, 0.83921569, 0.0], + [0.96862745, 0.85490196, 0.0], + [0.97254902, 0.87058824, 0.0], + [0.97647059, 0.89019608, 0.0], + [0.98039216, 0.90588235, 0.0], + [0.98431373, 0.9254902, 0.0], + [0.98823529, 0.94509804, 0.0], + [0.99215686, 0.96078431, 0.0], + [0.99607843, 0.98039216, 0.0], + [1.0, 1.0, 0.0], + ] + ) cmap_seismic = matplotlib.colors.ListedColormap(seismic) @@ -4090,11 +4419,10 @@ def get_seismic_cmap() -> matplotlib.colors.ListedColormap: gradient = np.vstack((gradient, gradient)) fig, ax = plt.subplots(nrows=1, figsize=(6, 1)) - fig.subplots_adjust(top=0.5, bottom=0.15, - left=0.2, right=1) - ax.set_title('Seismic Colorbar', fontsize=14) + fig.subplots_adjust(top=0.5, bottom=0.15, left=0.2, right=1) + ax.set_title("Seismic Colorbar", fontsize=14) - ax.imshow(gradient, aspect='auto', cmap=cmap_seismic) + ax.imshow(gradient, aspect="auto", cmap=cmap_seismic) # Turn off *all* ticks & spines, not just the ones with colormaps. ax.set_axis_off() @@ -4115,262 +4443,266 @@ def get_batlow_cmap() -> matplotlib.colors.ListedColormap: """ - batlow = np.array([[0.005193, 0.098238, 0.349842], - [0.009065, 0.104487, 0.350933], - [0.012963, 0.110779, 0.351992], - [0.016530, 0.116913, 0.353070], - [0.019936, 0.122985, 0.354120], - [0.023189, 0.129035, 0.355182], - [0.026291, 0.135044, 0.356210], - [0.029245, 0.140964, 0.357239], - [0.032053, 0.146774, 0.358239], - [0.034853, 0.152558, 0.359233], - [0.037449, 0.158313, 0.360216], - [0.039845, 0.163978, 0.361187], - [0.042104, 0.169557, 0.362151], - [0.044069, 0.175053, 0.363084], - [0.045905, 0.180460, 0.364007], - [0.047665, 0.185844, 0.364915], - [0.049378, 0.191076, 0.365810], - [0.050795, 0.196274, 0.366684], - [0.052164, 0.201323, 0.367524], - [0.053471, 0.206357, 0.368370], - [0.054721, 0.211234, 0.369184], - [0.055928, 0.216046, 0.369974], - [0.057033, 0.220754, 0.370750], - [0.058032, 0.225340, 0.371509], - [0.059164, 0.229842, 0.372252], - [0.060167, 0.234299, 0.372978], - [0.061052, 0.238625, 0.373691], - [0.062060, 0.242888, 0.374386], - [0.063071, 0.247085, 0.375050], - [0.063982, 0.251213, 0.375709], - [0.064936, 0.255264, 0.376362], - [0.065903, 0.259257, 0.376987], - [0.066899, 0.263188, 0.377594], - [0.067921, 0.267056, 0.378191], - [0.069002, 0.270922, 0.378774], - [0.070001, 0.274713, 0.379342], - [0.071115, 0.278497, 0.379895], - [0.072192, 0.282249, 0.380434], - [0.073440, 0.285942, 0.380957], - [0.074595, 0.289653, 0.381452], - [0.075833, 0.293321, 0.381922], - [0.077136, 0.296996, 0.382376], - [0.078517, 0.300622, 0.382814], - [0.079984, 0.304252, 0.383224], - [0.081553, 0.307858, 0.383598], - [0.083082, 0.311461, 0.383936], - [0.084778, 0.315043, 0.384240], - [0.086503, 0.318615, 0.384506], - [0.088353, 0.322167, 0.384731], - [0.090281, 0.325685, 0.384910], - [0.092304, 0.329220, 0.385040], - [0.094462, 0.332712, 0.385116], - [0.096618, 0.336161, 0.385134], - [0.099015, 0.339621, 0.385090], - [0.101481, 0.343036, 0.384981], - [0.104078, 0.346410, 0.384801], - [0.106842, 0.349774, 0.384548], - [0.109695, 0.353098, 0.384217], - [0.112655, 0.356391, 0.383807], - [0.115748, 0.359638, 0.383310], - [0.118992, 0.362849, 0.382713], - [0.122320, 0.366030, 0.382026], - [0.125889, 0.369160, 0.381259], - [0.129519, 0.372238, 0.380378], - [0.133298, 0.375282, 0.379395], - [0.137212, 0.378282, 0.378315], - [0.141260, 0.381240, 0.377135], - [0.145432, 0.384130, 0.375840], - [0.149706, 0.386975, 0.374449], - [0.154073, 0.389777, 0.372934], - [0.158620, 0.392531, 0.371320], - [0.163246, 0.395237, 0.369609], - [0.167952, 0.397889, 0.367784], - [0.172788, 0.400496, 0.365867], - [0.177752, 0.403041, 0.363833], - [0.182732, 0.405551, 0.361714], - [0.187886, 0.408003, 0.359484], - [0.193050, 0.410427, 0.357177], - [0.198310, 0.412798, 0.354767], - [0.203676, 0.415116, 0.352253], - [0.209075, 0.417412, 0.349677], - [0.214555, 0.419661, 0.347019], - [0.220112, 0.421864, 0.344261], - [0.225707, 0.424049, 0.341459], - [0.231362, 0.426197, 0.338572], - [0.237075, 0.428325, 0.335634], - [0.242795, 0.430418, 0.332635], - [0.248617, 0.432493, 0.329571], - [0.254452, 0.434529, 0.326434], - [0.260320, 0.436556, 0.323285], - [0.266241, 0.438555, 0.320085], - [0.272168, 0.440541, 0.316831], - [0.278171, 0.442524, 0.313552], - [0.284175, 0.444484, 0.310243], - [0.290214, 0.446420, 0.306889], - [0.296294, 0.448357, 0.303509], - [0.302379, 0.450282, 0.300122], - [0.308517, 0.452205, 0.296721], - [0.314648, 0.454107, 0.293279], - [0.320834, 0.456006, 0.289841], - [0.327007, 0.457900, 0.286377], - [0.333235, 0.459794, 0.282937], - [0.339469, 0.461685, 0.279468], - [0.345703, 0.463563, 0.275998], - [0.351976, 0.465440, 0.272492], - [0.358277, 0.467331, 0.269037], - [0.364589, 0.469213, 0.265543], - [0.370922, 0.471085, 0.262064], - [0.377291, 0.472952, 0.258588], - [0.383675, 0.474842, 0.255131], - [0.390070, 0.476711, 0.251665], - [0.396505, 0.478587, 0.248212], - [0.402968, 0.480466, 0.244731], - [0.409455, 0.482351, 0.241314], - [0.415967, 0.484225, 0.237895], - [0.422507, 0.486113, 0.234493], - [0.429094, 0.488011, 0.231096], - [0.435714, 0.489890, 0.227728], - [0.442365, 0.491795, 0.224354], - [0.449052, 0.493684, 0.221074], - [0.455774, 0.495585, 0.217774], - [0.462539, 0.497497, 0.214518], - [0.469368, 0.499393, 0.211318], - [0.476221, 0.501314, 0.208148], - [0.483123, 0.503216, 0.205037], - [0.490081, 0.505137, 0.201976], - [0.497089, 0.507058, 0.198994], - [0.504153, 0.508984, 0.196118], - [0.511253, 0.510898, 0.193296], - [0.518425, 0.512822, 0.190566], - [0.525637, 0.514746, 0.187990], - [0.532907, 0.516662, 0.185497], - [0.540225, 0.518584, 0.183099], - [0.547599, 0.520486, 0.180884], - [0.555024, 0.522391, 0.178854], - [0.562506, 0.524293, 0.176964], - [0.570016, 0.526186, 0.175273], - [0.577582, 0.528058, 0.173775], - [0.585199, 0.529927, 0.172493], - [0.592846, 0.531777, 0.171449], - [0.600520, 0.533605, 0.170648], - [0.608240, 0.535423, 0.170104], - [0.615972, 0.537231, 0.169826], - [0.623739, 0.539002, 0.169814], - [0.631513, 0.540752, 0.170075], - [0.639301, 0.542484, 0.170622], - [0.647098, 0.544183, 0.171465], - [0.654889, 0.545863, 0.172603], - [0.662691, 0.547503, 0.174044], - [0.670477, 0.549127, 0.175747], - [0.678244, 0.550712, 0.177803], - [0.685995, 0.552274, 0.180056], - [0.693720, 0.553797, 0.182610], - [0.701421, 0.555294, 0.185478], - [0.709098, 0.556772, 0.188546], - [0.716731, 0.558205, 0.191851], - [0.724322, 0.559628, 0.195408], - [0.731878, 0.561011, 0.199174], - [0.739393, 0.562386, 0.203179], - [0.746850, 0.563725, 0.207375], - [0.754268, 0.565033, 0.211761], - [0.761629, 0.566344, 0.216322], - [0.768942, 0.567630, 0.221045], - [0.776208, 0.568899, 0.225930], - [0.783416, 0.570162, 0.230962], - [0.790568, 0.571421, 0.236160], - [0.797665, 0.572682, 0.241490], - [0.804709, 0.573928, 0.246955], - [0.811692, 0.575187, 0.252572], - [0.818610, 0.576462, 0.258303], - [0.825472, 0.577725, 0.264197], - [0.832272, 0.579026, 0.270211], - [0.838999, 0.580339, 0.276353], - [0.845657, 0.581672, 0.282631], - [0.852247, 0.583037, 0.289036], - [0.858747, 0.584440, 0.295572], - [0.865168, 0.585882, 0.302255], - [0.871505, 0.587352, 0.309112], - [0.877741, 0.588873, 0.316081], - [0.883878, 0.590450, 0.323195], - [0.889900, 0.592087, 0.330454], - [0.895809, 0.593765, 0.337865], - [0.901590, 0.595507, 0.345429], - [0.907242, 0.597319, 0.353142], - [0.912746, 0.599191, 0.360986], - [0.918103, 0.601126, 0.368999], - [0.923300, 0.603137, 0.377139], - [0.928323, 0.605212, 0.385404], - [0.933176, 0.607369, 0.393817], - [0.937850, 0.609582, 0.402345], - [0.942332, 0.611867, 0.411006], - [0.946612, 0.614218, 0.419767], - [0.950697, 0.616649, 0.428624], - [0.954574, 0.619137, 0.437582], - [0.958244, 0.621671, 0.446604], - [0.961696, 0.624282, 0.455702], - [0.964943, 0.626934, 0.464860], - [0.967983, 0.629639, 0.474057], - [0.970804, 0.632394, 0.483290], - [0.973424, 0.635183, 0.492547], - [0.975835, 0.638012, 0.501826], - [0.978052, 0.640868, 0.511090], - [0.980079, 0.643752, 0.520350], - [0.981918, 0.646664, 0.529602], - [0.983574, 0.649590, 0.538819], - [0.985066, 0.652522, 0.547998], - [0.986392, 0.655470, 0.557142], - [0.987567, 0.658422, 0.566226], - [0.988596, 0.661378, 0.575265], - [0.989496, 0.664329, 0.584246], - [0.990268, 0.667280, 0.593174], - [0.990926, 0.670230, 0.602031], - [0.991479, 0.673165, 0.610835], - [0.991935, 0.676091, 0.619575], - [0.992305, 0.679007, 0.628251], - [0.992595, 0.681914, 0.636869], - [0.992813, 0.684815, 0.645423], - [0.992967, 0.687705, 0.653934], - [0.993064, 0.690579, 0.662398], - [0.993111, 0.693451, 0.670810], - [0.993112, 0.696314, 0.679177], - [0.993074, 0.699161, 0.687519], - [0.993002, 0.702006, 0.695831], - [0.992900, 0.704852, 0.704114], - [0.992771, 0.707689, 0.712380], - [0.992619, 0.710530, 0.720639], - [0.992447, 0.713366, 0.728892], - [0.992258, 0.716210, 0.737146], - [0.992054, 0.719049, 0.745403], - [0.991837, 0.721893, 0.753673], - [0.991607, 0.724754, 0.761959], - [0.991367, 0.727614, 0.770270], - [0.991116, 0.730489, 0.778606], - [0.990855, 0.733373, 0.786976], - [0.990586, 0.736265, 0.795371], - [0.990307, 0.739184, 0.803810], - [0.990018, 0.742102, 0.812285], - [0.989720, 0.745039, 0.820804], - [0.989411, 0.747997, 0.829372], - [0.989089, 0.750968, 0.837979], - [0.988754, 0.753949, 0.846627], - [0.988406, 0.756949, 0.855332], - [0.988046, 0.759964, 0.864078], - [0.987672, 0.762996, 0.872864], - [0.987280, 0.766047, 0.881699], - [0.986868, 0.769105, 0.890573], - [0.986435, 0.772184, 0.899493], - [0.985980, 0.775272, 0.908448], - [0.985503, 0.778378, 0.917444], - [0.985002, 0.781495, 0.926468], - [0.984473, 0.784624, 0.935531], - [0.983913, 0.787757, 0.944626], - [0.983322, 0.790905, 0.953748], - [0.982703, 0.794068, 0.962895], - [0.982048, 0.797228, 0.972070], - [0.981354, 0.800406, 0.981267]]) + batlow = np.array( + [ + [0.005193, 0.098238, 0.349842], + [0.009065, 0.104487, 0.350933], + [0.012963, 0.110779, 0.351992], + [0.016530, 0.116913, 0.353070], + [0.019936, 0.122985, 0.354120], + [0.023189, 0.129035, 0.355182], + [0.026291, 0.135044, 0.356210], + [0.029245, 0.140964, 0.357239], + [0.032053, 0.146774, 0.358239], + [0.034853, 0.152558, 0.359233], + [0.037449, 0.158313, 0.360216], + [0.039845, 0.163978, 0.361187], + [0.042104, 0.169557, 0.362151], + [0.044069, 0.175053, 0.363084], + [0.045905, 0.180460, 0.364007], + [0.047665, 0.185844, 0.364915], + [0.049378, 0.191076, 0.365810], + [0.050795, 0.196274, 0.366684], + [0.052164, 0.201323, 0.367524], + [0.053471, 0.206357, 0.368370], + [0.054721, 0.211234, 0.369184], + [0.055928, 0.216046, 0.369974], + [0.057033, 0.220754, 0.370750], + [0.058032, 0.225340, 0.371509], + [0.059164, 0.229842, 0.372252], + [0.060167, 0.234299, 0.372978], + [0.061052, 0.238625, 0.373691], + [0.062060, 0.242888, 0.374386], + [0.063071, 0.247085, 0.375050], + [0.063982, 0.251213, 0.375709], + [0.064936, 0.255264, 0.376362], + [0.065903, 0.259257, 0.376987], + [0.066899, 0.263188, 0.377594], + [0.067921, 0.267056, 0.378191], + [0.069002, 0.270922, 0.378774], + [0.070001, 0.274713, 0.379342], + [0.071115, 0.278497, 0.379895], + [0.072192, 0.282249, 0.380434], + [0.073440, 0.285942, 0.380957], + [0.074595, 0.289653, 0.381452], + [0.075833, 0.293321, 0.381922], + [0.077136, 0.296996, 0.382376], + [0.078517, 0.300622, 0.382814], + [0.079984, 0.304252, 0.383224], + [0.081553, 0.307858, 0.383598], + [0.083082, 0.311461, 0.383936], + [0.084778, 0.315043, 0.384240], + [0.086503, 0.318615, 0.384506], + [0.088353, 0.322167, 0.384731], + [0.090281, 0.325685, 0.384910], + [0.092304, 0.329220, 0.385040], + [0.094462, 0.332712, 0.385116], + [0.096618, 0.336161, 0.385134], + [0.099015, 0.339621, 0.385090], + [0.101481, 0.343036, 0.384981], + [0.104078, 0.346410, 0.384801], + [0.106842, 0.349774, 0.384548], + [0.109695, 0.353098, 0.384217], + [0.112655, 0.356391, 0.383807], + [0.115748, 0.359638, 0.383310], + [0.118992, 0.362849, 0.382713], + [0.122320, 0.366030, 0.382026], + [0.125889, 0.369160, 0.381259], + [0.129519, 0.372238, 0.380378], + [0.133298, 0.375282, 0.379395], + [0.137212, 0.378282, 0.378315], + [0.141260, 0.381240, 0.377135], + [0.145432, 0.384130, 0.375840], + [0.149706, 0.386975, 0.374449], + [0.154073, 0.389777, 0.372934], + [0.158620, 0.392531, 0.371320], + [0.163246, 0.395237, 0.369609], + [0.167952, 0.397889, 0.367784], + [0.172788, 0.400496, 0.365867], + [0.177752, 0.403041, 0.363833], + [0.182732, 0.405551, 0.361714], + [0.187886, 0.408003, 0.359484], + [0.193050, 0.410427, 0.357177], + [0.198310, 0.412798, 0.354767], + [0.203676, 0.415116, 0.352253], + [0.209075, 0.417412, 0.349677], + [0.214555, 0.419661, 0.347019], + [0.220112, 0.421864, 0.344261], + [0.225707, 0.424049, 0.341459], + [0.231362, 0.426197, 0.338572], + [0.237075, 0.428325, 0.335634], + [0.242795, 0.430418, 0.332635], + [0.248617, 0.432493, 0.329571], + [0.254452, 0.434529, 0.326434], + [0.260320, 0.436556, 0.323285], + [0.266241, 0.438555, 0.320085], + [0.272168, 0.440541, 0.316831], + [0.278171, 0.442524, 0.313552], + [0.284175, 0.444484, 0.310243], + [0.290214, 0.446420, 0.306889], + [0.296294, 0.448357, 0.303509], + [0.302379, 0.450282, 0.300122], + [0.308517, 0.452205, 0.296721], + [0.314648, 0.454107, 0.293279], + [0.320834, 0.456006, 0.289841], + [0.327007, 0.457900, 0.286377], + [0.333235, 0.459794, 0.282937], + [0.339469, 0.461685, 0.279468], + [0.345703, 0.463563, 0.275998], + [0.351976, 0.465440, 0.272492], + [0.358277, 0.467331, 0.269037], + [0.364589, 0.469213, 0.265543], + [0.370922, 0.471085, 0.262064], + [0.377291, 0.472952, 0.258588], + [0.383675, 0.474842, 0.255131], + [0.390070, 0.476711, 0.251665], + [0.396505, 0.478587, 0.248212], + [0.402968, 0.480466, 0.244731], + [0.409455, 0.482351, 0.241314], + [0.415967, 0.484225, 0.237895], + [0.422507, 0.486113, 0.234493], + [0.429094, 0.488011, 0.231096], + [0.435714, 0.489890, 0.227728], + [0.442365, 0.491795, 0.224354], + [0.449052, 0.493684, 0.221074], + [0.455774, 0.495585, 0.217774], + [0.462539, 0.497497, 0.214518], + [0.469368, 0.499393, 0.211318], + [0.476221, 0.501314, 0.208148], + [0.483123, 0.503216, 0.205037], + [0.490081, 0.505137, 0.201976], + [0.497089, 0.507058, 0.198994], + [0.504153, 0.508984, 0.196118], + [0.511253, 0.510898, 0.193296], + [0.518425, 0.512822, 0.190566], + [0.525637, 0.514746, 0.187990], + [0.532907, 0.516662, 0.185497], + [0.540225, 0.518584, 0.183099], + [0.547599, 0.520486, 0.180884], + [0.555024, 0.522391, 0.178854], + [0.562506, 0.524293, 0.176964], + [0.570016, 0.526186, 0.175273], + [0.577582, 0.528058, 0.173775], + [0.585199, 0.529927, 0.172493], + [0.592846, 0.531777, 0.171449], + [0.600520, 0.533605, 0.170648], + [0.608240, 0.535423, 0.170104], + [0.615972, 0.537231, 0.169826], + [0.623739, 0.539002, 0.169814], + [0.631513, 0.540752, 0.170075], + [0.639301, 0.542484, 0.170622], + [0.647098, 0.544183, 0.171465], + [0.654889, 0.545863, 0.172603], + [0.662691, 0.547503, 0.174044], + [0.670477, 0.549127, 0.175747], + [0.678244, 0.550712, 0.177803], + [0.685995, 0.552274, 0.180056], + [0.693720, 0.553797, 0.182610], + [0.701421, 0.555294, 0.185478], + [0.709098, 0.556772, 0.188546], + [0.716731, 0.558205, 0.191851], + [0.724322, 0.559628, 0.195408], + [0.731878, 0.561011, 0.199174], + [0.739393, 0.562386, 0.203179], + [0.746850, 0.563725, 0.207375], + [0.754268, 0.565033, 0.211761], + [0.761629, 0.566344, 0.216322], + [0.768942, 0.567630, 0.221045], + [0.776208, 0.568899, 0.225930], + [0.783416, 0.570162, 0.230962], + [0.790568, 0.571421, 0.236160], + [0.797665, 0.572682, 0.241490], + [0.804709, 0.573928, 0.246955], + [0.811692, 0.575187, 0.252572], + [0.818610, 0.576462, 0.258303], + [0.825472, 0.577725, 0.264197], + [0.832272, 0.579026, 0.270211], + [0.838999, 0.580339, 0.276353], + [0.845657, 0.581672, 0.282631], + [0.852247, 0.583037, 0.289036], + [0.858747, 0.584440, 0.295572], + [0.865168, 0.585882, 0.302255], + [0.871505, 0.587352, 0.309112], + [0.877741, 0.588873, 0.316081], + [0.883878, 0.590450, 0.323195], + [0.889900, 0.592087, 0.330454], + [0.895809, 0.593765, 0.337865], + [0.901590, 0.595507, 0.345429], + [0.907242, 0.597319, 0.353142], + [0.912746, 0.599191, 0.360986], + [0.918103, 0.601126, 0.368999], + [0.923300, 0.603137, 0.377139], + [0.928323, 0.605212, 0.385404], + [0.933176, 0.607369, 0.393817], + [0.937850, 0.609582, 0.402345], + [0.942332, 0.611867, 0.411006], + [0.946612, 0.614218, 0.419767], + [0.950697, 0.616649, 0.428624], + [0.954574, 0.619137, 0.437582], + [0.958244, 0.621671, 0.446604], + [0.961696, 0.624282, 0.455702], + [0.964943, 0.626934, 0.464860], + [0.967983, 0.629639, 0.474057], + [0.970804, 0.632394, 0.483290], + [0.973424, 0.635183, 0.492547], + [0.975835, 0.638012, 0.501826], + [0.978052, 0.640868, 0.511090], + [0.980079, 0.643752, 0.520350], + [0.981918, 0.646664, 0.529602], + [0.983574, 0.649590, 0.538819], + [0.985066, 0.652522, 0.547998], + [0.986392, 0.655470, 0.557142], + [0.987567, 0.658422, 0.566226], + [0.988596, 0.661378, 0.575265], + [0.989496, 0.664329, 0.584246], + [0.990268, 0.667280, 0.593174], + [0.990926, 0.670230, 0.602031], + [0.991479, 0.673165, 0.610835], + [0.991935, 0.676091, 0.619575], + [0.992305, 0.679007, 0.628251], + [0.992595, 0.681914, 0.636869], + [0.992813, 0.684815, 0.645423], + [0.992967, 0.687705, 0.653934], + [0.993064, 0.690579, 0.662398], + [0.993111, 0.693451, 0.670810], + [0.993112, 0.696314, 0.679177], + [0.993074, 0.699161, 0.687519], + [0.993002, 0.702006, 0.695831], + [0.992900, 0.704852, 0.704114], + [0.992771, 0.707689, 0.712380], + [0.992619, 0.710530, 0.720639], + [0.992447, 0.713366, 0.728892], + [0.992258, 0.716210, 0.737146], + [0.992054, 0.719049, 0.745403], + [0.991837, 0.721893, 0.753673], + [0.991607, 0.724754, 0.761959], + [0.991367, 0.727614, 0.770270], + [0.991116, 0.730489, 0.778606], + [0.990855, 0.733373, 0.786976], + [0.990586, 0.736265, 0.795371], + [0.990307, 0.739184, 0.803810], + [0.990018, 0.742102, 0.812285], + [0.989720, 0.745039, 0.820804], + [0.989411, 0.747997, 0.829372], + [0.989089, 0.750968, 0.837979], + [0.988754, 0.753949, 0.846627], + [0.988406, 0.756949, 0.855332], + [0.988046, 0.759964, 0.864078], + [0.987672, 0.762996, 0.872864], + [0.987280, 0.766047, 0.881699], + [0.986868, 0.769105, 0.890573], + [0.986435, 0.772184, 0.899493], + [0.985980, 0.775272, 0.908448], + [0.985503, 0.778378, 0.917444], + [0.985002, 0.781495, 0.926468], + [0.984473, 0.784624, 0.935531], + [0.983913, 0.787757, 0.944626], + [0.983322, 0.790905, 0.953748], + [0.982703, 0.794068, 0.962895], + [0.982048, 0.797228, 0.972070], + [0.981354, 0.800406, 0.981267], + ] + ) cmap_batlow = matplotlib.colors.ListedColormap(batlow) @@ -4379,11 +4711,10 @@ def get_batlow_cmap() -> matplotlib.colors.ListedColormap: gradient = np.vstack((gradient, gradient)) fig, ax = plt.subplots(nrows=1, figsize=(6, 1)) - fig.subplots_adjust(top=0.5, bottom=0.15, - left=0.2, right=1) - ax.set_title('Batlow Colorbar', fontsize=14) + fig.subplots_adjust(top=0.5, bottom=0.15, left=0.2, right=1) + ax.set_title("Batlow Colorbar", fontsize=14) - ax.imshow(gradient, aspect='auto', cmap=cmap_batlow) + ax.imshow(gradient, aspect="auto", cmap=cmap_batlow) # Turn off *all* ticks & spines, not just the ones with colormaps. ax.set_axis_off() @@ -4404,262 +4735,266 @@ def get_petrel_cmap() -> matplotlib.colors.ListedColormap: """ - seismic = np.array([[255, 255, 0], - [255, 253, 0], - [254, 252, 0], - [254, 250, 0], - [253, 249, 0], - [253, 247, 0], - [253, 246, 0], - [252, 244, 0], - [252, 242, 0], - [251, 241, 0], - [251, 239, 0], - [251, 237, 0], - [250, 236, 0], - [250, 234, 0], - [249, 232, 0], - [249, 230, 0], - [248, 229, 0], - [248, 227, 0], - [247, 225, 0], - [247, 223, 0], - [246, 221, 0], - [246, 219, 0], - [246, 217, 0], - [245, 215, 0], - [245, 213, 0], - [244, 211, 0], - [243, 209, 0], - [243, 207, 0], - [242, 205, 0], - [242, 203, 0], - [241, 200, 0], - [241, 198, 0], - [240, 196, 0], - [240, 194, 0], - [239, 191, 0], - [238, 189, 0], - [238, 186, 0], - [237, 184, 0], - [237, 181, 0], - [236, 179, 0], - [235, 176, 0], - [235, 174, 0], - [234, 171, 0], - [233, 169, 0], - [233, 166, 0], - [232, 163, 0], - [231, 160, 0], - [231, 157, 0], - [230, 155, 0], - [229, 152, 0], - [228, 149, 0], - [228, 146, 0], - [227, 143, 0], - [226, 139, 0], - [225, 136, 0], - [225, 133, 0], - [224, 130, 0], - [223, 126, 0], - [222, 123, 0], - [221, 119, 0], - [220, 116, 0], - [219, 112, 0], - [218, 109, 0], - [217, 105, 0], - [217, 101, 0], - [216, 97, 0], - [215, 93, 0], - [214, 89, 0], - [213, 85, 0], - [211, 81, 0], - [210, 76, 0], - [209, 72, 0], - [208, 68, 0], - [207, 63, 0], - [206, 59, 0], - [205, 54, 0], - [203, 49, 0], - [202, 44, 0], - [201, 39, 0], - [200, 34, 0], - [198, 29, 0], - [197, 23, 0], - [196, 17, 0], - [194, 12, 0], - [193, 6, 0], - [191, 0, 0], - [186, 4, 0], - [180, 8, 0], - [175, 12, 0], - [169, 16, 0], - [164, 20, 0], - [158, 24, 0], - [152, 28, 0], - [147, 32, 0], - [141, 36, 0], - [136, 40, 0], - [130, 44, 0], - [125, 48, 0], - [119, 53, 0], - [114, 56, 0], - [108, 61, 0], - [103, 65, 0], - [97, 69, 0], - [101, 74, 8], - [105, 79, 16], - [110, 85, 24], - [114, 90, 32], - [118, 95, 40], - [122, 101, 48], - [126, 106, 56], - [130, 111, 64], - [135, 117, 72], - [139, 122, 80], - [143, 127, 88], - [147, 133, 96], - [151, 138, 104], - [156, 143, 112], - [160, 148, 120], - [164, 154, 128], - [168, 159, 136], - [173, 164, 144], - [177, 170, 152], - [181, 175, 160], - [185, 180, 168], - [190, 186, 176], - [194, 191, 184], - [198, 196, 192], - [202, 202, 200], - [201, 201, 201], - [196, 196, 196], - [191, 191, 191], - [186, 186, 186], - [181, 181, 181], - [176, 176, 176], - [171, 171, 171], - [166, 166, 166], - [161, 161, 161], - [156, 156, 156], - [151, 151, 151], - [146, 146, 146], - [141, 141, 141], - [136, 136, 136], - [131, 131, 131], - [126, 126, 126], - [121, 121, 121], - [116, 116, 116], - [111, 111, 111], - [106, 106, 106], - [101, 101, 101], - [96, 96, 96], - [91, 91, 91], - [86, 86, 86], - [81, 81, 81], - [77, 77, 77], - [72, 72, 83], - [67, 67, 90], - [63, 63, 97], - [58, 58, 104], - [54, 54, 110], - [49, 49, 117], - [45, 45, 124], - [40, 40, 131], - [36, 36, 138], - [32, 32, 144], - [27, 27, 151], - [22, 22, 158], - [18, 18, 164], - [13, 13, 171], - [9, 9, 178], - [5, 5, 184], - [0, 0, 191], - [4, 6, 193], - [8, 12, 194], - [11, 17, 196], - [14, 23, 197], - [18, 29, 198], - [21, 34, 200], - [24, 39, 201], - [28, 44, 202], - [31, 49, 203], - [34, 54, 205], - [37, 59, 206], - [40, 63, 207], - [43, 68, 208], - [46, 72, 209], - [48, 76, 210], - [51, 81, 211], - [54, 85, 213], - [56, 89, 214], - [59, 93, 215], - [61, 97, 216], - [64, 101, 217], - [66, 105, 217], - [68, 109, 218], - [71, 112, 219], - [73, 116, 220], - [75, 120, 221], - [78, 123, 222], - [80, 126, 223], - [82, 130, 224], - [84, 133, 225], - [86, 136, 225], - [88, 140, 226], - [90, 143, 227], - [92, 146, 228], - [94, 149, 228], - [96, 152, 229], - [98, 155, 230], - [99, 158, 231], - [101, 160, 231], - [103, 163, 232], - [105, 166, 233], - [106, 169, 233], - [108, 171, 234], - [110, 174, 235], - [111, 177, 235], - [113, 179, 236], - [114, 182, 237], - [116, 184, 237], - [118, 187, 238], - [119, 189, 238], - [121, 191, 239], - [122, 194, 240], - [123, 196, 240], - [125, 198, 241], - [126, 200, 241], - [128, 203, 242], - [129, 205, 242], - [130, 207, 243], - [132, 209, 244], - [133, 211, 244], - [134, 213, 245], - [136, 215, 245], - [137, 217, 246], - [138, 219, 246], - [139, 221, 247], - [140, 223, 247], - [142, 225, 247], - [143, 227, 248], - [144, 229, 248], - [145, 230, 249], - [146, 232, 249], - [147, 234, 250], - [148, 236, 250], - [150, 237, 251], - [151, 239, 251], - [152, 241, 251], - [153, 242, 252], - [154, 244, 252], - [155, 246, 253], - [156, 247, 253], - [157, 249, 253], - [158, 250, 254], - [159, 252, 254], - [160, 254, 255], - [161, 255, 255]]) + seismic = np.array( + [ + [255, 255, 0], + [255, 253, 0], + [254, 252, 0], + [254, 250, 0], + [253, 249, 0], + [253, 247, 0], + [253, 246, 0], + [252, 244, 0], + [252, 242, 0], + [251, 241, 0], + [251, 239, 0], + [251, 237, 0], + [250, 236, 0], + [250, 234, 0], + [249, 232, 0], + [249, 230, 0], + [248, 229, 0], + [248, 227, 0], + [247, 225, 0], + [247, 223, 0], + [246, 221, 0], + [246, 219, 0], + [246, 217, 0], + [245, 215, 0], + [245, 213, 0], + [244, 211, 0], + [243, 209, 0], + [243, 207, 0], + [242, 205, 0], + [242, 203, 0], + [241, 200, 0], + [241, 198, 0], + [240, 196, 0], + [240, 194, 0], + [239, 191, 0], + [238, 189, 0], + [238, 186, 0], + [237, 184, 0], + [237, 181, 0], + [236, 179, 0], + [235, 176, 0], + [235, 174, 0], + [234, 171, 0], + [233, 169, 0], + [233, 166, 0], + [232, 163, 0], + [231, 160, 0], + [231, 157, 0], + [230, 155, 0], + [229, 152, 0], + [228, 149, 0], + [228, 146, 0], + [227, 143, 0], + [226, 139, 0], + [225, 136, 0], + [225, 133, 0], + [224, 130, 0], + [223, 126, 0], + [222, 123, 0], + [221, 119, 0], + [220, 116, 0], + [219, 112, 0], + [218, 109, 0], + [217, 105, 0], + [217, 101, 0], + [216, 97, 0], + [215, 93, 0], + [214, 89, 0], + [213, 85, 0], + [211, 81, 0], + [210, 76, 0], + [209, 72, 0], + [208, 68, 0], + [207, 63, 0], + [206, 59, 0], + [205, 54, 0], + [203, 49, 0], + [202, 44, 0], + [201, 39, 0], + [200, 34, 0], + [198, 29, 0], + [197, 23, 0], + [196, 17, 0], + [194, 12, 0], + [193, 6, 0], + [191, 0, 0], + [186, 4, 0], + [180, 8, 0], + [175, 12, 0], + [169, 16, 0], + [164, 20, 0], + [158, 24, 0], + [152, 28, 0], + [147, 32, 0], + [141, 36, 0], + [136, 40, 0], + [130, 44, 0], + [125, 48, 0], + [119, 53, 0], + [114, 56, 0], + [108, 61, 0], + [103, 65, 0], + [97, 69, 0], + [101, 74, 8], + [105, 79, 16], + [110, 85, 24], + [114, 90, 32], + [118, 95, 40], + [122, 101, 48], + [126, 106, 56], + [130, 111, 64], + [135, 117, 72], + [139, 122, 80], + [143, 127, 88], + [147, 133, 96], + [151, 138, 104], + [156, 143, 112], + [160, 148, 120], + [164, 154, 128], + [168, 159, 136], + [173, 164, 144], + [177, 170, 152], + [181, 175, 160], + [185, 180, 168], + [190, 186, 176], + [194, 191, 184], + [198, 196, 192], + [202, 202, 200], + [201, 201, 201], + [196, 196, 196], + [191, 191, 191], + [186, 186, 186], + [181, 181, 181], + [176, 176, 176], + [171, 171, 171], + [166, 166, 166], + [161, 161, 161], + [156, 156, 156], + [151, 151, 151], + [146, 146, 146], + [141, 141, 141], + [136, 136, 136], + [131, 131, 131], + [126, 126, 126], + [121, 121, 121], + [116, 116, 116], + [111, 111, 111], + [106, 106, 106], + [101, 101, 101], + [96, 96, 96], + [91, 91, 91], + [86, 86, 86], + [81, 81, 81], + [77, 77, 77], + [72, 72, 83], + [67, 67, 90], + [63, 63, 97], + [58, 58, 104], + [54, 54, 110], + [49, 49, 117], + [45, 45, 124], + [40, 40, 131], + [36, 36, 138], + [32, 32, 144], + [27, 27, 151], + [22, 22, 158], + [18, 18, 164], + [13, 13, 171], + [9, 9, 178], + [5, 5, 184], + [0, 0, 191], + [4, 6, 193], + [8, 12, 194], + [11, 17, 196], + [14, 23, 197], + [18, 29, 198], + [21, 34, 200], + [24, 39, 201], + [28, 44, 202], + [31, 49, 203], + [34, 54, 205], + [37, 59, 206], + [40, 63, 207], + [43, 68, 208], + [46, 72, 209], + [48, 76, 210], + [51, 81, 211], + [54, 85, 213], + [56, 89, 214], + [59, 93, 215], + [61, 97, 216], + [64, 101, 217], + [66, 105, 217], + [68, 109, 218], + [71, 112, 219], + [73, 116, 220], + [75, 120, 221], + [78, 123, 222], + [80, 126, 223], + [82, 130, 224], + [84, 133, 225], + [86, 136, 225], + [88, 140, 226], + [90, 143, 227], + [92, 146, 228], + [94, 149, 228], + [96, 152, 229], + [98, 155, 230], + [99, 158, 231], + [101, 160, 231], + [103, 163, 232], + [105, 166, 233], + [106, 169, 233], + [108, 171, 234], + [110, 174, 235], + [111, 177, 235], + [113, 179, 236], + [114, 182, 237], + [116, 184, 237], + [118, 187, 238], + [119, 189, 238], + [121, 191, 239], + [122, 194, 240], + [123, 196, 240], + [125, 198, 241], + [126, 200, 241], + [128, 203, 242], + [129, 205, 242], + [130, 207, 243], + [132, 209, 244], + [133, 211, 244], + [134, 213, 245], + [136, 215, 245], + [137, 217, 246], + [138, 219, 246], + [139, 221, 247], + [140, 223, 247], + [142, 225, 247], + [143, 227, 248], + [144, 229, 248], + [145, 230, 249], + [146, 232, 249], + [147, 234, 250], + [148, 236, 250], + [150, 237, 251], + [151, 239, 251], + [152, 241, 251], + [153, 242, 252], + [154, 244, 252], + [155, 246, 253], + [156, 247, 253], + [157, 249, 253], + [158, 250, 254], + [159, 252, 254], + [160, 254, 255], + [161, 255, 255], + ] + ) cmap_seismic = matplotlib.colors.ListedColormap(seismic) @@ -4668,11 +5003,10 @@ def get_petrel_cmap() -> matplotlib.colors.ListedColormap: gradient = np.vstack((gradient, gradient)) fig, ax = plt.subplots(nrows=1, figsize=(6, 1)) - fig.subplots_adjust(top=0.5, bottom=0.15, - left=0.2, right=1) - ax.set_title('Seismic Colorbar', fontsize=14) + fig.subplots_adjust(top=0.5, bottom=0.15, left=0.2, right=1) + ax.set_title("Seismic Colorbar", fontsize=14) - ax.imshow(gradient, aspect='auto', cmap=cmap_seismic) + ax.imshow(gradient, aspect="auto", cmap=cmap_seismic) # Turn off *all* ticks & spines, not just the ones with colormaps. ax.set_axis_off() @@ -4680,11 +5014,13 @@ def get_petrel_cmap() -> matplotlib.colors.ListedColormap: return cmap_seismic -def get_color_lot(geo_model, - lith_c: pd.DataFrame = None, - index='surface', - is_faults: bool = True, - is_basement: bool = False) -> pd.Series: +def get_color_lot( + geo_model, + lith_c: pd.DataFrame = None, + index="surface", + is_faults: bool = True, + is_basement: bool = False, +) -> pd.Series: """Method to get the right color list depending on the type of plot. Borrowed from https://github.com/cgre-aachen/gempy/blob/6aed72a4dfa26830df142a0461294bd9d21a4fa4/gempy/plot/vista.py#L133-L167 @@ -4713,30 +5049,33 @@ def get_color_lot(geo_model, """ if lith_c is None: surf_df = geo_model._surfaces.df.set_index(index) - unique_surf_points = np.unique(geo_model._surface_points.df['id']).astype(int) + unique_surf_points = np.unique(geo_model._surface_points.df["id"]).astype(int) if len(unique_surf_points) != 0: bool_surf_points = np.zeros(surf_df.shape[0], dtype=bool) - bool_surf_points[unique_surf_points.astype('int') - 1] = True + bool_surf_points[unique_surf_points.astype("int") - 1] = True - surf_df['isActive'] = (surf_df['isActive'] | bool_surf_points) + surf_df["isActive"] = surf_df["isActive"] | bool_surf_points if is_faults is True and is_basement is True: - lith_c = surf_df.groupby('isActive').get_group(True)['color'] + lith_c = surf_df.groupby("isActive").get_group(True)["color"] elif is_faults is True and is_basement is False: - lith_c = surf_df.groupby(['isActive', 'isBasement']).get_group((True, False))['color'] + lith_c = surf_df.groupby(["isActive", "isBasement"]).get_group( + (True, False) + )["color"] else: - lith_c = surf_df.groupby(['isActive', 'isFault']).get_group((True, False))[ - 'color'] + lith_c = surf_df.groupby(["isActive", "isFault"]).get_group( + (True, False) + )["color"] color_lot = lith_c return color_lot -def get_mesh_geological_map(geo_model) -> Tuple[pv.core.pointset.PolyData, - matplotlib.colors.ListedColormap, - bool]: +def get_mesh_geological_map( + geo_model, +) -> Tuple[pv.core.pointset.PolyData, matplotlib.colors.ListedColormap, bool]: """Getting the geological map of a GemPy Model draped over the topography as mesh. Borrowed from https://github.com/cgre-aachen/gempy/blob/6aed72a4dfa26830df142a0461294bd9d21a4fa4/gempy/plot/vista.py#L512-L604 @@ -4769,30 +5108,28 @@ def get_mesh_geological_map(geo_model) -> Tuple[pv.core.pointset.PolyData, polydata = pv.PolyData(topography) # Getting color values - colors_hex = get_color_lot(geo_model=geo_model, - is_faults=False, - is_basement=True, index='id') + colors_hex = get_color_lot( + geo_model=geo_model, is_faults=False, is_basement=True, index="id" + ) colors_rgb_ = colors_hex.apply(lambda val: list(mcolors.hex2color(val))) - colors_rgb = pd.DataFrame(colors_rgb_.to_list(), - index=colors_hex.index) * 255 + colors_rgb = pd.DataFrame(colors_rgb_.to_list(), index=colors_hex.index) * 255 sel = np.round(geo_model.solutions.geological_map[0]).astype(int)[0] # Converting color values - scalars_val = pv.convert_array(colors_rgb.loc[sel].values, - array_type=3) + scalars_val = pv.convert_array(colors_rgb.loc[sel].values, array_type=3) # Creating colormap - cm = mcolors.ListedColormap(list(get_color_lot(geo_model=geo_model, - is_faults=True, - is_basement=True))) + cm = mcolors.ListedColormap( + list(get_color_lot(geo_model=geo_model, is_faults=True, is_basement=True)) + ) rgb = True # Interpolating the polydata and assigning values polydata.delaunay_2d(inplace=True) - polydata['id'] = scalars_val - polydata['height'] = topography[:, 2] + polydata["id"] = scalars_val + polydata["height"] = topography[:, 2] return polydata, cm, rgb @@ -4819,11 +5156,13 @@ def resample_between_well_deviation_points(coordinates: np.ndarray) -> np.ndarra # Checking that the coordinates are provided as np.ndarray if not isinstance(coordinates, np.ndarray): - raise TypeError('Coordinates must be provided as NumPy Array') + raise TypeError("Coordinates must be provided as NumPy Array") # Checking that three coordinates are provided for each point if coordinates.shape[1] != 3: - raise ValueError('Three coordinates X, Y, and Z must be provided for each point') + raise ValueError( + "Three coordinates X, Y, and Z must be provided for each point" + ) # Creating list for storing points list_points = [] @@ -4844,8 +5183,7 @@ def resample_between_well_deviation_points(coordinates: np.ndarray) -> np.ndarra return points_resampled -def get_points_along_spline(spline: pv.core.pointset.PolyData, - dist: np.ndarray): +def get_points_along_spline(spline: pv.core.pointset.PolyData, dist: np.ndarray): """Returning the closest point on the spline a given a length along a spline. Parameters @@ -4869,18 +5207,20 @@ def get_points_along_spline(spline: pv.core.pointset.PolyData, # Checking that the spline is a PyVista PolyData Pointset if not isinstance(spline, pv.core.pointset.PolyData): - raise TypeError('The well path/the spline must be provided as PyVista PolyData Pointset') + raise TypeError( + "The well path/the spline must be provided as PyVista PolyData Pointset" + ) # Checking that the distances are provided as np.ndarray if not isinstance(dist, np.ndarray): - raise TypeError('The distances must be provided as np.ndarray') + raise TypeError("The distances must be provided as np.ndarray") # Creating list for storing indices idx_list = [] # Getting index of spline that match with a measured value and append index to list of indices for distance in dist: - idx = np.argmin(np.abs(spline.point_data['arc_length'] - distance)) + idx = np.argmin(np.abs(spline.point_data["arc_length"] - distance)) idx_list.append(idx) points = spline.points[idx_list] @@ -4888,10 +5228,12 @@ def get_points_along_spline(spline: pv.core.pointset.PolyData, return points -def show_well_log_along_well(coordinates: np.ndarray, - dist: np.ndarray, - values: np.ndarray, - radius_factor: Union[int, float] = 2) -> pv.core.pointset.PolyData: +def show_well_log_along_well( + coordinates: np.ndarray, + dist: np.ndarray, + values: np.ndarray, + radius_factor: Union[int, float] = 2, +) -> pv.core.pointset.PolyData: """Function to return a tube representing well log values along a well path Parameters @@ -4922,23 +5264,25 @@ def show_well_log_along_well(coordinates: np.ndarray, # Checking that the coordinates are provided as np.ndarray if not isinstance(coordinates, np.ndarray): - raise TypeError('Coordinates must be provided as NumPy Array') + raise TypeError("Coordinates must be provided as NumPy Array") # Checking that three coordinates are provided for each point if coordinates.shape[1] != 3: - raise ValueError('Three coordinates X, Y, and Z must be provided for each point') + raise ValueError( + "Three coordinates X, Y, and Z must be provided for each point" + ) # Checking that the distances are provided as np.ndarray if not isinstance(dist, np.ndarray): - raise TypeError('The distances must be provided as np.ndarray') + raise TypeError("The distances must be provided as np.ndarray") # Checking that the values are provided as np.ndarray if not isinstance(values, np.ndarray): - raise TypeError('The well log values must be provided as np.ndarray') + raise TypeError("The well log values must be provided as np.ndarray") # Checking that the radius factor is provided as int or float if not isinstance(radius_factor, (int, float)): - raise TypeError('The radius factor must be provided as int or float') + raise TypeError("The radius factor must be provided as int or float") # Resampling points along the well path points_resampled = resample_between_well_deviation_points(coordinates=coordinates) @@ -4953,11 +5297,12 @@ def show_well_log_along_well(coordinates: np.ndarray, polyline_along_spline = polyline_from_points(points_along_spline) # Assigning values as data array to Polyline - polyline_along_spline['values'] = values + polyline_along_spline["values"] = values # Create Tube with radius as function of the scalar values - tube_along_spline = polyline_along_spline.tube(scalars='values', - radius_factor=radius_factor) + tube_along_spline = polyline_along_spline.tube( + scalars="values", radius_factor=radius_factor + ) return tube_along_spline @@ -4983,7 +5328,7 @@ def polyline_from_points(points: np.ndarray) -> pv.core.pointset.PolyData: # Checking that the points are of type PolyData Pointset if not isinstance(points, np.ndarray): - raise TypeError('The points must be provided as NumPy Array') + raise TypeError("The points must be provided as NumPy Array") # Creating PolyData Object poly = pv.PolyData() From d0a2bf7c8baeeb841bdee3e17ec6db5ebf517662 Mon Sep 17 00:00:00 2001 From: AlexanderJuestel Date: Mon, 22 Jul 2024 20:45:45 +0200 Subject: [PATCH 16/63] Format vector.py --- gemgis/vector.py | 3230 ++++++++++++++++++++++++++-------------------- 1 file changed, 1843 insertions(+), 1387 deletions(-) diff --git a/gemgis/vector.py b/gemgis/vector.py index 303ff910..2e1fc6be 100644 --- a/gemgis/vector.py +++ b/gemgis/vector.py @@ -35,22 +35,24 @@ try: import rasterio except ModuleNotFoundError: - raise ModuleNotFoundError('No valid rasterio installation found') + raise ModuleNotFoundError("No valid rasterio installation found") -pd.set_option('display.float_format', lambda x: '%.2f' % x) +pd.set_option("display.float_format", lambda x: "%.2f" % x) # Extracting X and Y coordinates from Vector Data ################################################# -def extract_xy_points(gdf: gpd.geodataframe.GeoDataFrame, - reset_index: bool = True, - drop_id: bool = True, - drop_index: bool = True, - overwrite_xy: bool = False, - target_crs: Union[str, pyproj.crs.crs.CRS] = None, - bbox: Optional[Sequence[float]] = None) -> gpd.geodataframe.GeoDataFrame: +def extract_xy_points( + gdf: gpd.geodataframe.GeoDataFrame, + reset_index: bool = True, + drop_id: bool = True, + drop_index: bool = True, + overwrite_xy: bool = False, + target_crs: Union[str, pyproj.crs.crs.CRS] = None, + bbox: Optional[Sequence[float]] = None, +) -> gpd.geodataframe.GeoDataFrame: """Extracting X and Y coordinates from a GeoDataFrame (Points) and returning a GeoDataFrame with X and Y coordinates as additional columns @@ -123,65 +125,68 @@ def extract_xy_points(gdf: gpd.geodataframe.GeoDataFrame, extract_xy_linestrings : Extracting X and Y coordinates from a GeoDataFrame containing Shapely LineStrings extract_xy : Extracting X and Y coordinates from Vector Data - """ + """ # Checking that gdf is of type GepDataFrame if not isinstance(gdf, gpd.geodataframe.GeoDataFrame): - raise TypeError('Loaded object is not a GeoDataFrame') + raise TypeError("Loaded object is not a GeoDataFrame") # Check that all entries of the gdf are of type Point if not all(shapely.get_type_id(gdf.geometry) == 0): - raise TypeError('All GeoDataFrame entries must be of geom_type Point') + raise TypeError("All GeoDataFrame entries must be of geom_type Point") # Checking that the bbox is of type None or list if bbox is not None: if not isinstance(bbox, Sequence): - raise TypeError('The bbox values must be provided as a sequence') + raise TypeError("The bbox values must be provided as a sequence") # Checking that the bbox list only has four elements if len(bbox) != 4: - raise ValueError('Provide minx, maxx, miny and maxy values for the bbox') + raise ValueError("Provide minx, maxx, miny and maxy values for the bbox") # Checking that all elements of the list are of type int or float if not all(isinstance(bound, (int, float)) for bound in bbox): - raise TypeError('Bbox values must be of type float or int') + raise TypeError("Bbox values must be of type float or int") # Checking that all Shapely Objects are valid if not all(shapely.is_valid(gdf.geometry)): - raise ValueError('Not all Shapely Objects are valid objects') + raise ValueError("Not all Shapely Objects are valid objects") # Checking that no empty Shapely Objects are present if any(shapely.is_empty(gdf.geometry)): - raise ValueError('One or more Shapely objects are empty') + raise ValueError("One or more Shapely objects are empty") # Checking that none of the points have a Z component if any(shapely.has_z(gdf.geometry)): raise ValueError( - 'One or more Shapely objects contain a Z component. Use gg.vector.extract_xyz(...) to obtain all coordinates.') + "One or more Shapely objects contain a Z component. Use gg.vector.extract_xyz(...) to obtain all coordinates." + ) # Checking that drop_id is of type bool if not isinstance(drop_id, bool): - raise TypeError('Drop_id argument must be of type bool') + raise TypeError("Drop_id argument must be of type bool") # Checking that drop_index is of type bool if not isinstance(drop_index, bool): - raise TypeError('Drop_index argument must be of type bool') + raise TypeError("Drop_index argument must be of type bool") # Checking that reset_index is of type bool if not isinstance(reset_index, bool): - raise TypeError('Reset_index argument must be of type bool') + raise TypeError("Reset_index argument must be of type bool") # Checking that the target_crs is of type string if not isinstance(target_crs, (str, type(None), pyproj.crs.crs.CRS)): - raise TypeError('target_crs must be of type string or a pyproj object') + raise TypeError("target_crs must be of type string or a pyproj object") # Checking that overwrite_xy is of type bool if not isinstance(overwrite_xy, bool): - raise TypeError('Overwrite_xy argument must be of type bool') + raise TypeError("Overwrite_xy argument must be of type bool") # Checking that X and Y are not in the GeoDataFrame - if not overwrite_xy and {'X', 'Y'}.issubset(gdf.columns): - raise ValueError('X and Y columns must not be present in GeoDataFrame before the extraction of coordinates') + if not overwrite_xy and {"X", "Y"}.issubset(gdf.columns): + raise ValueError( + "X and Y columns must not be present in GeoDataFrame before the extraction of coordinates" + ) # Copying GeoDataFrame gdf = gdf.copy(deep=True) @@ -191,33 +196,38 @@ def extract_xy_points(gdf: gpd.geodataframe.GeoDataFrame, gdf = gdf.to_crs(crs=target_crs) # Extracting x,y coordinates from point vector data - gdf['X'] = shapely.get_x(gdf.geometry) - gdf['Y'] = shapely.get_y(gdf.geometry) + gdf["X"] = shapely.get_x(gdf.geometry) + gdf["Y"] = shapely.get_y(gdf.geometry) # Limiting the extent of the data if bbox is not None: - gdf = gdf[(gdf.X > bbox[0]) & (gdf.X < bbox[1]) & (gdf.Y > bbox[2]) & (gdf.Y < bbox[3])] + gdf = gdf[ + (gdf.X > bbox[0]) + & (gdf.X < bbox[1]) + & (gdf.Y > bbox[2]) + & (gdf.Y < bbox[3]) + ] # Resetting the index if reset_index: gdf = gdf.reset_index() # Dropping index column - if 'index' in gdf and drop_index: - gdf = gdf.drop(columns='index', - axis=1) + if "index" in gdf and drop_index: + gdf = gdf.drop(columns="index", axis=1) # Dropping id column - if 'id' in gdf and drop_id: - gdf = gdf.drop(columns='id', - axis=1) + if "id" in gdf and drop_id: + gdf = gdf.drop(columns="id", axis=1) return gdf -def extract_xy_linestring(gdf: gpd.geodataframe.GeoDataFrame, - target_crs: Union[str, pyproj.crs.crs.CRS] = None, - bbox: Optional[Sequence[float]] = None) -> gpd.geodataframe.GeoDataFrame: +def extract_xy_linestring( + gdf: gpd.geodataframe.GeoDataFrame, + target_crs: Union[str, pyproj.crs.crs.CRS] = None, + bbox: Optional[Sequence[float]] = None, +) -> gpd.geodataframe.GeoDataFrame: """Extracting the coordinates of Shapely LineStrings within a GeoDataFrame and storing the X and Y coordinates in lists per LineString @@ -273,68 +283,79 @@ def extract_xy_linestring(gdf: gpd.geodataframe.GeoDataFrame, # Checking that gdf is of type GepDataFrame if not isinstance(gdf, gpd.geodataframe.GeoDataFrame): - raise TypeError('Loaded object is not a GeoDataFrame') + raise TypeError("Loaded object is not a GeoDataFrame") # Check that all entries of the gdf are of type LineString if not all(shapely.get_type_id(gdf.geometry) == 1): - raise TypeError('All GeoDataFrame entries must be of geom_type linestrings') + raise TypeError("All GeoDataFrame entries must be of geom_type linestrings") # Checking that all Shapely Objects are valid if not all(shapely.is_valid(gdf.geometry)): - raise ValueError('Not all Shapely Objects are valid objects') + raise ValueError("Not all Shapely Objects are valid objects") # Checking that no empty Shapely Objects are present if any(shapely.is_empty(gdf.geometry)): - raise ValueError('One or more Shapely objects are empty') + raise ValueError("One or more Shapely objects are empty") # Checking that none of the points have a Z component if any(shapely.has_z(gdf.geometry)): - raise ValueError('One or more Shapely objects contain a Z component') + raise ValueError("One or more Shapely objects contain a Z component") # Checking that the bbox is of type None or list if bbox is not None: if not isinstance(bbox, Sequence): - raise TypeError('The bbox values must be provided as a sequence') + raise TypeError("The bbox values must be provided as a sequence") # Checking that the bbox list only has four elements if len(bbox) != 4: - raise ValueError('Provide minx, maxx, miny and maxy values for the bbox') + raise ValueError("Provide minx, maxx, miny and maxy values for the bbox") # Checking that all elements of the list are of type int or float if not all(isinstance(bound, (int, float)) for bound in bbox): - raise TypeError('Bbox values must be of type float or int') + raise TypeError("Bbox values must be of type float or int") # Checking that the target_crs is of type string if target_crs is not None and not isinstance(target_crs, (str, pyproj.crs.crs.CRS)): - raise TypeError('target_crs must be of type string or a pyproj object') + raise TypeError("target_crs must be of type string or a pyproj object") # Reprojecting coordinates to provided the target_crs if target_crs is not None: gdf = gdf.to_crs(crs=target_crs) # Extracting X coordinates - gdf['X'] = [list(shapely.get_coordinates(gdf.geometry[i])[:, 0]) for i in range(len(gdf))] + gdf["X"] = [ + list(shapely.get_coordinates(gdf.geometry[i])[:, 0]) for i in range(len(gdf)) + ] # Extracting Y coordinates - gdf['Y'] = [list(shapely.get_coordinates(gdf.geometry[i])[:, 1]) for i in range(len(gdf))] + gdf["Y"] = [ + list(shapely.get_coordinates(gdf.geometry[i])[:, 1]) for i in range(len(gdf)) + ] # Limiting the extent of the data if bbox is not None: - gdf = gdf[(gdf.X > bbox[0]) & (gdf.X < bbox[1]) & (gdf.Y > bbox[2]) & (gdf.Y < bbox[3])] + gdf = gdf[ + (gdf.X > bbox[0]) + & (gdf.X < bbox[1]) + & (gdf.Y > bbox[2]) + & (gdf.Y < bbox[3]) + ] return gdf -def extract_xy_linestrings(gdf: gpd.geodataframe.GeoDataFrame, - reset_index: bool = True, - drop_id: bool = True, - drop_index: bool = True, - drop_points: bool = True, - drop_level0: bool = True, - drop_level1: bool = True, - overwrite_xy: bool = False, - target_crs: Union[str, pyproj.crs.crs.CRS] = None, - bbox: Optional[Sequence[float]] = None) -> gpd.geodataframe.GeoDataFrame: +def extract_xy_linestrings( + gdf: gpd.geodataframe.GeoDataFrame, + reset_index: bool = True, + drop_id: bool = True, + drop_index: bool = True, + drop_points: bool = True, + drop_level0: bool = True, + drop_level1: bool = True, + overwrite_xy: bool = False, + target_crs: Union[str, pyproj.crs.crs.CRS] = None, + bbox: Optional[Sequence[float]] = None, +) -> gpd.geodataframe.GeoDataFrame: """Extracting X and Y coordinates from a GeoDataFrame (LineStrings) and returning a GeoDataFrame with X and Y coordinates as additional columns @@ -412,68 +433,70 @@ def extract_xy_linestrings(gdf: gpd.geodataframe.GeoDataFrame, # Checking that gdf is of type GepDataFrame if not isinstance(gdf, gpd.geodataframe.GeoDataFrame): - raise TypeError('Loaded object is not a GeoDataFrame') + raise TypeError("Loaded object is not a GeoDataFrame") # Check that all entries of the gdf are of type LineString if not all(shapely.get_type_id(gdf.geometry) == 1): - raise TypeError('All GeoDataFrame entries must be of geom_type linestrings') + raise TypeError("All GeoDataFrame entries must be of geom_type linestrings") # Checking that all Shapely Objects are valid if not all(shapely.is_valid(gdf.geometry)): - raise ValueError('Not all Shapely Objects are valid objects') + raise ValueError("Not all Shapely Objects are valid objects") # Checking that no empty Shapely Objects are present if any(shapely.is_empty(gdf.geometry)): - raise ValueError('One or more Shapely objects are empty') + raise ValueError("One or more Shapely objects are empty") # Checking that the bbox is of type None or list if bbox is not None: if not isinstance(bbox, Sequence): - raise TypeError('The bbox values must be provided as a sequence') + raise TypeError("The bbox values must be provided as a sequence") # Checking that the bbox list only has four elements if len(bbox) != 4: - raise ValueError('Provide minx, maxx, miny and maxy values for the bbox') + raise ValueError("Provide minx, maxx, miny and maxy values for the bbox") # Checking that all elements of the list are of type int or float if not all(isinstance(bound, (int, float)) for bound in bbox): - raise TypeError('Bbox values must be of type float or int') + raise TypeError("Bbox values must be of type float or int") # Checking that drop_index is of type bool if not isinstance(drop_index, bool): - raise TypeError('Drop_index argument must be of type bool') + raise TypeError("Drop_index argument must be of type bool") # Checking that drop_id is of type bool if not isinstance(drop_id, bool): - raise TypeError('Drop_id argument must be of type bool') + raise TypeError("Drop_id argument must be of type bool") # Checking that drop_level0 is of type bool if not isinstance(drop_level0, bool): - raise TypeError('Drop_index_level0 argument must be of type bool') + raise TypeError("Drop_index_level0 argument must be of type bool") # Checking that drop_level1 is of type bool if not isinstance(drop_level1, bool): - raise TypeError('Drop_index_level1 argument must be of type bool') + raise TypeError("Drop_index_level1 argument must be of type bool") # Checking that drop_points is of type bool if not isinstance(drop_points, bool): - raise TypeError('Drop_points argument must be of type bool') + raise TypeError("Drop_points argument must be of type bool") # Checking that reset_index is of type bool if not isinstance(reset_index, bool): - raise TypeError('Reset_index argument must be of type bool') + raise TypeError("Reset_index argument must be of type bool") # Checking that the target_crs is of type string if target_crs is not None and not isinstance(target_crs, (str, pyproj.crs.crs.CRS)): - raise TypeError('target_crs must be of type string or a pyproj object') + raise TypeError("target_crs must be of type string or a pyproj object") # Checking that overwrite_xy is of type bool if not isinstance(overwrite_xy, bool): - raise TypeError('Overwrite_xy argument must be of type bool') + raise TypeError("Overwrite_xy argument must be of type bool") # Checking if overwrite_xy is False and if X and Y coordinates are already present in the GeoDataFrame - if not overwrite_xy and {'X', 'Y'}.issubset(gdf.columns): - raise ValueError('X and Y columns must not be present in GeoDataFrame before the extraction of coordinates') + if not overwrite_xy and {"X", "Y"}.issubset(gdf.columns): + raise ValueError( + "X and Y columns must not be present in GeoDataFrame before the extraction of coordinates" + ) # Copying GeoDataFrame gdf = gdf.copy(deep=True) @@ -487,78 +510,80 @@ def extract_xy_linestrings(gdf: gpd.geodataframe.GeoDataFrame, # Extracting x,y coordinates from line vector data if all(shapely.has_z(gdf.geometry)): - gdf['points'] = [shapely.get_coordinates(geometry=gdf.geometry[i], - include_z=True) for i in range(len(gdf))] + gdf["points"] = [ + shapely.get_coordinates(geometry=gdf.geometry[i], include_z=True) + for i in range(len(gdf)) + ] else: - gdf['points'] = [shapely.get_coordinates(geometry=gdf.geometry[i], - include_z=False) for i in range(len(gdf))] + gdf["points"] = [ + shapely.get_coordinates(geometry=gdf.geometry[i], include_z=False) + for i in range(len(gdf)) + ] # Creating DataFrame from exploded columns - df = pd.DataFrame(data=gdf).explode('points') + df = pd.DataFrame(data=gdf).explode("points") # Try creating the DataFrame for planar LineStrings if not all(shapely.has_z(gdf.geometry)): - df[['X', 'Y']] = pd.DataFrame(data=df['points'].tolist(), - index=df.index) + df[["X", "Y"]] = pd.DataFrame(data=df["points"].tolist(), index=df.index) # If LineStrings also contain Z value, then also append a Z column else: - df[['X', 'Y', 'Z']] = pd.DataFrame(data=df['points'].tolist(), - index=df.index) + df[["X", "Y", "Z"]] = pd.DataFrame(data=df["points"].tolist(), index=df.index) # Resetting index if reset_index: df = df.reset_index() # Creating new GeoDataFrame - gdf = gpd.GeoDataFrame(data=df, - geometry=gpd.points_from_xy(df.X, df.Y), - crs=crs) + gdf = gpd.GeoDataFrame(data=df, geometry=gpd.points_from_xy(df.X, df.Y), crs=crs) # Dropping id column - if 'id' in gdf and drop_id: - gdf = gdf.drop(columns='id', - axis=1) + if "id" in gdf and drop_id: + gdf = gdf.drop(columns="id", axis=1) # Dropping index column - if 'index' in gdf and drop_index: - gdf = gdf.drop(columns='index', - axis=1) + if "index" in gdf and drop_index: + gdf = gdf.drop(columns="index", axis=1) # Dropping points column - if 'points' in gdf and drop_points: - gdf = gdf.drop(columns='points', - axis=1) + if "points" in gdf and drop_points: + gdf = gdf.drop(columns="points", axis=1) # Dropping level_0 column - if reset_index and drop_level0 and 'level_0' in gdf: - gdf = gdf.drop(columns='level_0', - axis=1) + if reset_index and drop_level0 and "level_0" in gdf: + gdf = gdf.drop(columns="level_0", axis=1) # Dropping level_1 column - if reset_index and drop_level1 and 'level_1' in gdf: - gdf = gdf.drop(columns='level_1', - axis=1) + if reset_index and drop_level1 and "level_1" in gdf: + gdf = gdf.drop(columns="level_1", axis=1) # Limiting the extent of the data if bbox is not None: - gdf = gdf[(gdf.X > bbox[0]) & (gdf.X < bbox[1]) & (gdf.Y > bbox[2]) & (gdf.Y < bbox[3])] + gdf = gdf[ + (gdf.X > bbox[0]) + & (gdf.X < bbox[1]) + & (gdf.Y > bbox[2]) + & (gdf.Y < bbox[3]) + ] return gdf -def extract_xy(gdf: gpd.geodataframe.GeoDataFrame, - reset_index: bool = True, - drop_index: bool = True, - drop_id: bool = True, - drop_points: bool = True, - drop_level0: bool = True, - drop_level1: bool = True, - overwrite_xy: bool = True, - target_crs: Union[str, pyproj.crs.crs.CRS] = None, - bbox: Optional[Sequence[float]] = None, - remove_total_bounds: bool = False, - threshold_bounds: Union[float, int] = 0.1) -> gpd.geodataframe.GeoDataFrame: +def extract_xy( + gdf: gpd.geodataframe.GeoDataFrame, + reset_index: bool = True, + drop_index: bool = True, + drop_id: bool = True, + drop_points: bool = True, + drop_level0: bool = True, + drop_level1: bool = True, + overwrite_xy: bool = True, + target_crs: Union[str, pyproj.crs.crs.CRS] = None, + bbox: Optional[Sequence[float]] = None, + remove_total_bounds: bool = False, + threshold_bounds: Union[float, int] = 0.1, +) -> gpd.geodataframe.GeoDataFrame: """Extracting X and Y coordinates from a GeoDataFrame (Points, LineStrings, MultiLineStrings, Polygons, Geometry Collections) and returning a GeoDataFrame with X and Y coordinates as additional columns @@ -664,76 +689,80 @@ def extract_xy(gdf: gpd.geodataframe.GeoDataFrame, GeoDataFrames that contain multiple types of geometries are currently not supported. Please use ``gdf = gdf.explode().reset_index(drop=True)`` to create a GeoDataFrame with only one type of geometries - """ + """ # Input object must be a GeoDataFrame if not isinstance(gdf, gpd.geodataframe.GeoDataFrame): - raise TypeError('Loaded object is not a GeoDataFrame') + raise TypeError("Loaded object is not a GeoDataFrame") # Checking that overwrite_xy is of type bool if not isinstance(overwrite_xy, bool): - raise TypeError('Overwrite_xy argument must be of type bool') + raise TypeError("Overwrite_xy argument must be of type bool") # Checking that X and Y columns are not in the GeoDataFrame - if not overwrite_xy and {'X', 'Y'}.issubset(gdf.columns): - raise ValueError('X and Y columns must not be present in GeoDataFrame before the extraction of coordinates') + if not overwrite_xy and {"X", "Y"}.issubset(gdf.columns): + raise ValueError( + "X and Y columns must not be present in GeoDataFrame before the extraction of coordinates" + ) # Checking that drop_level0 is of type bool if not isinstance(drop_level0, bool): - raise TypeError('Drop_index_level0 argument must be of type bool') + raise TypeError("Drop_index_level0 argument must be of type bool") # Checking that drop_level1 is of type bool if not isinstance(drop_level1, bool): - raise TypeError('Drop_index_level1 argument must be of type bool') + raise TypeError("Drop_index_level1 argument must be of type bool") # Checking that reset_index is of type bool if not isinstance(reset_index, bool): - raise TypeError('Reset_index argument must be of type bool') + raise TypeError("Reset_index argument must be of type bool") # Checking that the bbox fulfills all criteria if bbox is not None: if not isinstance(bbox, Sequence): - raise TypeError('The bbox values must be provided as a sequence') + raise TypeError("The bbox values must be provided as a sequence") # Checking that the bbox list only has four elements if len(bbox) != 4: - raise ValueError('Provide minx, maxx, miny and maxy values for the bbox') + raise ValueError("Provide minx, maxx, miny and maxy values for the bbox") # Checking that all elements of the list are of type int or float if not all(isinstance(bound, (int, float)) for bound in bbox): - raise TypeError('Bbox values must be of type float or int') + raise TypeError("Bbox values must be of type float or int") # Checking that drop_id is of type bool if not isinstance(drop_id, bool): - raise TypeError('Drop_id argument must be of type bool') + raise TypeError("Drop_id argument must be of type bool") # Checking that drop_points is of type bool if not isinstance(drop_points, bool): - raise TypeError('Drop_points argument must be of type bool') + raise TypeError("Drop_points argument must be of type bool") # Checking that drop_index is of type bool if not isinstance(drop_index, bool): - raise TypeError('Drop_index argument must be of type bool') + raise TypeError("Drop_index argument must be of type bool") # Checking that the target_crs is of type string if not isinstance(target_crs, (str, type(None), pyproj.crs.crs.CRS)): - raise TypeError('target_crs must be of type string or a pyproj object') + raise TypeError("target_crs must be of type string or a pyproj object") # Checking that remove_total_bounds is of type bool if not isinstance(remove_total_bounds, bool): - raise TypeError('Remove_total_bounds argument must be of type bool') + raise TypeError("Remove_total_bounds argument must be of type bool") # Checking that threshold_bounds is of type float or int if not isinstance(threshold_bounds, (float, int)): - raise TypeError('The value for the threshold for removing the total bounds must be of type float or int') + raise TypeError( + "The value for the threshold for removing the total bounds must be of type float or int" + ) # Checking that all Shapely Objects are valid if not all(shapely.is_valid(gdf.geometry)): - raise ValueError('Not all Shapely Objects are valid objects') + raise ValueError("Not all Shapely Objects are valid objects") # Checking that no empty Shapely Objects are present if any(shapely.is_empty(gdf.geometry)): - raise ValueError('One or more Shapely objects are empty') + raise ValueError("One or more Shapely objects are empty") # Copying GeoDataFrame gdf = gdf.copy(deep=True) @@ -746,88 +775,95 @@ def extract_xy(gdf: gpd.geodataframe.GeoDataFrame, crs = gdf.crs # Exploding polygons to collection and saving total bounds - if all(gdf.geom_type == 'Polygon'): + if all(gdf.geom_type == "Polygon"): total_bounds = gdf.total_bounds gdf = explode_polygons(gdf=gdf) else: total_bounds = None # Exploding GeometryCollections to single geometry objects - if any(gdf.geom_type == 'GeometryCollection'): - gdf = explode_geometry_collections(gdf=gdf, - reset_index=True, - drop_level0=True, - drop_level1=True, - remove_points=True) + if any(gdf.geom_type == "GeometryCollection"): + gdf = explode_geometry_collections( + gdf=gdf, + reset_index=True, + drop_level0=True, + drop_level1=True, + remove_points=True, + ) # Converting MultiLineString to LineString for further processing - if gdf.geom_type.isin(('MultiLineString', 'LineString')).all(): - gdf = explode_multilinestrings(gdf=gdf, - reset_index=True, - drop_level0=False, - drop_level1=False) + if gdf.geom_type.isin(("MultiLineString", "LineString")).all(): + gdf = explode_multilinestrings( + gdf=gdf, reset_index=True, drop_level0=False, drop_level1=False + ) # Extracting x,y coordinates from line vector data if all(gdf.geom_type == "LineString"): - gdf = extract_xy_linestrings(gdf=gdf, - reset_index=False, - drop_id=False, - drop_index=False, - drop_points=False, - overwrite_xy=overwrite_xy, - target_crs=crs, - bbox=bbox) + gdf = extract_xy_linestrings( + gdf=gdf, + reset_index=False, + drop_id=False, + drop_index=False, + drop_points=False, + overwrite_xy=overwrite_xy, + target_crs=crs, + bbox=bbox, + ) # Extracting x,y coordinates from point vector data elif all(gdf.geom_type == "Point"): - gdf = extract_xy_points(gdf=gdf, - reset_index=False, - drop_id=False, - overwrite_xy=overwrite_xy, - target_crs=crs, - bbox=bbox) + gdf = extract_xy_points( + gdf=gdf, + reset_index=False, + drop_id=False, + overwrite_xy=overwrite_xy, + target_crs=crs, + bbox=bbox, + ) else: - raise TypeError('Input Geometry Type not supported') + raise TypeError("Input Geometry Type not supported") # Resetting the index if reset_index: gdf = gdf.reset_index() # Dropping level_0 column - if reset_index and drop_level0 and 'level_0' in gdf: - gdf = gdf.drop(columns='level_0', - axis=1) + if reset_index and drop_level0 and "level_0" in gdf: + gdf = gdf.drop(columns="level_0", axis=1) # Dropping level_1 column - if reset_index and drop_level1 and 'level_1' in gdf: - gdf = gdf.drop(columns='level_1', - axis=1) + if reset_index and drop_level1 and "level_1" in gdf: + gdf = gdf.drop(columns="level_1", axis=1) # Dropping id column - if 'id' in gdf and drop_id: - gdf = gdf.drop(columns='id', - axis=1) + if "id" in gdf and drop_id: + gdf = gdf.drop(columns="id", axis=1) # Dropping index column - if 'index' in gdf and drop_index: - gdf = gdf.drop(columns='index', - axis=1) + if "index" in gdf and drop_index: + gdf = gdf.drop(columns="index", axis=1) # Dropping points column - if 'points' in gdf and drop_points: - gdf = gdf.drop(columns='points', - axis=1) + if "points" in gdf and drop_points: + gdf = gdf.drop(columns="points", axis=1) # Removing the total bounds from the gdf if remove_total_bounds and total_bounds is not None: - gdf = gdf[~(gdf['X'] <= total_bounds[0] + threshold_bounds) & - ~(gdf['X'] >= total_bounds[2] - threshold_bounds) & - ~(gdf['Y'] <= total_bounds[1] + threshold_bounds) & - ~(gdf['Y'] >= total_bounds[3] - threshold_bounds)] + gdf = gdf[ + ~(gdf["X"] <= total_bounds[0] + threshold_bounds) + & ~(gdf["X"] >= total_bounds[2] - threshold_bounds) + & ~(gdf["Y"] <= total_bounds[1] + threshold_bounds) + & ~(gdf["Y"] >= total_bounds[3] - threshold_bounds) + ] # Limiting the extent of the data if bbox is not None: - gdf = gdf[(gdf.X > bbox[0]) & (gdf.X < bbox[1]) & (gdf.Y > bbox[2]) & (gdf.Y < bbox[3])] + gdf = gdf[ + (gdf.X > bbox[0]) + & (gdf.X < bbox[1]) + & (gdf.Y > bbox[2]) + & (gdf.Y < bbox[3]) + ] # Checking and setting the dtypes of the GeoDataFrame gdf = set_dtype(gdf=gdf) @@ -839,7 +875,9 @@ def extract_xy(gdf: gpd.geodataframe.GeoDataFrame, ############################################################### -def extract_xyz_points(gdf: gpd.geodataframe.GeoDataFrame) -> gpd.geodataframe.GeoDataFrame: +def extract_xyz_points( + gdf: gpd.geodataframe.GeoDataFrame, +) -> gpd.geodataframe.GeoDataFrame: """Extracting X, Y, and Z coordinates from a GeoDataFrame containing Shapely Points with Z components Parameters @@ -894,35 +932,37 @@ def extract_xyz_points(gdf: gpd.geodataframe.GeoDataFrame) -> gpd.geodataframe.G # Checking that the input data is of type GeoDataFrame if not isinstance(gdf, gpd.geodataframe.GeoDataFrame): - raise TypeError('Loaded object is not a GeoDataFrame') + raise TypeError("Loaded object is not a GeoDataFrame") # Checking that all geometry objects are points - if not all(gdf.geom_type == 'Point'): - raise TypeError('All geometry objects must be Shapely Points') + if not all(gdf.geom_type == "Point"): + raise TypeError("All geometry objects must be Shapely Points") # Checking that all Shapely Objects are valid if not all(shapely.is_valid(gdf.geometry)): - raise ValueError('Not all Shapely Objects are valid objects') + raise ValueError("Not all Shapely Objects are valid objects") # Checking that no empty Shapely Objects are present if any(shapely.is_empty(gdf.geometry)): - raise ValueError('One or more Shapely objects are empty') + raise ValueError("One or more Shapely objects are empty") # Checking that all points have a z component if not all(shapely.has_z(gdf.geometry)): - raise TypeError('Not all Shapely Objects have a z component') + raise TypeError("Not all Shapely Objects have a z component") # Appending coordinates - gdf['X'] = shapely.get_x(gdf.geometry) - gdf['Y'] = shapely.get_y(gdf.geometry) - gdf['Z'] = shapely.get_z(gdf.geometry) + gdf["X"] = shapely.get_x(gdf.geometry) + gdf["Y"] = shapely.get_y(gdf.geometry) + gdf["Z"] = shapely.get_z(gdf.geometry) return gdf -def extract_xyz_linestrings(gdf: gpd.geodataframe.GeoDataFrame, - reset_index: bool = True, - drop_index: bool = True) -> gpd.geodataframe.GeoDataFrame: +def extract_xyz_linestrings( + gdf: gpd.geodataframe.GeoDataFrame, + reset_index: bool = True, + drop_index: bool = True, +) -> gpd.geodataframe.GeoDataFrame: """Extracting X, Y, and Z coordinates from a GeoDataFrame containing Shapely LineStrings with Z components Parameters @@ -985,60 +1025,63 @@ def extract_xyz_linestrings(gdf: gpd.geodataframe.GeoDataFrame, """ # Checking that the input data is of type GeoDataFrame if not isinstance(gdf, gpd.geodataframe.GeoDataFrame): - raise TypeError('Loaded object is not a GeoDataFrame') + raise TypeError("Loaded object is not a GeoDataFrame") # Checking that all geometry objects are points if not all(shapely.get_type_id(gdf.geometry) == 1): - raise TypeError('All geometry objects must be Shapely LineStrings') + raise TypeError("All geometry objects must be Shapely LineStrings") # Checking that all Shapely Objects are valid if not all(shapely.is_valid(gdf.geometry)): - raise ValueError('Not all Shapely Objects are valid objects') + raise ValueError("Not all Shapely Objects are valid objects") # Checking that no empty Shapely Objects are present if any(shapely.is_empty(gdf.geometry)): - raise ValueError('One or more Shapely objects are empty') + raise ValueError("One or more Shapely objects are empty") # Checking that all points have a z component if not all(shapely.has_z(gdf.geometry)): - raise TypeError('Not all Shapely Objects have a z component') + raise TypeError("Not all Shapely Objects have a z component") # Checking that reset_index is of type bool if not isinstance(reset_index, bool): - raise TypeError('Reset_index argument must be of type bool') + raise TypeError("Reset_index argument must be of type bool") # Checking that drop_index is of type bool if not isinstance(drop_index, bool): - raise TypeError('Drop_index argument must be of type bool') + raise TypeError("Drop_index argument must be of type bool") # Extracting x,y coordinates from line vector data - gdf['points'] = [shapely.get_coordinates(gdf.geometry[i], include_z=True) for i in range(len(gdf))] - df = pd.DataFrame(data=gdf).explode('points') + gdf["points"] = [ + shapely.get_coordinates(gdf.geometry[i], include_z=True) + for i in range(len(gdf)) + ] + df = pd.DataFrame(data=gdf).explode("points") # Appending Column to DataFrame - df[['X', 'Y', 'Z']] = pd.DataFrame(data=df['points'].tolist(), - index=df.index) + df[["X", "Y", "Z"]] = pd.DataFrame(data=df["points"].tolist(), index=df.index) # Resetting index if reset_index: df = df.reset_index() # Creating new GeoDataFrame - gdf = gpd.GeoDataFrame(data=df, - geometry=gpd.points_from_xy(df.X, df.Y), - crs=gdf.crs) + gdf = gpd.GeoDataFrame( + data=df, geometry=gpd.points_from_xy(df.X, df.Y), crs=gdf.crs + ) # Dropping index column - if 'index' in gdf and drop_index: - gdf = gdf.drop(columns='index', - axis=1) + if "index" in gdf and drop_index: + gdf = gdf.drop(columns="index", axis=1) return gdf -def extract_xyz_polygons(gdf: gpd.geodataframe.GeoDataFrame, - reset_index: bool = True, - drop_index: bool = True) -> gpd.geodataframe.GeoDataFrame: +def extract_xyz_polygons( + gdf: gpd.geodataframe.GeoDataFrame, + reset_index: bool = True, + drop_index: bool = True, +) -> gpd.geodataframe.GeoDataFrame: """Extracting X, Y, and Z coordinates from a GeoDataFrame containing Shapely Polygons with Z components Parameters @@ -1103,72 +1146,74 @@ def extract_xyz_polygons(gdf: gpd.geodataframe.GeoDataFrame, # Checking that the input data is of type GeoDataFrame if not isinstance(gdf, gpd.geodataframe.GeoDataFrame): - raise TypeError('Loaded object is not a GeoDataFrame') + raise TypeError("Loaded object is not a GeoDataFrame") # Checking that all geometry objects are points if not all(shapely.get_type_id(gdf.geometry) == 3): - raise TypeError('All geometry objects must be Shapely Polygons') + raise TypeError("All geometry objects must be Shapely Polygons") # Checking that all Shapely Objects are valid if not all(shapely.is_valid(gdf.geometry)): - raise ValueError('Not all Shapely Objects are valid objects') + raise ValueError("Not all Shapely Objects are valid objects") # Checking that no empty Shapely Objects are present if any(shapely.is_empty(gdf.geometry)): - raise ValueError('One or more Shapely objects are empty') + raise ValueError("One or more Shapely objects are empty") # Checking that all points have a z component if not all(shapely.has_z(gdf.geometry)): - raise TypeError('Not all Shapely Objects have a z component') + raise TypeError("Not all Shapely Objects have a z component") # Checking that reset_index is of type bool if not isinstance(reset_index, bool): - raise TypeError('Reset_index argument must be of type bool') + raise TypeError("Reset_index argument must be of type bool") # Checking that drop_index is of type bool if not isinstance(drop_index, bool): - raise TypeError('Drop_index argument must be of type bool') + raise TypeError("Drop_index argument must be of type bool") # Extracting x,y coordinates from line vector data - gdf['points'] = [shapely.get_coordinates(gdf.geometry[i], include_z=True) for i in range(len(gdf))] - df = pd.DataFrame(data=gdf).explode('points') + gdf["points"] = [ + shapely.get_coordinates(gdf.geometry[i], include_z=True) + for i in range(len(gdf)) + ] + df = pd.DataFrame(data=gdf).explode("points") # Appending Column to DataFrame - df[['X', 'Y', 'Z']] = pd.DataFrame(data=df['points'].tolist(), - index=df.index) + df[["X", "Y", "Z"]] = pd.DataFrame(data=df["points"].tolist(), index=df.index) # Resetting index if reset_index: df = df.reset_index() # Creating new GeoDataFrame - gdf = gpd.GeoDataFrame(data=df, - geometry=gpd.points_from_xy(df.X, df.Y), - crs=gdf.crs) + gdf = gpd.GeoDataFrame( + data=df, geometry=gpd.points_from_xy(df.X, df.Y), crs=gdf.crs + ) # Dropping index column - if 'index' in gdf and drop_index: - gdf = gdf.drop(columns='index', - axis=1) + if "index" in gdf and drop_index: + gdf = gdf.drop(columns="index", axis=1) return gdf -def extract_xyz_rasterio(gdf: gpd.geodataframe.GeoDataFrame, - dem: rasterio.io.DatasetReader, - minz: float = None, - maxz: float = None, - reset_index: bool = True, - drop_index: bool = True, - drop_id: bool = True, - drop_points: bool = True, - drop_level0: bool = True, - drop_level1: bool = True, - target_crs: Union[str, pyproj.crs.crs.CRS, rasterio.crs.CRS] = None, - bbox: Optional[Sequence[float]] = None, - remove_total_bounds: bool = False, - threshold_bounds: Union[float, int] = 0.1 - ) -> gpd.geodataframe.GeoDataFrame: +def extract_xyz_rasterio( + gdf: gpd.geodataframe.GeoDataFrame, + dem: rasterio.io.DatasetReader, + minz: float = None, + maxz: float = None, + reset_index: bool = True, + drop_index: bool = True, + drop_id: bool = True, + drop_points: bool = True, + drop_level0: bool = True, + drop_level1: bool = True, + target_crs: Union[str, pyproj.crs.crs.CRS, rasterio.crs.CRS] = None, + bbox: Optional[Sequence[float]] = None, + remove_total_bounds: bool = False, + threshold_bounds: Union[float, int] = 0.1, +) -> gpd.geodataframe.GeoDataFrame: """Extracting X and Y coordinates from a GeoDataFrame (Points, LineStrings, MultiLineStrings Polygons) and z values from a rasterio object and returning a GeoDataFrame with X, Y, and Z coordinates as additional columns @@ -1273,186 +1318,199 @@ def extract_xyz_rasterio(gdf: gpd.geodataframe.GeoDataFrame, # Checking that the input data is of type GeoDataFrame if not isinstance(gdf, gpd.geodataframe.GeoDataFrame): - raise TypeError('Loaded object is not a GeoDataFrame') + raise TypeError("Loaded object is not a GeoDataFrame") # Checking that the dem is a rasterio object if not isinstance(dem, rasterio.io.DatasetReader): - raise TypeError('DEM must be a rasterio object') + raise TypeError("DEM must be a rasterio object") # Checking that the geometry types of the GeoDataFrame are the supported types - if not gdf.geom_type.isin(('MultiLineString', 'LineString', 'Point', 'Polygon', 'GeometryCollection')).all(): - raise TypeError('Geometry type within GeoDataFrame not supported') + if not gdf.geom_type.isin( + ("MultiLineString", "LineString", "Point", "Polygon", "GeometryCollection") + ).all(): + raise TypeError("Geometry type within GeoDataFrame not supported") # Checking that drop_level0 is of type bool if not isinstance(drop_level0, bool): - raise TypeError('Drop_index_level0 argument must be of type bool') + raise TypeError("Drop_index_level0 argument must be of type bool") # Checking that drop_level1 is of type bool if not isinstance(drop_level1, bool): - raise TypeError('Drop_index_level1 argument must be of type bool') + raise TypeError("Drop_index_level1 argument must be of type bool") # Checking that reset_index is of type bool if not isinstance(reset_index, bool): - raise TypeError('Reset_index argument must be of type bool') + raise TypeError("Reset_index argument must be of type bool") # Checking that drop_id is of type bool if not isinstance(drop_id, bool): - raise TypeError('Drop_id argument must be of type bool') + raise TypeError("Drop_id argument must be of type bool") # Checking that drop_points is of type bool if not isinstance(drop_points, bool): - raise TypeError('Drop_points argument must be of type bool') + raise TypeError("Drop_points argument must be of type bool") # Checking that remove_total_bounds is of type bool if not isinstance(remove_total_bounds, bool): - raise TypeError('Remove_total_bounds argument must be of type bool') + raise TypeError("Remove_total_bounds argument must be of type bool") # Checking that threshold_bounds is of type float or int if not isinstance(threshold_bounds, (float, int)): - raise TypeError('The value for the threshold for removing the total bounds must be of type float or int') + raise TypeError( + "The value for the threshold for removing the total bounds must be of type float or int" + ) # Checking the GeoDataFrame does not contain a Z value - if 'Z' in gdf: - raise ValueError('Data already contains Z-values') + if "Z" in gdf: + raise ValueError("Data already contains Z-values") # Checking that the bbox fulfills all criteria if bbox is not None: if not isinstance(bbox, Sequence): - raise TypeError('The bbox values must be provided as a sequence') + raise TypeError("The bbox values must be provided as a sequence") # Checking that the bbox list only has four elements if len(bbox) != 4: - raise ValueError('Provide minx, maxx, miny and maxy values for the bbox') + raise ValueError("Provide minx, maxx, miny and maxy values for the bbox") # Checking that all elements of the list are of type int or float if not all(isinstance(bound, (int, float)) for bound in bbox): - raise TypeError('Bbox values must be of type float or int') + raise TypeError("Bbox values must be of type float or int") # Checking that the target_crs is of type string - if not isinstance(target_crs, (str, type(None), pyproj.crs.crs.CRS, rasterio.crs.CRS)): - raise TypeError('target_crs must be of type string, pyproj CRS or rasterio CRS') + if not isinstance( + target_crs, (str, type(None), pyproj.crs.crs.CRS, rasterio.crs.CRS) + ): + raise TypeError("target_crs must be of type string, pyproj CRS or rasterio CRS") # Checking that the minz value is of type float if not isinstance(minz, (float, int, type(None))): - raise TypeError('minz value must be of type float or int') + raise TypeError("minz value must be of type float or int") # Checking that the max value is of type float if not isinstance(maxz, (float, int, type(None))): - raise TypeError('minz value must be of type float or int') + raise TypeError("minz value must be of type float or int") # Checking that minz is smaller than maxz if minz is not None and maxz is not None and minz >= maxz: - raise ValueError('minz must be smaller than maxz') + raise ValueError("minz must be smaller than maxz") # Checking that all Shapely Objects are valid if not all(shapely.is_valid(gdf.geometry)): - raise ValueError('Not all Shapely Objects are valid objects') + raise ValueError("Not all Shapely Objects are valid objects") # Checking that no empty Shapely Objects are present if any(shapely.is_empty(gdf.geometry)): - raise ValueError('One or more Shapely objects are empty') + raise ValueError("One or more Shapely objects are empty") # Create deep copy of gdf gdf = gdf.copy(deep=True) # Extracting X and Y coordinates if they are not present in the GeoDataFrame - if not {'X', 'Y'}.issubset(gdf.columns): - gdf = extract_xy(gdf=gdf, - reset_index=False, - drop_index=False, - drop_id=False, - drop_points=False, - drop_level0=False, - drop_level1=False, - overwrite_xy=False, - target_crs=None, - bbox=None, - remove_total_bounds=remove_total_bounds, - threshold_bounds=threshold_bounds) + if not {"X", "Y"}.issubset(gdf.columns): + gdf = extract_xy( + gdf=gdf, + reset_index=False, + drop_index=False, + drop_id=False, + drop_points=False, + drop_level0=False, + drop_level1=False, + overwrite_xy=False, + target_crs=None, + bbox=None, + remove_total_bounds=remove_total_bounds, + threshold_bounds=threshold_bounds, + ) # If the CRS of the gdf and the dem are identical, just extract the heights using the rasterio sample method # NB: for points outside the bounds of the raster, nodata values will be returned if gdf.crs == dem.crs: - gdf['Z'] = sample_from_rasterio(raster=dem, - point_x=gdf['X'].tolist(), - point_y=gdf['Y'].tolist()) + gdf["Z"] = sample_from_rasterio( + raster=dem, point_x=gdf["X"].tolist(), point_y=gdf["Y"].tolist() + ) # If the CRS of the gdf and the dem are not identical, the coordinates of the gdf will be reprojected and the # z values will be appended to the original gdf else: gdf_reprojected = gdf.to_crs(crs=dem.crs) - gdf_reprojected = extract_xy(gdf=gdf_reprojected, - reset_index=False, - drop_index=False, - drop_id=False, - drop_points=False, - drop_level0=False, - drop_level1=False, - overwrite_xy=True, - target_crs=None, - bbox=None, - remove_total_bounds=remove_total_bounds, - threshold_bounds=threshold_bounds - ) - - gdf['Z'] = sample_from_rasterio(raster=dem, - point_x=gdf_reprojected['X'].tolist(), - point_y=gdf_reprojected['Y'].tolist()) + gdf_reprojected = extract_xy( + gdf=gdf_reprojected, + reset_index=False, + drop_index=False, + drop_id=False, + drop_points=False, + drop_level0=False, + drop_level1=False, + overwrite_xy=True, + target_crs=None, + bbox=None, + remove_total_bounds=remove_total_bounds, + threshold_bounds=threshold_bounds, + ) + + gdf["Z"] = sample_from_rasterio( + raster=dem, + point_x=gdf_reprojected["X"].tolist(), + point_y=gdf_reprojected["Y"].tolist(), + ) # Reprojecting coordinates to provided target_crs if target_crs is not None: gdf = gdf.to_crs(crs=target_crs) # Extracting the X and Y coordinates of the reprojected gdf - gdf = extract_xy(gdf, - reset_index=False, - drop_index=False, - drop_id=False, - drop_points=False, - drop_level0=False, - drop_level1=False, - overwrite_xy=True, - target_crs=None, - bbox=None, - remove_total_bounds=remove_total_bounds, - threshold_bounds=threshold_bounds) + gdf = extract_xy( + gdf, + reset_index=False, + drop_index=False, + drop_id=False, + drop_points=False, + drop_level0=False, + drop_level1=False, + overwrite_xy=True, + target_crs=None, + bbox=None, + remove_total_bounds=remove_total_bounds, + threshold_bounds=threshold_bounds, + ) # Dropping level_0 column - if reset_index and drop_level0 and 'level_0' in gdf: - gdf = gdf.drop(columns='level_0', - axis=1) + if reset_index and drop_level0 and "level_0" in gdf: + gdf = gdf.drop(columns="level_0", axis=1) # Dropping level_1 column - if reset_index and drop_level1 and 'level_1' in gdf: - gdf = gdf.drop(columns='level_1', - axis=1) + if reset_index and drop_level1 and "level_1" in gdf: + gdf = gdf.drop(columns="level_1", axis=1) # Dropping id column - if 'id' in gdf and drop_id: - gdf = gdf.drop(columns='id', - axis=1) + if "id" in gdf and drop_id: + gdf = gdf.drop(columns="id", axis=1) # Dropping index column - if 'index' in gdf and drop_index: - gdf = gdf.drop(columns='index', - axis=1) + if "index" in gdf and drop_index: + gdf = gdf.drop(columns="index", axis=1) # Dropping points column - if 'points' in gdf and drop_points: - gdf = gdf.drop(columns='points', - axis=1) + if "points" in gdf and drop_points: + gdf = gdf.drop(columns="points", axis=1) # Limiting the extent of the data if bbox is not None: - gdf = gdf[(gdf.X > bbox[0]) & (gdf.X < bbox[1]) & (gdf.Y > bbox[2]) & (gdf.Y < bbox[3])] + gdf = gdf[ + (gdf.X > bbox[0]) + & (gdf.X < bbox[1]) + & (gdf.Y > bbox[2]) + & (gdf.Y < bbox[3]) + ] # Limiting the data to specified elevations if minz is not None: - gdf = gdf[gdf['Z'] >= minz] + gdf = gdf[gdf["Z"] >= minz] if maxz is not None: - gdf = gdf[gdf['Z'] <= maxz] + gdf = gdf[gdf["Z"] <= maxz] # Resetting the index if reset_index: @@ -1464,22 +1522,23 @@ def extract_xyz_rasterio(gdf: gpd.geodataframe.GeoDataFrame, return gdf -def extract_xyz_array(gdf: gpd.geodataframe.GeoDataFrame, - dem: np.ndarray, - extent: List[float], - minz: float = None, - maxz: float = None, - reset_index: bool = True, - drop_index: bool = True, - drop_id: bool = True, - drop_points: bool = True, - drop_level0: bool = True, - drop_level1: bool = True, - target_crs: Union[str, pyproj.crs.crs.CRS] = None, - bbox: Optional[Sequence[float]] = None, - remove_total_bounds: bool = False, - threshold_bounds: Union[float, int] = 0.1 - ) -> gpd.geodataframe.GeoDataFrame: +def extract_xyz_array( + gdf: gpd.geodataframe.GeoDataFrame, + dem: np.ndarray, + extent: List[float], + minz: float = None, + maxz: float = None, + reset_index: bool = True, + drop_index: bool = True, + drop_id: bool = True, + drop_points: bool = True, + drop_level0: bool = True, + drop_level1: bool = True, + target_crs: Union[str, pyproj.crs.crs.CRS] = None, + bbox: Optional[Sequence[float]] = None, + remove_total_bounds: bool = False, + threshold_bounds: Union[float, int] = 0.1, +) -> gpd.geodataframe.GeoDataFrame: """Extracting X and Y coordinates from a GeoDataFrame (Points, LineStrings, MultiLineStrings Polygons) and Z values from a NumPy nd.array and returning a GeoDataFrame with X, Y, and Z coordinates as additional columns @@ -1593,183 +1652,190 @@ def extract_xyz_array(gdf: gpd.geodataframe.GeoDataFrame, # Checking that the input data is of type GeoDataFrame if not isinstance(gdf, gpd.geodataframe.GeoDataFrame): - raise TypeError('Loaded object is not a GeoDataFrame') + raise TypeError("Loaded object is not a GeoDataFrame") # Checking that the dem is a np.ndarray if not isinstance(dem, np.ndarray): - raise TypeError('DEM must be a numpy.ndarray') + raise TypeError("DEM must be a numpy.ndarray") # Checking that the geometry types of the GeoDataFrame are the supported types - if not gdf.geom_type.isin(('MultiLineString', 'LineString', 'Point', 'Polygon')).all(): - raise TypeError('Geometry type within GeoDataFrame not supported') + if not gdf.geom_type.isin( + ("MultiLineString", "LineString", "Point", "Polygon") + ).all(): + raise TypeError("Geometry type within GeoDataFrame not supported") # Checking that drop_level0 is of type bool if not isinstance(drop_level0, bool): - raise TypeError('Drop_index_level0 argument must be of type bool') + raise TypeError("Drop_index_level0 argument must be of type bool") # Checking that drop_level1 is of type bool if not isinstance(drop_level1, bool): - raise TypeError('Drop_index_level1 argument must be of type bool') + raise TypeError("Drop_index_level1 argument must be of type bool") # Checking that reset_index is of type bool if not isinstance(reset_index, bool): - raise TypeError('Reset_index argument must be of type bool') + raise TypeError("Reset_index argument must be of type bool") # Checking that drop_id is of type bool if not isinstance(drop_id, bool): - raise TypeError('Drop_id argument must be of type bool') + raise TypeError("Drop_id argument must be of type bool") # Checking that drop_points is of type bool if not isinstance(drop_points, bool): - raise TypeError('Drop_points argument must be of type bool') + raise TypeError("Drop_points argument must be of type bool") # Checking that drop_id is of type bool if not isinstance(drop_index, bool): - raise TypeError('Drop_index argument must be of type bool') + raise TypeError("Drop_index argument must be of type bool") # Checking that the extent is of type list if not isinstance(extent, list): - raise TypeError('Extent must be of type list') + raise TypeError("Extent must be of type list") # Checking that all elements of the extent are of type int or float if not all(isinstance(n, (int, float)) for n in extent): - raise TypeError('Extent values must be of type int or float') + raise TypeError("Extent values must be of type int or float") # Checking that remove_total_bounds is of type bool if not isinstance(remove_total_bounds, bool): - raise TypeError('Remove_total_bounds argument must be of type bool') + raise TypeError("Remove_total_bounds argument must be of type bool") # Checking that threshold_bounds is of type float or int if not isinstance(threshold_bounds, (float, int)): - raise TypeError('The value for the threshold for removing the total bounds must be of type float or int') + raise TypeError( + "The value for the threshold for removing the total bounds must be of type float or int" + ) # Checking that the length of the list is either four or six if extent is not None: if not len(extent) == 4: if not len(extent) == 6: - raise ValueError('The extent must include only four or six values') + raise ValueError("The extent must include only four or six values") # Checking that the bbox fulfills all criteria if bbox is not None: if not isinstance(bbox, Sequence): - raise TypeError('The bbox values must be provided as a sequence') + raise TypeError("The bbox values must be provided as a sequence") # Checking that the bbox list only has four elements if len(bbox) != 4: - raise ValueError('Provide minx, maxx, miny and maxy values for the bbox') + raise ValueError("Provide minx, maxx, miny and maxy values for the bbox") # Checking that all elements of the list are of type int or float if not all(isinstance(bound, (int, float)) for bound in bbox): - raise TypeError('Bbox values must be of type float or int') + raise TypeError("Bbox values must be of type float or int") # Checking that the target_crs is of type string if not isinstance(target_crs, (str, type(None), pyproj.crs.crs.CRS)): - raise TypeError('target_crs must be of type string or a pyproj object') + raise TypeError("target_crs must be of type string or a pyproj object") # Selecting x and y bounds if bbox contains values for all three directions x, y, z extent = extent[:4] # Checking that the minz value is of type float if not isinstance(minz, (float, int, type(None))): - raise TypeError('minz value must be of type float or int') + raise TypeError("minz value must be of type float or int") # Checking that the max value is of type float if not isinstance(maxz, (float, int, type(None))): - raise TypeError('minz value must be of type float or int') + raise TypeError("minz value must be of type float or int") # Checking that minz is smaller than maxz if minz is not None and maxz is not None and minz >= maxz: - raise ValueError('minz must be smaller than maxz') + raise ValueError("minz must be smaller than maxz") # Checking that the GeoDataFrame does not contain a Z value - if 'Z' in gdf: - raise ValueError('Data already contains Z-values') + if "Z" in gdf: + raise ValueError("Data already contains Z-values") # Checking that all Shapely Objects are valid if not all(shapely.is_valid(gdf.geometry)): - raise ValueError('Not all Shapely Objects are valid objects') + raise ValueError("Not all Shapely Objects are valid objects") # Checking that no empty Shapely Objects are present if any(shapely.is_empty(gdf.geometry)): - raise ValueError('One or more Shapely objects are empty') + raise ValueError("One or more Shapely objects are empty") # Extracting X and Y coordinates if they are not present in the GeoDataFrame - if not {'X', 'Y'}.issubset(gdf.columns): - gdf = extract_xy(gdf=gdf, - reset_index=False, - drop_index=False, - drop_id=False, - drop_points=False, - drop_level0=False, - drop_level1=False, - overwrite_xy=False, - target_crs=None, - bbox=None, - remove_total_bounds=remove_total_bounds, - threshold_bounds=threshold_bounds) - - gdf['Z'] = sample_from_array(array=dem, - extent=extent, - point_x=gdf['X'].values, - point_y=gdf['Y'].values) + if not {"X", "Y"}.issubset(gdf.columns): + gdf = extract_xy( + gdf=gdf, + reset_index=False, + drop_index=False, + drop_id=False, + drop_points=False, + drop_level0=False, + drop_level1=False, + overwrite_xy=False, + target_crs=None, + bbox=None, + remove_total_bounds=remove_total_bounds, + threshold_bounds=threshold_bounds, + ) + + gdf["Z"] = sample_from_array( + array=dem, extent=extent, point_x=gdf["X"].values, point_y=gdf["Y"].values + ) # Reprojecting coordinates to provided target_crs if target_crs is not None: gdf = gdf.to_crs(crs=target_crs) # Extracting the X and Y coordinates of the reprojected gdf - gdf = extract_xy(gdf=gdf, - reset_index=False, - drop_index=False, - drop_id=False, - drop_points=False, - drop_level0=False, - drop_level1=False, - overwrite_xy=True, - target_crs=None, - bbox=None, - remove_total_bounds=remove_total_bounds, - threshold_bounds=threshold_bounds) + gdf = extract_xy( + gdf=gdf, + reset_index=False, + drop_index=False, + drop_id=False, + drop_points=False, + drop_level0=False, + drop_level1=False, + overwrite_xy=True, + target_crs=None, + bbox=None, + remove_total_bounds=remove_total_bounds, + threshold_bounds=threshold_bounds, + ) # Resetting the index if reset_index: gdf = gdf.reset_index() # Dropping level_0 column - if reset_index and drop_level0 and 'level_0' in gdf: - gdf = gdf.drop(columns='level_0', - axis=1) + if reset_index and drop_level0 and "level_0" in gdf: + gdf = gdf.drop(columns="level_0", axis=1) # Dropping level_1 column - if reset_index and drop_level1 and 'level_1' in gdf: - gdf = gdf.drop(columns='level_1', - axis=1) + if reset_index and drop_level1 and "level_1" in gdf: + gdf = gdf.drop(columns="level_1", axis=1) # Dropping id column - if 'id' in gdf and drop_id: - gdf = gdf.drop(columns='id', - axis=1) + if "id" in gdf and drop_id: + gdf = gdf.drop(columns="id", axis=1) # Dropping index column - if 'index' in gdf and drop_index: - gdf = gdf.drop(columns='index', - axis=1) + if "index" in gdf and drop_index: + gdf = gdf.drop(columns="index", axis=1) # Dropping points column - if 'points' in gdf and drop_points: - gdf = gdf.drop(columns='points', - axis=1) + if "points" in gdf and drop_points: + gdf = gdf.drop(columns="points", axis=1) # Limiting the extent of the data if bbox is not None: - gdf = gdf[(gdf.X > bbox[0]) & (gdf.X < bbox[1]) & (gdf.Y > bbox[2]) & (gdf.Y < bbox[3])] + gdf = gdf[ + (gdf.X > bbox[0]) + & (gdf.X < bbox[1]) + & (gdf.Y > bbox[2]) + & (gdf.Y < bbox[3]) + ] # Limiting the data to specified elevations if minz is not None: - gdf = gdf[gdf['Z'] >= minz] + gdf = gdf[gdf["Z"] >= minz] if maxz is not None: - gdf = gdf[gdf['Z'] <= maxz] + gdf = gdf[gdf["Z"] <= maxz] # Checking and setting the dtypes of the GeoDataFrame gdf = set_dtype(gdf=gdf) @@ -1777,22 +1843,23 @@ def extract_xyz_array(gdf: gpd.geodataframe.GeoDataFrame, return gdf -def extract_xyz(gdf: gpd.geodataframe.GeoDataFrame, - dem: Union[np.ndarray, rasterio.io.DatasetReader] = None, - minz: float = None, - maxz: float = None, - extent: List[Union[float, int]] = None, - reset_index: bool = True, - drop_index: bool = True, - drop_id: bool = True, - drop_points: bool = True, - drop_level0: bool = True, - drop_level1: bool = True, - target_crs: Union[str, pyproj.crs.crs.CRS, rasterio.crs.CRS] = None, - bbox: Optional[Sequence[float]] = None, - remove_total_bounds: bool = False, - threshold_bounds: Union[float, int] = 0.1 - ) -> gpd.geodataframe.GeoDataFrame: +def extract_xyz( + gdf: gpd.geodataframe.GeoDataFrame, + dem: Union[np.ndarray, rasterio.io.DatasetReader] = None, + minz: float = None, + maxz: float = None, + extent: List[Union[float, int]] = None, + reset_index: bool = True, + drop_index: bool = True, + drop_id: bool = True, + drop_points: bool = True, + drop_level0: bool = True, + drop_level1: bool = True, + target_crs: Union[str, pyproj.crs.crs.CRS, rasterio.crs.CRS] = None, + bbox: Optional[Sequence[float]] = None, + remove_total_bounds: bool = False, + threshold_bounds: Union[float, int] = 0.1, +) -> gpd.geodataframe.GeoDataFrame: """Extracting X and Y coordinates from a GeoDataFrame (Points, LineStrings, MultiLineStrings Polygons) and Z values from a NumPy nd.array or a Rasterio object and returning a GeoDataFrame with X, Y, and Z coordinates as additional columns @@ -1904,56 +1971,62 @@ def extract_xyz(gdf: gpd.geodataframe.GeoDataFrame, # Checking that the input data is of type GeoDataFrame if not isinstance(gdf, gpd.geodataframe.GeoDataFrame): - raise TypeError('Loaded object is not a GeoDataFrame') + raise TypeError("Loaded object is not a GeoDataFrame") # Checking that the dem is a np.ndarray or rasterio object if not isinstance(dem, (np.ndarray, rasterio.io.DatasetReader, type(None))): - raise TypeError('DEM must be a numpy.ndarray or rasterio object') + raise TypeError("DEM must be a numpy.ndarray or rasterio object") # Checking that the geometry types of the GeoDataFrame are the supported types - if not gdf.geom_type.isin(('MultiLineString', 'LineString', 'Point', 'Polygon', 'GeometryCollection')).all(): - raise TypeError('Geometry type within GeoDataFrame not supported') + if not gdf.geom_type.isin( + ("MultiLineString", "LineString", "Point", "Polygon", "GeometryCollection") + ).all(): + raise TypeError("Geometry type within GeoDataFrame not supported") # Checking that drop_level0 is of type bool if not isinstance(drop_level0, bool): - raise TypeError('Drop_index_level0 argument must be of type bool') + raise TypeError("Drop_index_level0 argument must be of type bool") # Checking that drop_level1 is of type bool if not isinstance(drop_level1, bool): - raise TypeError('Drop_index_level1 argument must be of type bool') + raise TypeError("Drop_index_level1 argument must be of type bool") # Checking that reset_index is of type bool if not isinstance(reset_index, bool): - raise TypeError('Reset_index argument must be of type bool') + raise TypeError("Reset_index argument must be of type bool") # Checking that drop_id is of type bool if not isinstance(drop_id, bool): - raise TypeError('Drop_id argument must be of type bool') + raise TypeError("Drop_id argument must be of type bool") # Checking that drop_points is of type bool if not isinstance(drop_points, bool): - raise TypeError('Drop_points argument must be of type bool') + raise TypeError("Drop_points argument must be of type bool") # Checking that drop_id is of type bool if not isinstance(drop_index, bool): - raise TypeError('Drop_index argument must be of type bool') + raise TypeError("Drop_index argument must be of type bool") # Checking that the target_crs is of type string - if not isinstance(target_crs, (str, type(None), pyproj.crs.crs.CRS, rasterio.crs.CRS)): - raise TypeError('target_crs must be of type string, pyproj CRS or rasterio CRS') + if not isinstance( + target_crs, (str, type(None), pyproj.crs.crs.CRS, rasterio.crs.CRS) + ): + raise TypeError("target_crs must be of type string, pyproj CRS or rasterio CRS") # Checking that the extent is of type list if isinstance(dem, np.ndarray) and not isinstance(extent, list): - raise TypeError('Extent must be of type list') + raise TypeError("Extent must be of type list") # Checking that all elements of the extent are of type int or float - if isinstance(dem, np.ndarray) and not all(isinstance(n, (int, float)) for n in extent): - raise TypeError('Extent values must be of type int or float') + if isinstance(dem, np.ndarray) and not all( + isinstance(n, (int, float)) for n in extent + ): + raise TypeError("Extent values must be of type int or float") # Checking that the length of the list is either four or six if extent is not None: if len(extent) not in (4, 6): - raise ValueError('The extent must include only four or six values') + raise ValueError("The extent must include only four or six values") # Selecting x and y bounds if bbox contains values for all three directions x, y, z if isinstance(dem, np.ndarray) and len(extent) == 6: @@ -1961,41 +2034,43 @@ def extract_xyz(gdf: gpd.geodataframe.GeoDataFrame, # Checking that the minz value is of type float if not isinstance(minz, (float, int, type(None))): - raise TypeError('minz value must be of type float or int') + raise TypeError("minz value must be of type float or int") # Checking that the max value is of type float if not isinstance(maxz, (float, int, type(None))): - raise TypeError('minz value must be of type float or int') + raise TypeError("minz value must be of type float or int") # Checking that minz is smaller than maxz if minz is not None and maxz is not None and minz >= maxz: - raise ValueError('minz must be smaller than maxz') + raise ValueError("minz must be smaller than maxz") # Checking that the bbox fulfills all criteria if bbox is not None: if not isinstance(bbox, Sequence): - raise TypeError('The bbox values must be provided as a sequence') + raise TypeError("The bbox values must be provided as a sequence") # Checking that the bbox list only has four elements if len(bbox) != 4: - raise ValueError('Provide minx, maxx, miny and maxy values for the bbox') + raise ValueError("Provide minx, maxx, miny and maxy values for the bbox") # Checking that all elements of the list are of type int or float if not all(isinstance(bound, (int, float)) for bound in bbox): - raise TypeError('Bbox values must be of type float or int') + raise TypeError("Bbox values must be of type float or int") # Checking the GeoDataFrame does not contain a Z value - if 'Z' in gdf and dem is not None: - raise ValueError('Data already contains Z-values. Please use dem=None to indicate that no DEM is needed or ' - 'remove Z values.') + if "Z" in gdf and dem is not None: + raise ValueError( + "Data already contains Z-values. Please use dem=None to indicate that no DEM is needed or " + "remove Z values." + ) # Checking that all Shapely Objects are valid if not all(shapely.is_valid(gdf.geometry)): - raise ValueError('Not all Shapely Objects are valid objects') + raise ValueError("Not all Shapely Objects are valid objects") # Checking that no empty Shapely Objects are present if any(shapely.is_empty(gdf.geometry)): - raise ValueError('One or more Shapely objects are empty') + raise ValueError("One or more Shapely objects are empty") # Reprojecting coordinates to provided target_crs if target_crs is not None: @@ -2003,84 +2078,92 @@ def extract_xyz(gdf: gpd.geodataframe.GeoDataFrame, # Extracting xyz if isinstance(dem, rasterio.io.DatasetReader): - gdf = extract_xyz_rasterio(gdf=gdf, - dem=dem, - reset_index=False, - drop_id=False, - drop_index=False, - drop_level0=False, - drop_level1=False, - drop_points=False, - remove_total_bounds=remove_total_bounds, - threshold_bounds=threshold_bounds) + gdf = extract_xyz_rasterio( + gdf=gdf, + dem=dem, + reset_index=False, + drop_id=False, + drop_index=False, + drop_level0=False, + drop_level1=False, + drop_points=False, + remove_total_bounds=remove_total_bounds, + threshold_bounds=threshold_bounds, + ) elif isinstance(dem, np.ndarray): - gdf = extract_xyz_array(gdf=gdf, - dem=dem, - extent=extent, - reset_index=False, - drop_id=False, - drop_index=False, - drop_level0=False, - drop_level1=False, - drop_points=False, - remove_total_bounds=remove_total_bounds, - threshold_bounds=threshold_bounds) + gdf = extract_xyz_array( + gdf=gdf, + dem=dem, + extent=extent, + reset_index=False, + drop_id=False, + drop_index=False, + drop_level0=False, + drop_level1=False, + drop_points=False, + remove_total_bounds=remove_total_bounds, + threshold_bounds=threshold_bounds, + ) # Extracting XYZ from point consisting of a Z value - elif all(shapely.has_z(gdf.geometry)) and all(shapely.get_type_id(gdf.geometry) == 0): + elif all(shapely.has_z(gdf.geometry)) and all( + shapely.get_type_id(gdf.geometry) == 0 + ): gdf = extract_xyz_points(gdf=gdf) else: - gdf = extract_xy(gdf=gdf, - reset_index=False, - drop_id=False, - drop_index=False, - drop_level0=False, - drop_level1=False, - drop_points=False, - remove_total_bounds=remove_total_bounds, - threshold_bounds=threshold_bounds - ) + gdf = extract_xy( + gdf=gdf, + reset_index=False, + drop_id=False, + drop_index=False, + drop_level0=False, + drop_level1=False, + drop_points=False, + remove_total_bounds=remove_total_bounds, + threshold_bounds=threshold_bounds, + ) # Resetting the index if reset_index: gdf = gdf.reset_index() # Dropping level_0 column - if reset_index and drop_level0 and 'level_0' in gdf: - gdf = gdf.drop(columns='level_0', - axis=1) + if reset_index and drop_level0 and "level_0" in gdf: + gdf = gdf.drop(columns="level_0", axis=1) # Dropping level_1 column - if reset_index and drop_level1 and 'level_1' in gdf: - gdf = gdf.drop(columns='level_1', - axis=1) + if reset_index and drop_level1 and "level_1" in gdf: + gdf = gdf.drop(columns="level_1", axis=1) # Dropping id column - if 'id' in gdf and drop_id: - gdf = gdf.drop(columns='id', - axis=1) + if "id" in gdf and drop_id: + gdf = gdf.drop(columns="id", axis=1) # Dropping index column - if 'index' in gdf and drop_index: - gdf = gdf.drop(columns='index', - axis=1) + if "index" in gdf and drop_index: + gdf = gdf.drop(columns="index", axis=1) # Dropping points column - if 'points' in gdf and drop_points: - gdf = gdf.drop(columns='points', axis=1) + if "points" in gdf and drop_points: + gdf = gdf.drop(columns="points", axis=1) # Limiting the extent of the data if bbox is not None: - gdf = gdf[(gdf.X > bbox[0]) & (gdf.X < bbox[1]) & (gdf.Y > bbox[2]) & (gdf.Y < bbox[3])] + gdf = gdf[ + (gdf.X > bbox[0]) + & (gdf.X < bbox[1]) + & (gdf.Y > bbox[2]) + & (gdf.Y < bbox[3]) + ] # Limiting the data to specified elevations if minz is not None: - gdf = gdf[gdf['Z'] >= minz] + gdf = gdf[gdf["Z"] >= minz] if maxz is not None: - gdf = gdf[gdf['Z'] <= maxz] + gdf = gdf[gdf["Z"] <= maxz] # Checking and setting the dtypes of the GeoDataFrame gdf = set_dtype(gdf=gdf) @@ -2091,7 +2174,10 @@ def extract_xyz(gdf: gpd.geodataframe.GeoDataFrame, # Exploding Geometries ############################################################### -def explode_linestring(linestring: shapely.geometry.linestring.LineString) -> List[shapely.geometry.point.Point]: + +def explode_linestring( + linestring: shapely.geometry.linestring.LineString, +) -> List[shapely.geometry.point.Point]: """Exploding a LineString to its vertices, also works for LineStrings with Z components Parameters @@ -2147,15 +2233,15 @@ def explode_linestring(linestring: shapely.geometry.linestring.LineString) -> Li # Checking that the input geometry is a Shapely LineString if not isinstance(linestring, shapely.geometry.linestring.LineString): - raise TypeError('Input geometry must be a Shapely LineString') + raise TypeError("Input geometry must be a Shapely LineString") # Checking that the LineString is valid if not linestring.is_valid: - raise ValueError('LineString is not a valid object') + raise ValueError("LineString is not a valid object") # Checking that the LineString is not empty if linestring.is_empty: - raise ValueError('LineString is an empty object') + raise ValueError("LineString is an empty object") # Extracting Points of LineString points_list = [geometry.Point(i) for i in list(linestring.coords)] @@ -2163,8 +2249,9 @@ def explode_linestring(linestring: shapely.geometry.linestring.LineString) -> Li return points_list -def explode_linestring_to_elements(linestring: shapely.geometry.linestring.LineString) -> \ - List[shapely.geometry.linestring.LineString]: +def explode_linestring_to_elements( + linestring: shapely.geometry.linestring.LineString, +) -> List[shapely.geometry.linestring.LineString]: """Separating a LineString into its single elements and returning a list of LineStrings representing these elements, also works for LineStrings with Z components @@ -2216,29 +2303,34 @@ def explode_linestring_to_elements(linestring: shapely.geometry.linestring.LineS # Checking that the LineString is a Shapely LineString if not isinstance(linestring, shapely.geometry.linestring.LineString): - raise TypeError('Input geometry must be a Shapely LineString') + raise TypeError("Input geometry must be a Shapely LineString") # Checking that the LineString is valid if not linestring.is_valid: - raise ValueError('LineString is not a valid object') + raise ValueError("LineString is not a valid object") # Checking that the LineString is not empty if linestring.is_empty: - raise ValueError('LineString is an empty object') + raise ValueError("LineString is an empty object") # Checking that the LineString only consists of two vertices if len(linestring.coords) < 2: - raise ValueError('LineString must contain at least two vertices') + raise ValueError("LineString must contain at least two vertices") # Splitting the LineString into single elements and returning a list of LineStrings splitted_linestrings = list( - map(shapely.geometry.linestring.LineString, zip(linestring.coords[:-1], linestring.coords[1:]))) + map( + shapely.geometry.linestring.LineString, + zip(linestring.coords[:-1], linestring.coords[1:]), + ) + ) return splitted_linestrings -def explode_multilinestring(multilinestring: shapely.geometry.multilinestring.MultiLineString) \ - -> List[shapely.geometry.linestring.LineString]: +def explode_multilinestring( + multilinestring: shapely.geometry.multilinestring.MultiLineString, +) -> List[shapely.geometry.linestring.LineString]: """Exploding a MultiLineString into a list of LineStrings Parameters @@ -2289,20 +2381,22 @@ def explode_multilinestring(multilinestring: shapely.geometry.multilinestring.Mu """ # Checking that the MultiLineString is a Shapely MultiLineString - if not isinstance(multilinestring, shapely.geometry.multilinestring.MultiLineString): - raise TypeError('MultiLineString must be a Shapely MultiLineString') + if not isinstance( + multilinestring, shapely.geometry.multilinestring.MultiLineString + ): + raise TypeError("MultiLineString must be a Shapely MultiLineString") # Checking that the MultiLineString is valid if not multilinestring.is_valid: - raise ValueError('MultiLineString is not a valid object') + raise ValueError("MultiLineString is not a valid object") # Checking that the MultiLineString is not empty if multilinestring.is_empty: - raise ValueError('MultiLineString is an empty object') + raise ValueError("MultiLineString is an empty object") # Checking that there is at least one LineString in the MultiLineString if len(list(multilinestring.geoms)) < 1: - raise ValueError('MultiLineString must at least contain one LineString') + raise ValueError("MultiLineString must at least contain one LineString") # Creating a list of single LineStrings from MultiLineString splitted_multilinestring = list(multilinestring.geoms) @@ -2310,11 +2404,12 @@ def explode_multilinestring(multilinestring: shapely.geometry.multilinestring.Mu return splitted_multilinestring -def explode_multilinestrings(gdf: gpd.geodataframe.GeoDataFrame, - reset_index: bool = True, - drop_level0: bool = True, - drop_level1: bool = True, - ) -> gpd.geodataframe.GeoDataFrame: +def explode_multilinestrings( + gdf: gpd.geodataframe.GeoDataFrame, + reset_index: bool = True, + drop_level0: bool = True, + drop_level1: bool = True, +) -> gpd.geodataframe.GeoDataFrame: """Exploding Shapely MultiLineStrings stored in a GeoDataFrame to Shapely LineStrings Parameters @@ -2373,31 +2468,33 @@ def explode_multilinestrings(gdf: gpd.geodataframe.GeoDataFrame, # Checking that gdf is of type GepDataFrame if not isinstance(gdf, gpd.geodataframe.GeoDataFrame): - raise TypeError('Loaded object is not a GeoDataFrame') + raise TypeError("Loaded object is not a GeoDataFrame") # Check that all entries of the gdf are of type MultiLineString or LineString - if not all(gdf.geom_type.isin(['MultiLineString', 'LineString'])): - raise TypeError('All GeoDataFrame entries must be of geom_type MultiLineString or LineString') + if not all(gdf.geom_type.isin(["MultiLineString", "LineString"])): + raise TypeError( + "All GeoDataFrame entries must be of geom_type MultiLineString or LineString" + ) # Checking that drop_level0 is of type bool if not isinstance(drop_level0, bool): - raise TypeError('Drop_index_level0 argument must be of type bool') + raise TypeError("Drop_index_level0 argument must be of type bool") # Checking that drop_level1 is of type bool if not isinstance(drop_level1, bool): - raise TypeError('Drop_index_level1 argument must be of type bool') + raise TypeError("Drop_index_level1 argument must be of type bool") # Checking that reset_index is of type bool if not isinstance(reset_index, bool): - raise TypeError('Reset_index argument must be of type bool') + raise TypeError("Reset_index argument must be of type bool") # Checking that all Shapely Objects are valid if not all(shapely.is_valid(gdf.geometry)): - raise ValueError('Not all Shapely Objects are valid objects') + raise ValueError("Not all Shapely Objects are valid objects") # Checking that no empty Shapely Objects are present if any(shapely.is_empty(gdf.geometry)): - raise ValueError('One or more Shapely objects are empty') + raise ValueError("One or more Shapely objects are empty") # Exploding MultiLineStrings gdf = gdf.explode(index_parts=True) @@ -2408,18 +2505,18 @@ def explode_multilinestrings(gdf: gpd.geodataframe.GeoDataFrame, # Dropping level_0 column if reset_index and drop_level0: - gdf = gdf.drop(columns='level_0', - axis=1) + gdf = gdf.drop(columns="level_0", axis=1) # Dropping level_1 column if reset_index and drop_level1: - gdf = gdf.drop(columns='level_1', - axis=1) + gdf = gdf.drop(columns="level_1", axis=1) return gdf -def explode_polygon(polygon: shapely.geometry.polygon.Polygon) -> List[shapely.geometry.point.Point]: +def explode_polygon( + polygon: shapely.geometry.polygon.Polygon, +) -> List[shapely.geometry.point.Point]: """Exploding Shapely Polygon to list of Points Parameters @@ -2471,22 +2568,24 @@ def explode_polygon(polygon: shapely.geometry.polygon.Polygon) -> List[shapely.g # Checking that the input polygon is a Shapely object if not isinstance(polygon, shapely.geometry.polygon.Polygon): - raise TypeError('Polygon must be a Shapely Polygon') + raise TypeError("Polygon must be a Shapely Polygon") # Checking that all Shapely Objects are valid if not polygon.is_valid: - raise ValueError('Not all Shapely Objects are valid objects') + raise ValueError("Not all Shapely Objects are valid objects") # Checking that no empty Shapely Objects are present if polygon.is_empty: - raise ValueError('One or more Shapely objects are empty') + raise ValueError("One or more Shapely objects are empty") points_list = [geometry.Point(point) for point in list(polygon.exterior.coords)] return points_list -def explode_polygons(gdf: gpd.geodataframe.GeoDataFrame) -> gpd.geodataframe.GeoDataFrame: +def explode_polygons( + gdf: gpd.geodataframe.GeoDataFrame, +) -> gpd.geodataframe.GeoDataFrame: """Converting a GeoDataFrame containing elements of geom_type Polygons to a GeoDataFrame with LineStrings Parameters @@ -2532,31 +2631,31 @@ def explode_polygons(gdf: gpd.geodataframe.GeoDataFrame) -> gpd.geodataframe.Geo # Checking that the input is a GeoDataFrame: if not isinstance(gdf, gpd.geodataframe.GeoDataFrame): - raise TypeError('gdf must be a GeoDataFrame') + raise TypeError("gdf must be a GeoDataFrame") # Checking that the geometry types of the GeoDataFrame are the supported types - if not gdf.geom_type.isin(('Polygon', 'MultiPolygon')).all(): - raise TypeError('Geometry type within GeoDataFrame not supported') + if not gdf.geom_type.isin(("Polygon", "MultiPolygon")).all(): + raise TypeError("Geometry type within GeoDataFrame not supported") # Checking that all Shapely Objects are valid if not all(shapely.is_valid(gdf.geometry)): - raise ValueError('Not all Shapely Objects are valid objects') + raise ValueError("Not all Shapely Objects are valid objects") # Checking that no empty Shapely Objects are present if any(shapely.is_empty(gdf.geometry)): - raise ValueError('One or more Shapely objects are empty') + raise ValueError("One or more Shapely objects are empty") # Creating GeoDataFrame containing only LineStrings and appending remaining columns as Pandas DataFrame - gdf_linestrings = gpd.GeoDataFrame(data=gdf.drop(columns='geometry', - axis=1), - geometry=gdf.boundary, - crs=gdf.crs) + gdf_linestrings = gpd.GeoDataFrame( + data=gdf.drop(columns="geometry", axis=1), geometry=gdf.boundary, crs=gdf.crs + ) return gdf_linestrings -def explode_geometry_collection(collection: shapely.geometry.collection.GeometryCollection) \ - -> List[shapely.geometry.base.BaseGeometry]: +def explode_geometry_collection( + collection: shapely.geometry.collection.GeometryCollection, +) -> List[shapely.geometry.base.BaseGeometry]: """Exploding a Shapely Geometry Collection to a List of Base Geometries Parameters @@ -2608,15 +2707,15 @@ def explode_geometry_collection(collection: shapely.geometry.collection.Geometry # Checking that the Geometry Collection is a Shapely Geometry Collection if not isinstance(collection, shapely.geometry.collection.GeometryCollection): - raise TypeError('Geometry Collection must be a Shapely Geometry Collection') + raise TypeError("Geometry Collection must be a Shapely Geometry Collection") # Checking that the Geometry Collection is valid if not collection.is_valid: - raise ValueError('Geometry Collection is not a valid object') + raise ValueError("Geometry Collection is not a valid object") # Checking that the Geometry Collection is not empty if collection.is_empty: - raise ValueError('Geometry Collection is an empty object') + raise ValueError("Geometry Collection is an empty object") # Creating list of Base Geometries collection_exploded = list(collection.geoms) @@ -2624,12 +2723,13 @@ def explode_geometry_collection(collection: shapely.geometry.collection.Geometry return collection_exploded -def explode_geometry_collections(gdf: gpd.geodataframe.GeoDataFrame, - reset_index: bool = True, - drop_level0: bool = True, - drop_level1: bool = True, - remove_points: bool = True, - ) -> gpd.geodataframe.GeoDataFrame: +def explode_geometry_collections( + gdf: gpd.geodataframe.GeoDataFrame, + reset_index: bool = True, + drop_level0: bool = True, + drop_level1: bool = True, + remove_points: bool = True, +) -> gpd.geodataframe.GeoDataFrame: """Exploding Shapely Geometry Collections stored in GeoDataFrames to different Shapely Base Geometries Parameters @@ -2701,38 +2801,38 @@ def explode_geometry_collections(gdf: gpd.geodataframe.GeoDataFrame, # Checking that gdf is of type GepDataFrame if not isinstance(gdf, gpd.geodataframe.GeoDataFrame): - raise TypeError('Loaded object is not a GeoDataFrame') + raise TypeError("Loaded object is not a GeoDataFrame") # Check that all entries of the gdf are of type MultiLineString or LineString if not any(gdf.geom_type == "GeometryCollection"): - raise TypeError('At least one geometry entry must be GeometryCollection') + raise TypeError("At least one geometry entry must be GeometryCollection") # Checking that drop_level0 is of type bool if not isinstance(drop_level0, bool): - raise TypeError('Drop_index_level0 argument must be of type bool') + raise TypeError("Drop_index_level0 argument must be of type bool") # Checking that drop_level1 is of type bool if not isinstance(drop_level1, bool): - raise TypeError('Drop_index_level1 argument must be of type bool') + raise TypeError("Drop_index_level1 argument must be of type bool") # Checking that reset_index is of type bool if not isinstance(reset_index, bool): - raise TypeError('Reset_index argument must be of type bool') + raise TypeError("Reset_index argument must be of type bool") # Checking that all Shapely Objects are valid if not all(shapely.is_valid(gdf.geometry)): - raise ValueError('Not all Shapely Objects are valid objects') + raise ValueError("Not all Shapely Objects are valid objects") # Checking that no empty Shapely Objects are present if any(shapely.is_empty(gdf.geometry)): - raise ValueError('One or more Shapely objects are empty') + raise ValueError("One or more Shapely objects are empty") # Exploding MultiLineStrings gdf = gdf.explode(index_parts=True) # Remove Point geometries if remove_points: - gdf = gdf[np.invert(gdf.geom_type == 'Point')] + gdf = gdf[np.invert(gdf.geom_type == "Point")] # Resetting the index if reset_index: @@ -2740,13 +2840,11 @@ def explode_geometry_collections(gdf: gpd.geodataframe.GeoDataFrame, # Dropping level_0 column if reset_index and drop_level0: - gdf = gdf.drop(columns='level_0', - axis=1) + gdf = gdf.drop(columns="level_0", axis=1) # Dropping level_1 column if reset_index and drop_level1: - gdf = gdf.drop(columns='level_1', - axis=1) + gdf = gdf.drop(columns="level_1", axis=1) return gdf @@ -2754,12 +2852,15 @@ def explode_geometry_collections(gdf: gpd.geodataframe.GeoDataFrame, # Creating LineStrings with Z components from points #################################################### -def create_linestring_from_xyz_points(points: Union[np.ndarray, gpd.geodataframe.GeoDataFrame], - nodata: Union[int, float] = 9999.0, - xcol: str = 'X', - ycol: str = 'Y', - zcol: str = 'Z', - drop_nan: bool = True) -> shapely.geometry.linestring.LineString: + +def create_linestring_from_xyz_points( + points: Union[np.ndarray, gpd.geodataframe.GeoDataFrame], + nodata: Union[int, float] = 9999.0, + xcol: str = "X", + ycol: str = "Y", + zcol: str = "Z", + drop_nan: bool = True, +) -> shapely.geometry.linestring.LineString: """ Create LineString from an array or GeoDataFrame containing X, Y, and Z coordinates of points. @@ -2805,26 +2906,28 @@ def create_linestring_from_xyz_points(points: Union[np.ndarray, gpd.geodataframe """ # Checking that the points are of type GeoDataFrame or a NumPy array if not isinstance(points, (np.ndarray, gpd.geodataframe.GeoDataFrame)): - raise TypeError('Input points must either be provided as GeoDataFrame or NumPy array') + raise TypeError( + "Input points must either be provided as GeoDataFrame or NumPy array" + ) # Checking of geometry objects are valid and converting GeoDataFrame to array if isinstance(points, gpd.geodataframe.GeoDataFrame): # Checking that all Shapely Objects are valid if not all(shapely.is_valid(points.geometry)): - raise ValueError('Not all Shapely Objects are valid objects') + raise ValueError("Not all Shapely Objects are valid objects") # Checking that no empty Shapely Objects are present if any(shapely.is_empty(points.geometry)): - raise ValueError('One or more Shapely objects are empty') + raise ValueError("One or more Shapely objects are empty") # Checking that all geometry objects are of type point if not all(shapely.get_type_id(points.geometry) == 0): - raise TypeError('All geometry objects must be of geom type Point') + raise TypeError("All geometry objects must be of geom type Point") # Checking that the Z column are present in GeoDataFrame if zcol not in points: - raise ValueError('Z values could not be found') + raise ValueError("Z values could not be found") # Extract X and Y coordinates from GeoDataFrame if not {xcol, ycol}.issubset(points.columns): @@ -2839,7 +2942,7 @@ def create_linestring_from_xyz_points(points: Union[np.ndarray, gpd.geodataframe # Checking that the NumPy array has the right dimensions if points.shape[1] != 3: - raise ValueError('Array must contain 3 values, X, Y, and Z values') + raise ValueError("Array must contain 3 values, X, Y, and Z values") # Getting indices where nodata values are present indices_nodata = np.where(points == nodata)[0] @@ -2856,17 +2959,18 @@ def create_linestring_from_xyz_points(points: Union[np.ndarray, gpd.geodataframe return linestring -def create_linestrings_from_xyz_points(gdf: gpd.geodataframe.GeoDataFrame, - groupby: str, - nodata: Union[int, float] = 9999.0, - xcol: str = 'X', - ycol: str = 'Y', - zcol: str = 'Z', - dem: Union[np.ndarray, rasterio.io.DatasetReader] = None, - extent: List[Union[float, int]] = None, - return_gdf: bool = True, - drop_nan: bool = True) -> Union[List[shapely.geometry.linestring.LineString], - gpd.geodataframe.GeoDataFrame]: +def create_linestrings_from_xyz_points( + gdf: gpd.geodataframe.GeoDataFrame, + groupby: str, + nodata: Union[int, float] = 9999.0, + xcol: str = "X", + ycol: str = "Y", + zcol: str = "Z", + dem: Union[np.ndarray, rasterio.io.DatasetReader] = None, + extent: List[Union[float, int]] = None, + return_gdf: bool = True, + drop_nan: bool = True, +) -> Union[List[shapely.geometry.linestring.LineString], gpd.geodataframe.GeoDataFrame]: """Creating LineStrings from a GeoDataFrame containing X, Y, and Z coordinates of vertices of multiple LineStrings Parameters @@ -2937,47 +3041,52 @@ def create_linestrings_from_xyz_points(gdf: gpd.geodataframe.GeoDataFrame, # Checking that the input is a GeoDataFrame if not isinstance(gdf, gpd.geodataframe.GeoDataFrame): - raise TypeError('Input must be provided as GeoDataFrame') + raise TypeError("Input must be provided as GeoDataFrame") # Checking that the geometry types of the GeoDataFrame are the supported types - if not gdf.geom_type.isin(('LineString', 'Point')).all(): - raise TypeError('Geometry type within GeoDataFrame not supported, only Point or LineString allowed') + if not gdf.geom_type.isin(("LineString", "Point")).all(): + raise TypeError( + "Geometry type within GeoDataFrame not supported, only Point or LineString allowed" + ) # Checking that all Shapely Objects are valid if not all(shapely.is_valid(gdf.geometry)): - raise ValueError('Not all Shapely Objects are valid objects') + raise ValueError("Not all Shapely Objects are valid objects") # Checking that no empty Shapely Objects are present if any(shapely.is_empty(gdf.geometry)): - raise ValueError('One or more Shapely objects are empty') + raise ValueError("One or more Shapely objects are empty") # Checking that return gdfs is of type bool if not isinstance(return_gdf, bool): - raise TypeError('Return_gdf argument must be of type bool') + raise TypeError("Return_gdf argument must be of type bool") # Checking that the GeoDataFrame contains Z values if zcol not in gdf: # Checking that the provided DEM is not of type None if not isinstance(dem, (np.ndarray, rasterio.io.DatasetReader)): - raise TypeError('Provide DEM as array or rasterio object to extract coordinates') + raise TypeError( + "Provide DEM as array or rasterio object to extract coordinates" + ) # Extracting Z values from dem - gdf = extract_xyz(gdf=gdf, - dem=dem, - extent=extent) + gdf = extract_xyz(gdf=gdf, dem=dem, extent=extent) # Checking if X and Y are in GeoDataFrame - if not {'X', 'Y'}.issubset(gdf.columns): - gdf = extract_xy(gdf=gdf, - reset_index=True) + if not {"X", "Y"}.issubset(gdf.columns): + gdf = extract_xy(gdf=gdf, reset_index=True) # Creating list of GeoDataFrames for the creating of LineStrings - list_gdfs = [gdf.groupby(by=groupby).get_group(group) for group in gdf[groupby].unique()] + list_gdfs = [ + gdf.groupby(by=groupby).get_group(group) for group in gdf[groupby].unique() + ] # Creating LineString for each GeoDataFrame in list_gdfs - list_linestrings = [create_linestring_from_xyz_points(points=geodf, - drop_nan=drop_nan) for geodf in list_gdfs] + list_linestrings = [ + create_linestring_from_xyz_points(points=geodf, drop_nan=drop_nan) + for geodf in list_gdfs + ] # Creating boolean list of empty geometries bool_empty_lines = [i.is_empty for i in list_linestrings] @@ -2986,25 +3095,36 @@ def create_linestrings_from_xyz_points(gdf: gpd.geodataframe.GeoDataFrame, indices_empty_lines = np.where(bool_empty_lines)[0].tolist() # Removing emtpy LineStrings from list of LineStrings by index - list_linestrings_new = [i for j, i in enumerate(list_linestrings) if j not in indices_empty_lines] + list_linestrings_new = [ + i for j, i in enumerate(list_linestrings) if j not in indices_empty_lines + ] # Removing GeoDataFrames at the indices of empty LineStrings list_gdfs_new = [i for j, i in enumerate(list_gdfs) if j not in indices_empty_lines] # Returning list of LineStrings as GeoDataFrame if return_gdf: - list_lines = [gpd.GeoDataFrame( - data=pd.DataFrame(data=list_gdfs_new[i].tail(1).drop(['geometry', xcol, ycol, zcol], axis=1)), - geometry=[list_linestrings_new[i]]) for i in range(len(list_linestrings_new))] + list_lines = [ + gpd.GeoDataFrame( + data=pd.DataFrame( + data=list_gdfs_new[i] + .tail(1) + .drop(["geometry", xcol, ycol, zcol], axis=1) + ), + geometry=[list_linestrings_new[i]], + ) + for i in range(len(list_linestrings_new)) + ] list_linestrings = pd.concat(list_lines).reset_index(drop=True) return list_linestrings -def create_linestrings_from_contours(contours: pv.core.pointset.PolyData, - return_gdf: bool = True, - crs: Union[str, pyproj.crs.crs.CRS] = None) \ - -> Union[List[shapely.geometry.linestring.LineString], gpd.geodataframe.GeoDataFrame]: +def create_linestrings_from_contours( + contours: pv.core.pointset.PolyData, + return_gdf: bool = True, + crs: Union[str, pyproj.crs.crs.CRS] = None, +) -> Union[List[shapely.geometry.linestring.LineString], gpd.geodataframe.GeoDataFrame]: """Creating LineStrings from PyVista Contour Lines and save them as list or GeoDataFrame Parameters @@ -3062,23 +3182,25 @@ def create_linestrings_from_contours(contours: pv.core.pointset.PolyData, # Checking that the input data is a PyVista PolyData dataset if not isinstance(contours, pv.core.pointset.PolyData): - raise TypeError('Input data must be a PyVista PolyData dataset') + raise TypeError("Input data must be a PyVista PolyData dataset") # Checking that the PolyData dataset does not contain any faces if contours.faces.size != 0: - raise TypeError('PolyData must not contain faces, only line, use mesh.contour() to extract contours') + raise TypeError( + "PolyData must not contain faces, only line, use mesh.contour() to extract contours" + ) # Checking that the PolyData dataset does contain lines if contours.lines.size == 0: - raise ValueError('Contours must contain lines') + raise ValueError("Contours must contain lines") # Checking that return gdfs is of type bool if not isinstance(return_gdf, bool): - raise TypeError('Return_gdf argument must be of type bool') + raise TypeError("Return_gdf argument must be of type bool") # Checking that the target_crs is of type string if not isinstance(crs, (str, type(None), pyproj.crs.crs.CRS)): - raise TypeError('target_crs must be of type string or a pyproj object') + raise TypeError("target_crs must be of type string or a pyproj object") # Defining empty list for LineStrings linestrings = [] @@ -3096,7 +3218,10 @@ def create_linestrings_from_contours(contours: pv.core.pointset.PolyData, number_of_points_of_line = contours.lines[index_to_find_length_of_line] # Getting the index values to look up points in contours.points - index_values = [contours.lines[index_to_find_length_of_line + i + 1] for i in range(number_of_points_of_line)] + index_values = [ + contours.lines[index_to_find_length_of_line + i + 1] + for i in range(number_of_points_of_line) + ] # Creating list of vertices belonging to one LineString vertices = [contours.points[value] for value in index_values] @@ -3113,7 +3238,10 @@ def create_linestrings_from_contours(contours: pv.core.pointset.PolyData, linestrings = gpd.GeoDataFrame(geometry=linestrings, crs=crs) # Adding a Z column containing the altitude of the LineString for better plotting - linestrings['Z'] = [list(linestrings.loc[i].geometry.coords)[0][2] for i in range(len(linestrings))] + linestrings["Z"] = [ + list(linestrings.loc[i].geometry.coords)[0][2] + for i in range(len(linestrings)) + ] return linestrings @@ -3122,14 +3250,16 @@ def create_linestrings_from_contours(contours: pv.core.pointset.PolyData, ################################################# -def interpolate_raster(gdf: gpd.geodataframe.GeoDataFrame, - value: str = 'Z', - method: str = 'nearest', - n: int = None, - res: int = 1, - extent: List[Union[float, int]] = None, - seed: int = None, - **kwargs) -> np.ndarray: +def interpolate_raster( + gdf: gpd.geodataframe.GeoDataFrame, + value: str = "Z", + method: str = "nearest", + n: int = None, + res: int = 1, + extent: List[Union[float, int]] = None, + seed: int = None, + **kwargs, +) -> np.ndarray: """Interpolating a raster/digital elevation model from point or line Shape file Parameters @@ -3198,37 +3328,41 @@ def interpolate_raster(gdf: gpd.geodataframe.GeoDataFrame, try: from scipy.interpolate import griddata, Rbf except ModuleNotFoundError: - raise ModuleNotFoundError('SciPy package is not installed. Use pip install scipy to install the latest version') + raise ModuleNotFoundError( + "SciPy package is not installed. Use pip install scipy to install the latest version" + ) # Checking if the gdf is of type GeoDataFrame if not isinstance(gdf, gpd.geodataframe.GeoDataFrame): - raise TypeError('gdf mus be of type GeoDataFrame') + raise TypeError("gdf mus be of type GeoDataFrame") # Checking that interpolation value is provided as string if not isinstance(value, str): - raise TypeError('Interpolation value must be provided as column name/string') + raise TypeError("Interpolation value must be provided as column name/string") # Checking if interpolation values are in the gdf if value not in gdf: - raise ValueError('Interpolation values not defined') + raise ValueError("Interpolation values not defined") # Checking that all Shapely Objects are valid if not all(shapely.is_valid(gdf.geometry)): - raise ValueError('Not all Shapely Objects are valid objects') + raise ValueError("Not all Shapely Objects are valid objects") # Checking that no empty Shapely Objects are present if any(shapely.is_empty(gdf.geometry)): - raise ValueError('One or more Shapely objects are empty') + raise ValueError("One or more Shapely objects are empty") # Checking if XY values are in the gdf - if not {'X', 'Y'}.issubset(gdf.columns): - gdf = extract_xy(gdf=gdf, - reset_index=True, - drop_index=False, - drop_level1=False, - drop_level0=False, - drop_id=False, - drop_points=True) + if not {"X", "Y"}.issubset(gdf.columns): + gdf = extract_xy( + gdf=gdf, + reset_index=True, + drop_index=False, + drop_level1=False, + drop_level0=False, + drop_id=False, + drop_points=True, + ) # Getting sample number n if n is None: @@ -3236,11 +3370,11 @@ def interpolate_raster(gdf: gpd.geodataframe.GeoDataFrame, # Checking that number of samples is of type int if not isinstance(n, int): - raise TypeError('Number of samples must be of type int') + raise TypeError("Number of samples must be of type int") # Checking that seed is of type int if not isinstance(seed, (int, type(None))): - raise TypeError('Seed must be of type int') + raise TypeError("Seed must be of type int") # Sampling gdf if n: @@ -3248,19 +3382,21 @@ def interpolate_raster(gdf: gpd.geodataframe.GeoDataFrame, if n <= len(gdf): gdf = gdf.sample(n=n) else: - raise ValueError('n must be smaller than the total number of points in the provided GeoDataFrame') + raise ValueError( + "n must be smaller than the total number of points in the provided GeoDataFrame" + ) # Checking that the method provided is of type string if not isinstance(method, str): - raise TypeError('Method must be of type string') + raise TypeError("Method must be of type string") # Checking that the resolution provided is of type int if not isinstance(res, int): - raise TypeError('Resolution must be of type int') + raise TypeError("Resolution must be of type int") # Checking that the extent provided is of type list or None if not isinstance(extent, (list, type(None))): - raise TypeError('Extent must be provided as list of corner values') + raise TypeError("Extent must be provided as list of corner values") # Creating a meshgrid based on the gdf bounds or a provided extent if extent: @@ -3276,27 +3412,32 @@ def interpolate_raster(gdf: gpd.geodataframe.GeoDataFrame, try: # Interpolating the raster if method in ["nearest", "linear", "cubic"]: - array = griddata((gdf['X'], gdf['Y']), gdf[value], (xx, yy), method=method, **kwargs) - elif method == 'rbf': - rbf = Rbf(gdf['X'], gdf['Y'], gdf[value], **kwargs) + array = griddata( + (gdf["X"], gdf["Y"]), gdf[value], (xx, yy), method=method, **kwargs + ) + elif method == "rbf": + rbf = Rbf(gdf["X"], gdf["Y"], gdf[value], **kwargs) array = rbf(xx, yy) else: - raise ValueError('No valid method defined') + raise ValueError("No valid method defined") except np.linalg.LinAlgError: - raise ValueError('LinAlgError: reduce the number of points by setting a value for n or check for duplicates') + raise ValueError( + "LinAlgError: reduce the number of points by setting a value for n or check for duplicates" + ) return array -def clip_by_bbox(gdf: gpd.geodataframe.GeoDataFrame, - bbox: List[Union[float, int]], - reset_index: bool = True, - drop_index: bool = True, - drop_id: bool = True, - drop_points: bool = True, - drop_level0: bool = True, - drop_level1: bool = True - ) -> gpd.geodataframe.GeoDataFrame: +def clip_by_bbox( + gdf: gpd.geodataframe.GeoDataFrame, + bbox: List[Union[float, int]], + reset_index: bool = True, + drop_index: bool = True, + drop_id: bool = True, + drop_points: bool = True, + drop_level0: bool = True, + drop_level1: bool = True, +) -> gpd.geodataframe.GeoDataFrame: """Clipping vector data contained in a GeoDataFrame to a provided bounding box/extent Parameters @@ -3385,125 +3526,131 @@ def clip_by_bbox(gdf: gpd.geodataframe.GeoDataFrame, # Checking that the input data is of type GeoDataFrame if not isinstance(gdf, gpd.geodataframe.GeoDataFrame): - raise TypeError('Loaded object is not a GeoDataFrame') + raise TypeError("Loaded object is not a GeoDataFrame") # Checking that the bounding box is a list if not isinstance(bbox, list): - raise TypeError('Bounding box must be of type list') + raise TypeError("Bounding box must be of type list") # Checking that all values are either ints or floats if not all(isinstance(n, (int, float)) for n in bbox): - raise TypeError('All bounding values must be of type int or float') + raise TypeError("All bounding values must be of type int or float") # Checking that the geometry types of the GeoDataFrame are the supported types - if not gdf.geom_type.isin(('MultiLineString', 'LineString', 'Point', 'Polygon')).all(): - raise TypeError('Geometry type within GeoDataFrame not supported') + if not gdf.geom_type.isin( + ("MultiLineString", "LineString", "Point", "Polygon") + ).all(): + raise TypeError("Geometry type within GeoDataFrame not supported") # Checking that drop_level0 is of type bool if not isinstance(drop_level0, bool): - raise TypeError('Drop_index_level0 argument must be of type bool') + raise TypeError("Drop_index_level0 argument must be of type bool") # Checking that drop_level1 is of type bool if not isinstance(drop_level1, bool): - raise TypeError('Drop_index_level1 argument must be of type bool') + raise TypeError("Drop_index_level1 argument must be of type bool") # Checking that reset_index is of type bool if not isinstance(reset_index, bool): - raise TypeError('Reset_index argument must be of type bool') + raise TypeError("Reset_index argument must be of type bool") # Checking that drop_id is of type bool if not isinstance(drop_id, bool): - raise TypeError('Drop_id argument must be of type bool') + raise TypeError("Drop_id argument must be of type bool") # Checking that drop_points is of type bool if not isinstance(drop_points, bool): - raise TypeError('Drop_points argument must be of type bool') + raise TypeError("Drop_points argument must be of type bool") # Checking that drop_index is of type bool if not isinstance(drop_index, bool): - raise TypeError('Drop_index argument must be of type bool') + raise TypeError("Drop_index argument must be of type bool") # Checking that the length of the list is either four or six if not len(bbox) == 4 or len(bbox) == 6: - raise ValueError('The bbox must include only four or six values') + raise ValueError("The bbox must include only four or six values") # Checking that all elements of the extent are of type int or float if not all(isinstance(n, (int, float)) for n in bbox): - raise TypeError('Extent values must be of type int or float') + raise TypeError("Extent values must be of type int or float") # Checking that all Shapely Objects are valid if not all(shapely.is_valid(gdf.geometry)): - raise ValueError('Not all Shapely Objects are valid objects') + raise ValueError("Not all Shapely Objects are valid objects") # Checking that no empty Shapely Objects are present if any(shapely.is_empty(gdf.geometry)): - raise ValueError('One or more Shapely objects are empty') + raise ValueError("One or more Shapely objects are empty") # Selecting x and y bounds if bbox contains values for all three directions x, y, z if len(bbox) == 6: bbox = bbox[:4] # If X and Y are not in the GeoDataFrame, extract them - if not {'X', 'Y'}.issubset(gdf.columns): - gdf = extract_xy(gdf=gdf, - reset_index=False, - drop_index=False, - drop_id=False, - drop_points=False, - drop_level0=False, - drop_level1=False, - overwrite_xy=False, - target_crs=None, - bbox=None) + if not {"X", "Y"}.issubset(gdf.columns): + gdf = extract_xy( + gdf=gdf, + reset_index=False, + drop_index=False, + drop_id=False, + drop_points=False, + drop_level0=False, + drop_level1=False, + overwrite_xy=False, + target_crs=None, + bbox=None, + ) # Clipping the data - gdf = gpd.clip(gdf=gdf, - mask=geometry.Polygon([(bbox[0], bbox[2]), - (bbox[1], bbox[2]), - (bbox[1], bbox[3]), - (bbox[0], bbox[3])])) + gdf = gpd.clip( + gdf=gdf, + mask=geometry.Polygon( + [ + (bbox[0], bbox[2]), + (bbox[1], bbox[2]), + (bbox[1], bbox[3]), + (bbox[0], bbox[3]), + ] + ), + ) # Resetting the index if reset_index: gdf = gdf.reset_index() # Dropping level_0 column - if reset_index and drop_level0 and 'level_0' in gdf: - gdf = gdf.drop(columns='level_0', - axis=1) + if reset_index and drop_level0 and "level_0" in gdf: + gdf = gdf.drop(columns="level_0", axis=1) # Dropping level_1 column - if reset_index and drop_level1 and 'level_1' in gdf: - gdf = gdf.drop(columns='level_1', - axis=1) + if reset_index and drop_level1 and "level_1" in gdf: + gdf = gdf.drop(columns="level_1", axis=1) # Dropping id column - if 'id' in gdf and drop_id: - gdf = gdf.drop(columns='id', - axis=1) + if "id" in gdf and drop_id: + gdf = gdf.drop(columns="id", axis=1) # Dropping index column - if 'index' in gdf and drop_index: - gdf = gdf.drop(columns='index', - axis=1) + if "index" in gdf and drop_index: + gdf = gdf.drop(columns="index", axis=1) # Dropping points column - if 'points' in gdf and drop_points: - gdf = gdf.drop(columns='points', - axis=1) + if "points" in gdf and drop_points: + gdf = gdf.drop(columns="points", axis=1) return gdf -def clip_by_polygon(gdf: gpd.geodataframe.GeoDataFrame, - polygon: shapely.geometry.polygon.Polygon, - reset_index: bool = True, - drop_index: bool = True, - drop_id: bool = True, - drop_points: bool = True, - drop_level0: bool = True, - drop_level1: bool = True - ) -> gpd.geodataframe.GeoDataFrame: +def clip_by_polygon( + gdf: gpd.geodataframe.GeoDataFrame, + polygon: shapely.geometry.polygon.Polygon, + reset_index: bool = True, + drop_index: bool = True, + drop_id: bool = True, + drop_points: bool = True, + drop_level0: bool = True, + drop_level1: bool = True, +) -> gpd.geodataframe.GeoDataFrame: """Clipping vector data contained in a GeoDataFrame to a provided bounding box/extent Parameters @@ -3596,55 +3743,49 @@ def clip_by_polygon(gdf: gpd.geodataframe.GeoDataFrame, # Checking if the gdf is of type GeoDataFrame if not isinstance(gdf, gpd.geodataframe.GeoDataFrame): - raise TypeError('gdf must be of type GeoDataFrame') + raise TypeError("gdf must be of type GeoDataFrame") # Checking if the polygon is of type GeoDataFrame if not isinstance(polygon, shapely.geometry.polygon.Polygon): - raise TypeError('Polygon must be of Shapely Polygon') + raise TypeError("Polygon must be of Shapely Polygon") # Checking that all Shapely Objects are valid if not all(shapely.is_valid(gdf.geometry)): - raise ValueError('Not all Shapely Objects are valid objects') + raise ValueError("Not all Shapely Objects are valid objects") # Checking that no empty Shapely Objects are present if any(shapely.is_empty(gdf.geometry)): - raise ValueError('One or more Shapely objects are empty') + raise ValueError("One or more Shapely objects are empty") # Create deep copy of gdf gdf = gdf.copy(deep=True) # Clipping the gdf - gdf = gpd.clip(gdf=gdf, - mask=polygon) + gdf = gpd.clip(gdf=gdf, mask=polygon) # Resetting the index if reset_index: gdf = gdf.reset_index() # Dropping level_0 column - if reset_index and drop_level0 and 'level_0' in gdf: - gdf = gdf.drop(columns='level_0', - axis=1) + if reset_index and drop_level0 and "level_0" in gdf: + gdf = gdf.drop(columns="level_0", axis=1) # Dropping level_1 column - if reset_index and drop_level1 and 'level_1' in gdf: - gdf = gdf.drop(columns='level_1', - axis=1) + if reset_index and drop_level1 and "level_1" in gdf: + gdf = gdf.drop(columns="level_1", axis=1) # Dropping id column - if 'id' in gdf and drop_id: - gdf = gdf.drop(columns='id', - axis=1) + if "id" in gdf and drop_id: + gdf = gdf.drop(columns="id", axis=1) # Dropping index column - if 'index' in gdf and drop_index: - gdf = gdf.drop(columns='index', - axis=1) + if "index" in gdf and drop_index: + gdf = gdf.drop(columns="index", axis=1) # Dropping points column - if 'points' in gdf and drop_points: - gdf = gdf.drop(columns='points', - axis=1) + if "points" in gdf and drop_points: + gdf = gdf.drop(columns="points", axis=1) return gdf @@ -3653,9 +3794,9 @@ def clip_by_polygon(gdf: gpd.geodataframe.GeoDataFrame, ###################################### -def create_buffer(geom_object: shapely.geometry.base.BaseGeometry, - distance: Union[float, - int]) -> shapely.geometry.polygon.Polygon: +def create_buffer( + geom_object: shapely.geometry.base.BaseGeometry, distance: Union[float, int] +) -> shapely.geometry.polygon.Polygon: """Creating a buffer around a Shapely LineString or a Point Parameters @@ -3700,11 +3841,13 @@ def create_buffer(geom_object: shapely.geometry.base.BaseGeometry, # Checking that the geometry object is a Shapely LineString or Point if not isinstance(geom_object, shapely.geometry.base.BaseGeometry): - raise TypeError('Geometry object must either be a Shapely LineString or Point object') + raise TypeError( + "Geometry object must either be a Shapely LineString or Point object" + ) # Checking that the distance is of type float or int if not isinstance(distance, (float, int)): - raise TypeError('Radius must be of type float or int') + raise TypeError("Radius must be of type float or int") # Creating buffer around object polygon = geom_object.buffer(distance=distance) @@ -3712,10 +3855,12 @@ def create_buffer(geom_object: shapely.geometry.base.BaseGeometry, return polygon -def create_unified_buffer(geom_object: Union[gpd.geodataframe.GeoDataFrame, - List[shapely.geometry.base.BaseGeometry]], - distance: Union[np.ndarray, List[Union[float, int]], Union[float, int]]) \ - -> shapely.geometry.multipolygon.MultiPolygon: +def create_unified_buffer( + geom_object: Union[ + gpd.geodataframe.GeoDataFrame, List[shapely.geometry.base.BaseGeometry] + ], + distance: Union[np.ndarray, List[Union[float, int]], Union[float, int]], +) -> shapely.geometry.multipolygon.MultiPolygon: """Creating a unified buffer around Shapely LineStrings or Points Parameters @@ -3767,31 +3912,37 @@ def create_unified_buffer(geom_object: Union[gpd.geodataframe.GeoDataFrame, """ # Checking that the geometry object is a Shapely LineString or Point - if not isinstance(geom_object, (gpd.geodataframe.GeoDataFrame, - list, - shapely.geometry.base.BaseGeometry)): - raise TypeError('Geometry object must either be a Shapely LineString or Point object') + if not isinstance( + geom_object, + (gpd.geodataframe.GeoDataFrame, list, shapely.geometry.base.BaseGeometry), + ): + raise TypeError( + "Geometry object must either be a Shapely LineString or Point object" + ) # Checking that the distance is of type float or int if not isinstance(distance, (float, int)): - raise TypeError('Radius must be of type float or int') + raise TypeError("Radius must be of type float or int") # Converting GeoDataFrame into list of Shapely objects if isinstance(geom_object, gpd.geodataframe.GeoDataFrame): # Checking that all Shapely Objects are valid if not all(shapely.is_valid(geom_object.geometry)): - raise ValueError('Not all Shapely Objects are valid objects') + raise ValueError("Not all Shapely Objects are valid objects") # Checking that no empty Shapely Objects are present if any(shapely.is_empty(geom_object.geometry)): - raise ValueError('One or more Shapely objects are empty') + raise ValueError("One or more Shapely objects are empty") # Converting geometry column of GeoDataFrame to list geom_object = geom_object.geometry.tolist() # Creating list of polygons - polygon_list = [create_buffer(geom_object=geomobject, distance=distance) for geomobject in geom_object] + polygon_list = [ + create_buffer(geom_object=geomobject, distance=distance) + for geomobject in geom_object + ] # Creating unified polygons unified_polygons = ops.unary_union(polygon_list) @@ -3799,9 +3950,10 @@ def create_unified_buffer(geom_object: Union[gpd.geodataframe.GeoDataFrame, return unified_polygons -def subtract_geom_objects(geom_object1: shapely.geometry.base.BaseGeometry, - geom_object2: shapely.geometry.base.BaseGeometry) \ - -> shapely.geometry.base.BaseGeometry: +def subtract_geom_objects( + geom_object1: shapely.geometry.base.BaseGeometry, + geom_object2: shapely.geometry.base.BaseGeometry, +) -> shapely.geometry.base.BaseGeometry: """Subtracting Shapely geometry objects from each other and returning the left over object Parameters @@ -3847,11 +3999,15 @@ def subtract_geom_objects(geom_object1: shapely.geometry.base.BaseGeometry, # Checking that the first geometry object is a Shapely Point, LineString or Polygon if not isinstance(geom_object1, shapely.geometry.base.BaseGeometry): - raise TypeError('First geometry object must be a Shapely Point, LineString or Polygon') + raise TypeError( + "First geometry object must be a Shapely Point, LineString or Polygon" + ) # Checking that the second geometry object is a Shapely Point, LineString or Polygon if not isinstance(geom_object2, shapely.geometry.base.BaseGeometry): - raise TypeError('Second geometry object must be a Shapely Point, LineString or Polygon') + raise TypeError( + "Second geometry object must be a Shapely Point, LineString or Polygon" + ) # Subtracting object 2 from object 1 result = geom_object1 - geom_object2 @@ -3859,13 +4015,12 @@ def subtract_geom_objects(geom_object1: shapely.geometry.base.BaseGeometry, return result -def remove_object_within_buffer(buffer_object: shapely.geometry.base.BaseGeometry, - buffered_object: shapely.geometry.base.BaseGeometry, - distance: Union[int, - float] = None, - buffer: bool = True) \ - -> Tuple[shapely.geometry.base.BaseGeometry, - shapely.geometry.base.BaseGeometry]: +def remove_object_within_buffer( + buffer_object: shapely.geometry.base.BaseGeometry, + buffered_object: shapely.geometry.base.BaseGeometry, + distance: Union[int, float] = None, + buffer: bool = True, +) -> Tuple[shapely.geometry.base.BaseGeometry, shapely.geometry.base.BaseGeometry]: """Removing object from a buffered object by providing a distance Parameters @@ -3932,35 +4087,35 @@ def remove_object_within_buffer(buffer_object: shapely.geometry.base.BaseGeometr # Checking that the buffer object is a Shapely Point or LineString if not isinstance(buffer_object, shapely.geometry.base.BaseGeometry): - raise TypeError('Buffer object must be a Shapely Point or LineString') + raise TypeError("Buffer object must be a Shapely Point or LineString") # Checking that the buffered object is a Shapely Point or LineString if not isinstance(buffered_object, shapely.geometry.base.BaseGeometry): - raise TypeError('Buffered object must be a Shapely Point or LineString') + raise TypeError("Buffered object must be a Shapely Point or LineString") # Checking that the buffer_object is valid if not buffer_object.is_valid: - raise ValueError('Buffer object is not a valid object') + raise ValueError("Buffer object is not a valid object") # Checking that the buffer_object is not empty if buffer_object.is_empty: - raise ValueError('Buffer object is an empty object') + raise ValueError("Buffer object is an empty object") # Checking that the buffered_object is valid if not buffered_object.is_valid: - raise ValueError('Buffered Object is not a valid object') + raise ValueError("Buffered Object is not a valid object") # Checking that the buffered_object is not empty if buffered_object.is_empty: - raise ValueError('Buffered Object is an empty object') + raise ValueError("Buffered Object is an empty object") # Checking that the distance is of type float or int if not isinstance(distance, (float, int, type(None))): - raise TypeError('Radius must be of type float or int') + raise TypeError("Radius must be of type float or int") # Checking that create_buffer is of type bool if not isinstance(buffer, bool): - raise TypeError('create_buffer must be of type bool') + raise TypeError("create_buffer must be of type bool") # Create buffer object if buffer and distance is not None: @@ -3975,17 +4130,20 @@ def remove_object_within_buffer(buffer_object: shapely.geometry.base.BaseGeometr return result_out, result_in -def remove_objects_within_buffer(buffer_object: shapely.geometry.base.BaseGeometry, - buffered_objects_gdf: Union[gpd.geodataframe.GeoDataFrame, - List[shapely.geometry.base.BaseGeometry]], - distance: Union[int, - float] = None, - return_gdfs: bool = False, - remove_empty_geometries: bool = False, - extract_coordinates: bool = False, - buffer: bool = True) \ - -> Tuple[Union[List[shapely.geometry.base.BaseGeometry], gpd.geodataframe.GeoDataFrame], - Union[List[shapely.geometry.base.BaseGeometry], gpd.geodataframe.GeoDataFrame]]: +def remove_objects_within_buffer( + buffer_object: shapely.geometry.base.BaseGeometry, + buffered_objects_gdf: Union[ + gpd.geodataframe.GeoDataFrame, List[shapely.geometry.base.BaseGeometry] + ], + distance: Union[int, float] = None, + return_gdfs: bool = False, + remove_empty_geometries: bool = False, + extract_coordinates: bool = False, + buffer: bool = True, +) -> Tuple[ + Union[List[shapely.geometry.base.BaseGeometry], gpd.geodataframe.GeoDataFrame], + Union[List[shapely.geometry.base.BaseGeometry], gpd.geodataframe.GeoDataFrame], +]: """Removing objects from a buffered object by providing a distance Parameters @@ -4074,54 +4232,55 @@ def remove_objects_within_buffer(buffer_object: shapely.geometry.base.BaseGeomet # Checking that the buffer object is a Shapely Point or LineString if not isinstance(buffer_object, shapely.geometry.base.BaseGeometry): - raise TypeError('Buffer object must be a Shapely Point or LineString') + raise TypeError("Buffer object must be a Shapely Point or LineString") # Checking that the buffer_object is valid if not buffer_object.is_valid: - raise ValueError('Buffer object is not a valid object') + raise ValueError("Buffer object is not a valid object") # Checking that the buffer_object is not empty if buffer_object.is_empty: - raise ValueError('Buffer object is an empty object') + raise ValueError("Buffer object is an empty object") # Checking that the buffered objects are provided within a GeoDataFrame if not isinstance(buffered_objects_gdf, (gpd.geodataframe.GeoDataFrame, list)): - raise TypeError('Buffered objects must be stored as GeoSeries within a GeoDataFrame or as element in a list') + raise TypeError( + "Buffered objects must be stored as GeoSeries within a GeoDataFrame or as element in a list" + ) # Checking that the distance is of type float or int if not isinstance(distance, (float, int, type(None))): - raise TypeError('Radius must be of type float or int') + raise TypeError("Radius must be of type float or int") # Checking that return gdfs is of type bool if not isinstance(return_gdfs, bool): - raise TypeError('Return_gdf argument must be of type bool') + raise TypeError("Return_gdf argument must be of type bool") # Checking that remove empty geometries is of type bool if not isinstance(remove_empty_geometries, bool): - raise TypeError('Remove_emtpy_geometries argument must be of type bool') + raise TypeError("Remove_emtpy_geometries argument must be of type bool") # Checking that extract coordinates is of type bool if not isinstance(extract_coordinates, bool): - raise TypeError('Extract_coordinates argument must be of type bool') + raise TypeError("Extract_coordinates argument must be of type bool") # Checking that create_buffer is of type bool if not isinstance(buffer, bool): - raise TypeError('create_buffer must be of type bool') + raise TypeError("create_buffer must be of type bool") # Creating buffer if buffer and distance is not None: - buffer_object = create_buffer(geom_object=buffer_object, - distance=distance) + buffer_object = create_buffer(geom_object=buffer_object, distance=distance) # Converting the GeoDataFrame to a list if isinstance(buffered_objects_gdf, gpd.geodataframe.GeoDataFrame): # Checking that all Shapely Objects are valid if not all(shapely.is_valid(buffered_objects_gdf.geometry)): - raise ValueError('Not all Shapely Objects are valid objects') + raise ValueError("Not all Shapely Objects are valid objects") # Checking that no empty Shapely Objects are present if any(shapely.is_empty(buffered_objects_gdf.geometry)): - raise ValueError('One or more Shapely objects are empty') + raise ValueError("One or more Shapely objects are empty") # Converting geometry column of the GeoDataFrame to a list buffered_objects_list = buffered_objects_gdf.geometry.tolist() @@ -4132,10 +4291,12 @@ def remove_objects_within_buffer(buffer_object: shapely.geometry.base.BaseGeomet buffered_objects_list = None # Creating tuples of buffered and non-buffered Shapely objects - results = [remove_object_within_buffer(buffer_object=buffer_object, - buffered_object=i, - distance=None, - buffer=False) for i in buffered_objects_list] + results = [ + remove_object_within_buffer( + buffer_object=buffer_object, buffered_object=i, distance=None, buffer=False + ) + for i in buffered_objects_list + ] # Creating lists of remaining and buffered geometry objects results_out = [results[i][0] for i in range(len(results))] @@ -4143,12 +4304,16 @@ def remove_objects_within_buffer(buffer_object: shapely.geometry.base.BaseGeomet # If return gdfs is true, create GeoDataFrames from list if return_gdfs: - results_out = gpd.GeoDataFrame(data=buffered_objects_gdf.drop('geometry', axis=1), - geometry=results_out, - crs=buffered_objects_gdf.crs) - results_in = gpd.GeoDataFrame(data=buffered_objects_gdf.drop('geometry', axis=1), - geometry=results_in, - crs=buffered_objects_gdf.crs) + results_out = gpd.GeoDataFrame( + data=buffered_objects_gdf.drop("geometry", axis=1), + geometry=results_out, + crs=buffered_objects_gdf.crs, + ) + results_in = gpd.GeoDataFrame( + data=buffered_objects_gdf.drop("geometry", axis=1), + geometry=results_in, + crs=buffered_objects_gdf.crs, + ) # Remove empty geometries if remove_empty_geometries: @@ -4165,13 +4330,13 @@ def remove_objects_within_buffer(buffer_object: shapely.geometry.base.BaseGeomet return results_out, results_in -def remove_interfaces_within_fault_buffers(fault_gdf: gpd.geodataframe.GeoDataFrame, - interfaces_gdf: gpd.geodataframe.GeoDataFrame, - distance: Union[int, - float] = None, - remove_empty_geometries: bool = True, - extract_coordinates: bool = True) \ - -> Tuple[gpd.geodataframe.GeoDataFrame, gpd.geodataframe.GeoDataFrame]: +def remove_interfaces_within_fault_buffers( + fault_gdf: gpd.geodataframe.GeoDataFrame, + interfaces_gdf: gpd.geodataframe.GeoDataFrame, + distance: Union[int, float] = None, + remove_empty_geometries: bool = True, + extract_coordinates: bool = True, +) -> Tuple[gpd.geodataframe.GeoDataFrame, gpd.geodataframe.GeoDataFrame]: """Function to create a buffer around a GeoDataFrame containing fault data and removing interface points that are located within this buffer @@ -4269,56 +4434,60 @@ def remove_interfaces_within_fault_buffers(fault_gdf: gpd.geodataframe.GeoDataFr # Checking that the buffer object is a Shapely Point or LineString if not isinstance(fault_gdf, gpd.geodataframe.GeoDataFrame): - raise TypeError('Buffer object must be a Shapely Point or LineString') + raise TypeError("Buffer object must be a Shapely Point or LineString") # Checking that the buffered objects are provided within a GeoDataFrame if not isinstance(interfaces_gdf, gpd.geodataframe.GeoDataFrame): - raise TypeError('Buffered objects must be stored as GeoSeries within a GeoDataFrame') + raise TypeError( + "Buffered objects must be stored as GeoSeries within a GeoDataFrame" + ) # Checking that the distance is of type float or int if not isinstance(distance, (float, int)): - raise TypeError('Radius must be of type float or int') + raise TypeError("Radius must be of type float or int") # Checking that remove empty geometries is of type bool if not isinstance(remove_empty_geometries, bool): - raise TypeError('Remove_emtpy_geometries argument must be of type bool') + raise TypeError("Remove_emtpy_geometries argument must be of type bool") # Checking that extract coordinates is of type bool if not isinstance(extract_coordinates, bool): - raise TypeError('Extract_coordinates argument must be of type bool') + raise TypeError("Extract_coordinates argument must be of type bool") # Checking that all Shapely Objects are valid if not all(shapely.is_valid(fault_gdf.geometry)): - raise ValueError('Not all Shapely Objects are valid objects') + raise ValueError("Not all Shapely Objects are valid objects") # Checking that no empty Shapely Objects are present if any(shapely.is_empty(fault_gdf.geometry)): - raise ValueError('One or more Shapely objects are empty') + raise ValueError("One or more Shapely objects are empty") # Checking that all Shapely Objects are valid if not all(shapely.is_valid(interfaces_gdf.geometry)): - raise ValueError('Not all Shapely Objects are valid objects') + raise ValueError("Not all Shapely Objects are valid objects") # Checking that no empty Shapely Objects are present if any(shapely.is_empty(interfaces_gdf.geometry)): - raise ValueError('One or more Shapely objects are empty') + raise ValueError("One or more Shapely objects are empty") # Creating list of fault lines faults_list = fault_gdf.geometry.tolist() # Exploding Polygons - if all(interfaces_gdf.geom_type == 'Polygon'): + if all(interfaces_gdf.geom_type == "Polygon"): interfaces_gdf = explode_polygons(gdf=interfaces_gdf) # Creating unified polygons unified_polygons = ops.unary_union(geoms=faults_list) - gdf_out, gdf_in = remove_objects_within_buffer(buffer_object=unified_polygons, - buffered_objects_gdf=interfaces_gdf, - distance=distance, - return_gdfs=True, - remove_empty_geometries=remove_empty_geometries, - extract_coordinates=extract_coordinates) + gdf_out, gdf_in = remove_objects_within_buffer( + buffer_object=unified_polygons, + buffered_objects_gdf=interfaces_gdf, + distance=distance, + return_gdfs=True, + remove_empty_geometries=remove_empty_geometries, + extract_coordinates=extract_coordinates, + ) return gdf_out, gdf_in @@ -4329,6 +4498,7 @@ def remove_interfaces_within_fault_buffers(fault_gdf: gpd.geodataframe.GeoDataFr # Calculating Angles and Directions ################################### + def calculate_angle(linestring: shapely.geometry.linestring.LineString) -> float: """Calculating the angle of a LineString to the vertical @@ -4379,27 +4549,33 @@ def calculate_angle(linestring: shapely.geometry.linestring.LineString) -> float # Checking that the LineString is a Shapely LineString if not isinstance(linestring, shapely.geometry.linestring.LineString): - raise TypeError('Input geometry must be a Shapley LineString') + raise TypeError("Input geometry must be a Shapley LineString") # Checking that the LineString only consists of two vertices if len(linestring.coords) != 2: - raise ValueError('LineString must only contain a start and end point') + raise ValueError("LineString must only contain a start and end point") # Checking that the LineString is valid if not linestring.is_valid: - raise ValueError('LineString is not a valid object') + raise ValueError("LineString is not a valid object") # Checking that the LineString is not empty if linestring.is_empty: - raise ValueError('LineString is an empty object') + raise ValueError("LineString is an empty object") # Calculating angle - angle = np.rad2deg(np.arccos((linestring.coords[0][1] - linestring.coords[1][1]) / linestring.length)) + angle = np.rad2deg( + np.arccos( + (linestring.coords[0][1] - linestring.coords[1][1]) / linestring.length + ) + ) return angle -def calculate_strike_direction_straight_linestring(linestring: shapely.geometry.linestring.LineString) -> float: +def calculate_strike_direction_straight_linestring( + linestring: shapely.geometry.linestring.LineString, +) -> float: """Function to calculate the strike direction of a straight Shapely LineString. The strike will always be calculated from start to end point @@ -4450,28 +4626,37 @@ def calculate_strike_direction_straight_linestring(linestring: shapely.geometry. # Checking that the LineString is a Shapely LineString if not isinstance(linestring, shapely.geometry.linestring.LineString): - raise TypeError('Input geometry must be a Shapley LineString') + raise TypeError("Input geometry must be a Shapley LineString") # Checking that the LineString only consists of two vertices if len(linestring.coords) != 2: - raise ValueError('LineString must only contain a start and end point') + raise ValueError("LineString must only contain a start and end point") # Checking that the LineString is valid if not linestring.is_valid: - raise ValueError('LineString is not a valid object') + raise ValueError("LineString is not a valid object") # Checking that the LineString is not empty if linestring.is_empty: - raise ValueError('LineString is an empty object') + raise ValueError("LineString is an empty object") # Calculating strike angle based on order and location of line vertices - if linestring.coords[0][0] < linestring.coords[1][0] and linestring.coords[0][1] >= linestring.coords[1][1]: + if ( + linestring.coords[0][0] < linestring.coords[1][0] + and linestring.coords[0][1] >= linestring.coords[1][1] + ): angle = 180 - calculate_angle(linestring=linestring) - elif linestring.coords[0][0] > linestring.coords[1][0] and linestring.coords[0][1] < linestring.coords[1][1]: + elif ( + linestring.coords[0][0] > linestring.coords[1][0] + and linestring.coords[0][1] < linestring.coords[1][1] + ): angle = 180 + calculate_angle(linestring=linestring) - elif linestring.coords[0][0] < linestring.coords[1][0] and linestring.coords[0][1] < linestring.coords[1][1]: + elif ( + linestring.coords[0][0] < linestring.coords[1][0] + and linestring.coords[0][1] < linestring.coords[1][1] + ): angle = 180 - calculate_angle(linestring=linestring) else: angle = 180 + calculate_angle(linestring=linestring) @@ -4483,7 +4668,9 @@ def calculate_strike_direction_straight_linestring(linestring: shapely.geometry. return angle -def calculate_strike_direction_bent_linestring(linestring: shapely.geometry.linestring.LineString) -> List[float]: +def calculate_strike_direction_bent_linestring( + linestring: shapely.geometry.linestring.LineString, +) -> List[float]: """Calculating the strike direction of a LineString with multiple elements Parameters @@ -4528,31 +4715,35 @@ def calculate_strike_direction_bent_linestring(linestring: shapely.geometry.line # Checking that the LineString is a Shapely LineString if not isinstance(linestring, shapely.geometry.linestring.LineString): - raise TypeError('Input geometry must be a Shapley LineString') + raise TypeError("Input geometry must be a Shapley LineString") # Checking that the LineString only consists of two vertices if len(linestring.coords) < 2: - raise ValueError('LineString must contain at least two vertices') + raise ValueError("LineString must contain at least two vertices") # Checking that the LineString is valid if not linestring.is_valid: - raise ValueError('LineString is not a valid object') + raise ValueError("LineString is not a valid object") # Checking that the LineString is not empty if linestring.is_empty: - raise ValueError('LineString is an empty object') + raise ValueError("LineString is an empty object") # Split LineString into list of single LineStrings with two vertices each splitted_linestrings = explode_linestring_to_elements(linestring=linestring) # Calculate strike angle for each single LineString element - angles_splitted_linestrings = [calculate_strike_direction_straight_linestring(linestring=i) for i in - splitted_linestrings] + angles_splitted_linestrings = [ + calculate_strike_direction_straight_linestring(linestring=i) + for i in splitted_linestrings + ] return angles_splitted_linestrings -def calculate_dipping_angle_linestring(linestring: shapely.geometry.linestring.LineString): +def calculate_dipping_angle_linestring( + linestring: shapely.geometry.linestring.LineString, +): """Calculating the dipping angle of a LineString digitized on a cross section Parameters @@ -4602,30 +4793,38 @@ def calculate_dipping_angle_linestring(linestring: shapely.geometry.linestring.L # Checking that the LineString is a Shapely LineString if not isinstance(linestring, shapely.geometry.linestring.LineString): - raise TypeError('Input geometry must be a Shapley LineString') + raise TypeError("Input geometry must be a Shapley LineString") # Checking that the LineString only consists of two vertices if len(linestring.coords) != 2: - raise ValueError('LineString must only contain a start and end point') + raise ValueError("LineString must only contain a start and end point") # Checking that the LineString is valid if not linestring.is_valid: - raise ValueError('LineString is not a valid object') + raise ValueError("LineString is not a valid object") # Checking that the LineString is not empty if linestring.is_empty: - raise ValueError('LineString is an empty object') + raise ValueError("LineString is an empty object") # Calculating the dip of LineString based on its slope - dip = np.abs(np.rad2deg(np.arctan((linestring.coords[1][1] - linestring.coords[0][1]) / - (linestring.coords[1][0] - linestring.coords[0][0])))) + dip = np.abs( + np.rad2deg( + np.arctan( + (linestring.coords[1][1] - linestring.coords[0][1]) + / (linestring.coords[1][0] - linestring.coords[0][0]) + ) + ) + ) return dip def calculate_dipping_angles_linestrings( - linestring_list: Union[gpd.geodataframe.GeoDataFrame, - List[shapely.geometry.linestring.LineString]]): + linestring_list: Union[ + gpd.geodataframe.GeoDataFrame, List[shapely.geometry.linestring.LineString] + ] +): """Calculating the dipping angles of LineStrings digitized on a cross section Parameters @@ -4677,30 +4876,34 @@ def calculate_dipping_angles_linestrings( # Checking that the list of LineStrings is either provided as list or within a GeoDataFrame if not isinstance(linestring_list, (list, gpd.geodataframe.GeoDataFrame)): - raise TypeError('LineStrings must be provided as list or within a GeoDataFrame') + raise TypeError("LineStrings must be provided as list or within a GeoDataFrame") # Convert LineStrings stored in GeoDataFrame to list if isinstance(linestring_list, gpd.geodataframe.GeoDataFrame): linestring_list = linestring_list.geometry.tolist() # Checking that all elements of the list are LineStrings - if not all(isinstance(n, shapely.geometry.linestring.LineString) for n in linestring_list): - raise TypeError('All list elements must be Shapely LineStrings') + if not all( + isinstance(n, shapely.geometry.linestring.LineString) for n in linestring_list + ): + raise TypeError("All list elements must be Shapely LineStrings") # Checking that all LineStrings only have two vertices if not all(len(n.coords) == 2 for n in linestring_list): - raise ValueError('All LineStrings must only have two vertices') + raise ValueError("All LineStrings must only have two vertices") # Checking that the LineString is valid if not all(n.is_valid for n in linestring_list): - raise ValueError('LineString is not a valid object') + raise ValueError("LineString is not a valid object") # Checking that the LineString is not empty if any(n.is_empty for n in linestring_list): - raise ValueError('LineString is an empty object') + raise ValueError("LineString is an empty object") # Calculating dipping angles - dipping_angles = [calculate_dipping_angle_linestring(linestring=i) for i in linestring_list] + dipping_angles = [ + calculate_dipping_angle_linestring(linestring=i) for i in linestring_list + ] return dipping_angles @@ -4708,9 +4911,11 @@ def calculate_dipping_angles_linestrings( # Calculating Coordinates for Vector Data from Cross Sections ############################################################ -def calculate_coordinates_for_point_on_cross_section(linestring: shapely.geometry.linestring.LineString, - point: Union[shapely.geometry.point.Point, - Tuple[float, float]]): + +def calculate_coordinates_for_point_on_cross_section( + linestring: shapely.geometry.linestring.LineString, + point: Union[shapely.geometry.point.Point, Tuple[float, float]], +): """Calculating the coordinates for one point digitized on a cross section provided as Shapely LineString Parameters @@ -4766,37 +4971,41 @@ def calculate_coordinates_for_point_on_cross_section(linestring: shapely.geometr # Checking that the LineString is a Shapely LineString if not isinstance(linestring, shapely.geometry.linestring.LineString): - raise TypeError('Input geometry must be a Shapley LineString') + raise TypeError("Input geometry must be a Shapley LineString") # Checking that the LineString is valid if not linestring.is_valid: - raise ValueError('LineString is not a valid object') + raise ValueError("LineString is not a valid object") # Checking that the LineString is not empty if linestring.is_empty: - raise ValueError('LineString is an empty object') + raise ValueError("LineString is an empty object") # Checking that the Point is a Shapely Point or a tuple if not isinstance(point, (shapely.geometry.point.Point, tuple)): - raise TypeError('Input geometry must be a Shapley Point or a tuple with X and Y coordinates') + raise TypeError( + "Input geometry must be a Shapley Point or a tuple with X and Y coordinates" + ) # Checking that all elements of the list are floats if isinstance(point, tuple) and not all(isinstance(n, float) for n in point): - raise TypeError('All tuple elements must be floats') + raise TypeError("All tuple elements must be floats") # Checking that the tuple only consists of two elements if isinstance(point, tuple) and len(point) != 2: - raise ValueError('The point tuple only takes X and Y coordinates') + raise ValueError("The point tuple only takes X and Y coordinates") # Converting the Shapely Point to a tuple if isinstance(point, shapely.geometry.point.Point): point = point.coords[0] # Creating Substrings from cross section LineString - substr = ops.substring(geom=linestring, - start_dist=point[0] / linestring.length, - end_dist=linestring.length, - normalized=True) + substr = ops.substring( + geom=linestring, + start_dist=point[0] / linestring.length, + end_dist=linestring.length, + normalized=True, + ) # Creating Shapely Point from Substring point = geometry.Point(substr.coords[0]) @@ -4804,8 +5013,10 @@ def calculate_coordinates_for_point_on_cross_section(linestring: shapely.geometr return point -def calculate_coordinates_for_linestring_on_cross_sections(linestring: shapely.geometry.linestring.LineString, - interfaces: shapely.geometry.linestring.LineString): +def calculate_coordinates_for_linestring_on_cross_sections( + linestring: shapely.geometry.linestring.LineString, + interfaces: shapely.geometry.linestring.LineString, +): """Calculating the coordinates of vertices for a LineString on a straight cross section provided as Shapely LineString @@ -4871,40 +5082,43 @@ def calculate_coordinates_for_linestring_on_cross_sections(linestring: shapely.g # Checking that the LineString is a Shapely LineString if not isinstance(linestring, shapely.geometry.linestring.LineString): - raise TypeError('Input geometry must be a Shapley LineString') + raise TypeError("Input geometry must be a Shapley LineString") # Checking that the LineString is a Shapely LineString if not isinstance(interfaces, shapely.geometry.linestring.LineString): - raise TypeError('Input interfaces must be a Shapley LineString') + raise TypeError("Input interfaces must be a Shapley LineString") # Checking that the LineString is valid if not linestring.is_valid: - raise ValueError('LineString is not a valid object') + raise ValueError("LineString is not a valid object") # Checking that the LineString is not empty if linestring.is_empty: - raise ValueError('LineString is an empty object') + raise ValueError("LineString is an empty object") # Checking that the LineString is valid if not interfaces.is_valid: - raise ValueError('LineString is not a valid object') + raise ValueError("LineString is not a valid object") # Checking that the LineString is not empty if interfaces.is_empty: - raise ValueError('LineString is an empty object') + raise ValueError("LineString is an empty object") # Calculating the real world coordinates of points digitized on a cross section - points = [calculate_coordinates_for_point_on_cross_section(linestring=linestring, - point=interfaces.coords[i]) for i in - range(len(interfaces.coords))] + points = [ + calculate_coordinates_for_point_on_cross_section( + linestring=linestring, point=interfaces.coords[i] + ) + for i in range(len(interfaces.coords)) + ] return points -def calculate_coordinates_for_linestrings_on_cross_sections(linestring: shapely.geometry.linestring.LineString, - linestring_interfaces_list: List[ - shapely.geometry.linestring.LineString]) -> \ - List[shapely.geometry.point.Point]: +def calculate_coordinates_for_linestrings_on_cross_sections( + linestring: shapely.geometry.linestring.LineString, + linestring_interfaces_list: List[shapely.geometry.linestring.LineString], +) -> List[shapely.geometry.point.Point]: """Calculating the coordinates of vertices for LineStrings on a straight cross section provided as Shapely LineString @@ -4982,28 +5196,34 @@ def calculate_coordinates_for_linestrings_on_cross_sections(linestring: shapely. # Checking that the LineString is a Shapely LineString if not isinstance(linestring, shapely.geometry.linestring.LineString): - raise TypeError('Input geometry must be a Shapley LineString') + raise TypeError("Input geometry must be a Shapley LineString") # Checking that the LineString is valid if not linestring.is_valid: - raise ValueError('LineString is not a valid object') + raise ValueError("LineString is not a valid object") # Checking that the LineString is not empty if linestring.is_empty: - raise ValueError('LineString is an empty object') + raise ValueError("LineString is an empty object") # Checking that the LineString is a Shapely LineString if not isinstance(linestring_interfaces_list, list): - raise TypeError('Input interfaces must be a list containing Shapley LineString') + raise TypeError("Input interfaces must be a list containing Shapley LineString") # Checking that all elements of the list are LineStrings - if not all(isinstance(n, shapely.geometry.linestring.LineString) for n in linestring_interfaces_list): - raise TypeError('All list elements must be Shapely LineStrings') + if not all( + isinstance(n, shapely.geometry.linestring.LineString) + for n in linestring_interfaces_list + ): + raise TypeError("All list elements must be Shapely LineStrings") # Calculating the coordinates for LineStrings on a cross section - points = [calculate_coordinates_for_linestring_on_cross_sections(linestring=linestring, - interfaces=i) for i in - linestring_interfaces_list] + points = [ + calculate_coordinates_for_linestring_on_cross_sections( + linestring=linestring, interfaces=i + ) + for i in linestring_interfaces_list + ] # Create list of points from list of lists points = [points[i][j] for i in range(len(points)) for j in range(len(points[i]))] @@ -5011,10 +5231,11 @@ def calculate_coordinates_for_linestrings_on_cross_sections(linestring: shapely. return points -def extract_interfaces_coordinates_from_cross_section(linestring: shapely.geometry.linestring.LineString, - interfaces_gdf: gpd.geodataframe.GeoDataFrame, - extract_coordinates: bool = True) \ - -> gpd.geodataframe.GeoDataFrame: +def extract_interfaces_coordinates_from_cross_section( + linestring: shapely.geometry.linestring.LineString, + interfaces_gdf: gpd.geodataframe.GeoDataFrame, + extract_coordinates: bool = True, +) -> gpd.geodataframe.GeoDataFrame: """Extracting coordinates of interfaces digitized on a cross section Parameters @@ -5082,75 +5303,88 @@ def extract_interfaces_coordinates_from_cross_section(linestring: shapely.geomet # Checking that the LineString is a Shapely LineString if not isinstance(linestring, shapely.geometry.linestring.LineString): - raise TypeError('Input geometry must be a Shapley LineString') + raise TypeError("Input geometry must be a Shapley LineString") # Checking that the LineString is valid if not linestring.is_valid: - raise ValueError('LineString is not a valid object') + raise ValueError("LineString is not a valid object") # Checking that the LineString is not empty if linestring.is_empty: - raise ValueError('LineString is an empty object') + raise ValueError("LineString is an empty object") # Checking that the interfaces_gdf is a GeoDataFrame if not isinstance(interfaces_gdf, gpd.geodataframe.GeoDataFrame): - raise TypeError('Interfaces must be stored as a GeoDataFrame') + raise TypeError("Interfaces must be stored as a GeoDataFrame") # Checking that all elements of the geometry column are LineStrings - if not all(isinstance(n, shapely.geometry.linestring.LineString) for n in interfaces_gdf.geometry.tolist()): - raise TypeError('All geometry elements must be Shapely LineStrings') + if not all( + isinstance(n, shapely.geometry.linestring.LineString) + for n in interfaces_gdf.geometry.tolist() + ): + raise TypeError("All geometry elements must be Shapely LineStrings") # Checking that all Shapely Objects are valid if not all(shapely.is_valid(interfaces_gdf.geometry)): - raise ValueError('Not all Shapely Objects are valid objects') + raise ValueError("Not all Shapely Objects are valid objects") # Checking that no empty Shapely Objects are present if any(shapely.is_empty(interfaces_gdf.geometry)): - raise ValueError('One or more Shapely objects are empty') + raise ValueError("One or more Shapely objects are empty") # Calculating coordinates for LineStrings on cross sections geom_objects = calculate_coordinates_for_linestrings_on_cross_sections( linestring=linestring, - linestring_interfaces_list=interfaces_gdf.geometry.tolist()) + linestring_interfaces_list=interfaces_gdf.geometry.tolist(), + ) # Resetting index of GeoDataFrame interfaces_gdf = interfaces_gdf.reset_index() # Creating column with lists of coordinates - interfaces_gdf['list_geoms'] = [list(interfaces_gdf.geometry[i].coords) for i in range(len(interfaces_gdf))] + interfaces_gdf["list_geoms"] = [ + list(interfaces_gdf.geometry[i].coords) for i in range(len(interfaces_gdf)) + ] # Creating DataFrame from interfaces_gdf without geometry column and explode column list_geoms - data_gdf = pd.DataFrame(interfaces_gdf.drop('geometry', axis=1)).explode('list_geoms') + data_gdf = pd.DataFrame(interfaces_gdf.drop("geometry", axis=1)).explode( + "list_geoms" + ) # Creating GeoDataFrame from data_gdf and geom_objects - gdf = gpd.GeoDataFrame(data=data_gdf, - geometry=geom_objects) + gdf = gpd.GeoDataFrame(data=data_gdf, geometry=geom_objects) # Extracting X and Y coordinates from Point objects if extract_coordinates: - gdf = extract_xy(gdf=gdf, - reset_index=True, - drop_index=True, - drop_id=True, - drop_points=True, - drop_level0=True, - drop_level1=True, - overwrite_xy=True, - ) + gdf = extract_xy( + gdf=gdf, + reset_index=True, + drop_index=True, + drop_id=True, + drop_points=True, + drop_level0=True, + drop_level1=True, + overwrite_xy=True, + ) # Creating Z column from - gdf['Z'] = [interfaces_gdf.geometry[i].coords[j][1] for i in range(len(interfaces_gdf)) for j in - range(len(list(interfaces_gdf.geometry[i].coords)))] + gdf["Z"] = [ + interfaces_gdf.geometry[i].coords[j][1] + for i in range(len(interfaces_gdf)) + for j in range(len(list(interfaces_gdf.geometry[i].coords))) + ] # Dropping the column with the geometry lists - gdf = gdf.drop('list_geoms', axis=1) + gdf = gdf.drop("list_geoms", axis=1) return gdf -def extract_xyz_from_cross_sections(profile_gdf: gpd.geodataframe.GeoDataFrame, - interfaces_gdf: gpd.geodataframe.GeoDataFrame, - profile_name_column: str = 'name') -> gpd.geodataframe.GeoDataFrame: +def extract_xyz_from_cross_sections( + profile_gdf: gpd.geodataframe.GeoDataFrame, + interfaces_gdf: gpd.geodataframe.GeoDataFrame, + profile_name_column: str = "name", +) -> gpd.geodataframe.GeoDataFrame: """Extracting X, Y, and Z coordinates from cross sections and digitized interfaces Parameters @@ -5229,60 +5463,80 @@ def extract_xyz_from_cross_sections(profile_gdf: gpd.geodataframe.GeoDataFrame, # Checking that the profile traces are provided as a GeoDataFrame if not isinstance(profile_gdf, gpd.geodataframe.GeoDataFrame): - raise TypeError('Input geometry must be a GeoDataFrame') + raise TypeError("Input geometry must be a GeoDataFrame") # Checking that the column profile name column is present in the GeoDataFrame if profile_name_column not in profile_gdf: - raise ValueError('Column with profile names not found, provide profile_name_column') + raise ValueError( + "Column with profile names not found, provide profile_name_column" + ) # Checking that the column profile name column is present in the GeoDataFrame if profile_name_column not in interfaces_gdf: - raise ValueError('Column with profile names not found, provide profile_name_column') + raise ValueError( + "Column with profile names not found, provide profile_name_column" + ) # Checking that the interfaces_gdf is a GeoDataFrame if not isinstance(interfaces_gdf, gpd.geodataframe.GeoDataFrame): - raise TypeError('Interfaces must be stored as a GeoDataFrame') + raise TypeError("Interfaces must be stored as a GeoDataFrame") # Checking that all elements of the geometry column are LineStrings - if not all(isinstance(n, shapely.geometry.linestring.LineString) for n in profile_gdf.geometry.tolist()): - raise TypeError('All geometry elements of the profile_gdf must be Shapely LineStrings') + if not all( + isinstance(n, shapely.geometry.linestring.LineString) + for n in profile_gdf.geometry.tolist() + ): + raise TypeError( + "All geometry elements of the profile_gdf must be Shapely LineStrings" + ) # Checking that all elements of the geometry column are LineStrings - if not all(isinstance(n, shapely.geometry.linestring.LineString) for n in interfaces_gdf.geometry.tolist()): - raise TypeError('All geometry elements of the interface_gdf must be Shapely LineStrings') + if not all( + isinstance(n, shapely.geometry.linestring.LineString) + for n in interfaces_gdf.geometry.tolist() + ): + raise TypeError( + "All geometry elements of the interface_gdf must be Shapely LineStrings" + ) # Checking that profile_name_column is in profile_gdf if profile_name_column not in profile_gdf: - raise ValueError('Profile Column not found, provide a valid name or add column') + raise ValueError("Profile Column not found, provide a valid name or add column") # Checking that the profile_name_column is in interfaces_gdf if profile_name_column not in interfaces_gdf: - raise ValueError('Profile Column not found, provide a valid name or add column') + raise ValueError("Profile Column not found, provide a valid name or add column") # Checking that the profile names are identical if not sorted(profile_gdf[profile_name_column].unique().tolist()) == sorted( - interfaces_gdf[profile_name_column].unique().tolist()): - raise ValueError('Profile names in DataFrames are not identical') + interfaces_gdf[profile_name_column].unique().tolist() + ): + raise ValueError("Profile names in DataFrames are not identical") # Creating a list of GeoDataFrames containing the X, Y, and Z coordinates of digitized interfaces - list_gdf = [extract_interfaces_coordinates_from_cross_section(profile_gdf.geometry[i], - interfaces_gdf[ - interfaces_gdf[profile_name_column] == - profile_gdf[profile_name_column][i]]) - for i in range(len(profile_gdf))] + list_gdf = [ + extract_interfaces_coordinates_from_cross_section( + profile_gdf.geometry[i], + interfaces_gdf[ + interfaces_gdf[profile_name_column] + == profile_gdf[profile_name_column][i] + ], + ) + for i in range(len(profile_gdf)) + ] # Concat list of GeoDataFrames to one large DataFrame df = pd.concat(list_gdf).reset_index(drop=True) # Creating GeoDataFrame - gdf = gpd.GeoDataFrame(data=df, - geometry=df['geometry'], - crs=interfaces_gdf.crs) + gdf = gpd.GeoDataFrame(data=df, geometry=df["geometry"], crs=interfaces_gdf.crs) return gdf -def calculate_midpoint_linestring(linestring: shapely.geometry.linestring.LineString) -> shapely.geometry.point.Point: +def calculate_midpoint_linestring( + linestring: shapely.geometry.linestring.LineString, +) -> shapely.geometry.point.Point: """Calculating the midpoint of a LineString with two vertices Parameters @@ -5329,25 +5583,24 @@ def calculate_midpoint_linestring(linestring: shapely.geometry.linestring.LineSt # Checking that the LineString is a Shapely LineString if not isinstance(linestring, shapely.geometry.linestring.LineString): - raise TypeError('Input geometry must be a Shapley LineString') + raise TypeError("Input geometry must be a Shapley LineString") # Checking that the LineString only consists of two vertices if len(linestring.coords) != 2: - raise ValueError('LineString must only contain a start and end point') + raise ValueError("LineString must only contain a start and end point") # Checking that the LineString is valid if not linestring.is_valid: - raise ValueError('LineString is not a valid object') + raise ValueError("LineString is not a valid object") # Checking that the LineString is not empty if linestring.is_empty: - raise ValueError('LineString is an empty object') + raise ValueError("LineString is an empty object") # Creating a substring at half the distance of the LineString - substr = ops.substring(geom=linestring, - start_dist=0.5, - end_dist=linestring.length, - normalized=True) + substr = ops.substring( + geom=linestring, start_dist=0.5, end_dist=linestring.length, normalized=True + ) # Extracting midpoint from substring point = geometry.Point(substr.coords[0]) @@ -5355,9 +5608,11 @@ def calculate_midpoint_linestring(linestring: shapely.geometry.linestring.LineSt return point -def calculate_midpoints_linestrings(linestring_gdf: Union[gpd.geodataframe.GeoDataFrame, - List[shapely.geometry.linestring.LineString]]) -> \ - List[shapely.geometry.point.Point]: +def calculate_midpoints_linestrings( + linestring_gdf: Union[ + gpd.geodataframe.GeoDataFrame, List[shapely.geometry.linestring.LineString] + ] +) -> List[shapely.geometry.point.Point]: """Calculating the midpoints of LineStrings with two vertices each Parameters @@ -5410,33 +5665,39 @@ def calculate_midpoints_linestrings(linestring_gdf: Union[gpd.geodataframe.GeoDa # Checking that the LineString is a Shapely LineString if not isinstance(linestring_gdf, (gpd.geodataframe.GeoDataFrame, list)): - raise TypeError('Input geometry must be a GeoDataFrame or a List containing LineStrings') + raise TypeError( + "Input geometry must be a GeoDataFrame or a List containing LineStrings" + ) # Converting LineStrings in GeoDataFrame to list of LineStrings if isinstance(linestring_gdf, gpd.geodataframe.GeoDataFrame): # Checking that all Shapely Objects are valid if not all(shapely.is_valid(linestring_gdf.geometry)): - raise ValueError('Not all Shapely Objects are valid objects') + raise ValueError("Not all Shapely Objects are valid objects") # Checking that no empty Shapely Objects are present if any(shapely.is_empty(linestring_gdf.geometry)): - raise ValueError('One or more Shapely objects are empty') + raise ValueError("One or more Shapely objects are empty") # Creating list from geometry column linestring_gdf = linestring_gdf.geometry.tolist() # Checking that all LineStrings are valid if not all(i.is_valid for i in linestring_gdf): - raise ValueError('Not all Shapely LineStrings are valid') + raise ValueError("Not all Shapely LineStrings are valid") # Checking that no LineStrings are empty if any(i.is_empty for i in linestring_gdf): - raise ValueError('One or more LineString Objects are empty') + raise ValueError("One or more LineString Objects are empty") # Checking that all elements of the geometry column are LineStrings - if not all(isinstance(n, shapely.geometry.linestring.LineString) for n in linestring_gdf): - raise TypeError('All geometry elements of the linestring_gdf must be Shapely LineStrings') + if not all( + isinstance(n, shapely.geometry.linestring.LineString) for n in linestring_gdf + ): + raise TypeError( + "All geometry elements of the linestring_gdf must be Shapely LineStrings" + ) # Calculating midpoints midpoints = [calculate_midpoint_linestring(linestring=i) for i in linestring_gdf] @@ -5448,8 +5709,10 @@ def calculate_midpoints_linestrings(linestring_gdf: Union[gpd.geodataframe.GeoDa ####################################################################### -def calculate_orientation_from_cross_section(profile_linestring: shapely.geometry.linestring.LineString, - orientation_linestring: shapely.geometry.linestring.LineString) -> list: +def calculate_orientation_from_cross_section( + profile_linestring: shapely.geometry.linestring.LineString, + orientation_linestring: shapely.geometry.linestring.LineString, +) -> list: """Calculating the orientation for one LineString on one cross sections Parameters @@ -5507,44 +5770,50 @@ def calculate_orientation_from_cross_section(profile_linestring: shapely.geometr # Checking that the LineString is a Shapely LineString if not isinstance(profile_linestring, shapely.geometry.linestring.LineString): - raise TypeError('Input geometry must be a Shapley LineString') + raise TypeError("Input geometry must be a Shapley LineString") # Checking that the LineString is a Shapely LineString if not isinstance(orientation_linestring, shapely.geometry.linestring.LineString): - raise TypeError('Input geometry must be a Shapley LineString') + raise TypeError("Input geometry must be a Shapley LineString") # Checking that the LineString is valid if not profile_linestring.is_valid: - raise ValueError('LineString is not a valid object') + raise ValueError("LineString is not a valid object") # Checking that the LineString is not empty if profile_linestring.is_empty: - raise ValueError('LineString is an empty object') + raise ValueError("LineString is an empty object") # Checking that the LineString is valid if not orientation_linestring.is_valid: - raise ValueError('LineString is not a valid object') + raise ValueError("LineString is not a valid object") # Checking that the LineString is not empty if orientation_linestring.is_empty: - raise ValueError('LineString is an empty object') + raise ValueError("LineString is an empty object") # Checking that the LineString only consists of two vertices if len(orientation_linestring.coords) != 2: - raise ValueError('LineString must only contain a start and end point') + raise ValueError("LineString must only contain a start and end point") # Checking that the X coordinates/ the distances to the origin are always positive if list(orientation_linestring.coords)[0][0] < 0: - raise ValueError('X coordinates must always be positive, check the orientation of your profile') + raise ValueError( + "X coordinates must always be positive, check the orientation of your profile" + ) if list(orientation_linestring.coords)[1][0] < 0: - raise ValueError('X coordinates must always be positive, check the orientation of your profile') + raise ValueError( + "X coordinates must always be positive, check the orientation of your profile" + ) # Calculating midpoint of orientation LineString midpoint = calculate_midpoint_linestring(orientation_linestring) # Calculating the coordinates for the midpoint on the cross section - coordinates = calculate_coordinates_for_point_on_cross_section(profile_linestring, midpoint) + coordinates = calculate_coordinates_for_point_on_cross_section( + profile_linestring, midpoint + ) # Calculating the dipping angle for the orientation LineString dip = calculate_dipping_angle_linestring(orientation_linestring) @@ -5553,16 +5822,22 @@ def calculate_orientation_from_cross_section(profile_linestring: shapely.geometr azimuth_profile = calculate_strike_direction_straight_linestring(profile_linestring) # Calculating the azimuth of the orientation based on the dip direction of the orientation - if orientation_linestring.coords[0][0] < orientation_linestring.coords[1][0] and \ - orientation_linestring.coords[0][1] > orientation_linestring.coords[1][1]: + if ( + orientation_linestring.coords[0][0] < orientation_linestring.coords[1][0] + and orientation_linestring.coords[0][1] > orientation_linestring.coords[1][1] + ): azimuth = azimuth_profile - elif orientation_linestring.coords[0][0] > orientation_linestring.coords[1][0] and \ - orientation_linestring.coords[0][1] < orientation_linestring.coords[1][1]: + elif ( + orientation_linestring.coords[0][0] > orientation_linestring.coords[1][0] + and orientation_linestring.coords[0][1] < orientation_linestring.coords[1][1] + ): azimuth = azimuth_profile - elif orientation_linestring.coords[0][0] < orientation_linestring.coords[1][0] and \ - orientation_linestring.coords[0][1] < orientation_linestring.coords[1][1]: + elif ( + orientation_linestring.coords[0][0] < orientation_linestring.coords[1][0] + and orientation_linestring.coords[0][1] < orientation_linestring.coords[1][1] + ): azimuth = 180 + azimuth_profile else: @@ -5581,9 +5856,10 @@ def calculate_orientation_from_cross_section(profile_linestring: shapely.geometr return orientation -def calculate_orientation_from_bent_cross_section(profile_linestring: shapely.geometry.linestring.LineString, - orientation_linestring: shapely.geometry.linestring.LineString) \ - -> list: +def calculate_orientation_from_bent_cross_section( + profile_linestring: shapely.geometry.linestring.LineString, + orientation_linestring: shapely.geometry.linestring.LineString, +) -> list: """Calculating the orientation of a LineString on a bent cross section provided as Shapely LineString Parameters @@ -5640,44 +5916,49 @@ def calculate_orientation_from_bent_cross_section(profile_linestring: shapely.ge # Checking that the LineString is a Shapely LineString if not isinstance(profile_linestring, shapely.geometry.linestring.LineString): - raise TypeError('Input geometry must be a Shapley LineString') + raise TypeError("Input geometry must be a Shapley LineString") # Checking that the LineString is a Shapely LineString if not isinstance(orientation_linestring, shapely.geometry.linestring.LineString): - raise TypeError('Input geometry must be a Shapley LineString') + raise TypeError("Input geometry must be a Shapley LineString") # Checking that the LineString is valid if not profile_linestring.is_valid: - raise ValueError('LineString is not a valid object') + raise ValueError("LineString is not a valid object") # Checking that the LineString is not empty if profile_linestring.is_empty: - raise ValueError('LineString is an empty object') + raise ValueError("LineString is an empty object") # Checking that the LineString is valid if not orientation_linestring.is_valid: - raise ValueError('LineString is not a valid object') + raise ValueError("LineString is not a valid object") # Checking that the LineString is not empty if orientation_linestring.is_empty: - raise ValueError('LineString is an empty object') + raise ValueError("LineString is an empty object") # Checking that the LineString only consists of two vertices if len(orientation_linestring.coords) != 2: - raise ValueError('LineString must only contain a start and end point') + raise ValueError("LineString must only contain a start and end point") # Checking that the X coordinates/ the distances to the origin are always positive if list(orientation_linestring.coords)[0][0] < 0: - raise ValueError('X coordinates must always be positive, check the orientation of your profile') + raise ValueError( + "X coordinates must always be positive, check the orientation of your profile" + ) if list(orientation_linestring.coords)[1][0] < 0: - raise ValueError('X coordinates must always be positive, check the orientation of your profile') + raise ValueError( + "X coordinates must always be positive, check the orientation of your profile" + ) splitted_linestrings = explode_linestring_to_elements(linestring=profile_linestring) # Calculating real world coordinates of endpoints of orientation LineString - points = calculate_coordinates_for_linestring_on_cross_sections(linestring=profile_linestring, - interfaces=orientation_linestring) + points = calculate_coordinates_for_linestring_on_cross_sections( + linestring=profile_linestring, interfaces=orientation_linestring + ) # Setting the orientation to None orientation = None @@ -5689,12 +5970,18 @@ def calculate_orientation_from_bent_cross_section(profile_linestring: shapely.ge linestring = i # Calculating orientation for the previously created LineString and the original orientation linestring - orientation = calculate_orientation_from_cross_section(profile_linestring=linestring, - orientation_linestring=orientation_linestring) + orientation = calculate_orientation_from_cross_section( + profile_linestring=linestring, + orientation_linestring=orientation_linestring, + ) # Replace point of orientation value - midpoint = geometry.Point([((points[0].coords[0][0] + points[1].coords[0][0]) / 2), - ((points[0].coords[0][1] + points[1].coords[0][1]) / 2)]) + midpoint = geometry.Point( + [ + ((points[0].coords[0][0] + points[1].coords[0][0]) / 2), + ((points[0].coords[0][1] + points[1].coords[0][1]) / 2), + ] + ) orientation[0] = midpoint @@ -5704,15 +5991,20 @@ def calculate_orientation_from_bent_cross_section(profile_linestring: shapely.ge # If the orientation is none, hence either one or both points are too far away from the linestring, return an error if orientation is None: - raise ValueError('Orientations may have been digitized across a bent, no orientations were calculated') + raise ValueError( + "Orientations may have been digitized across a bent, no orientations were calculated" + ) return orientation -def calculate_orientations_from_cross_section(profile_linestring: shapely.geometry.linestring.LineString, - orientation_linestrings: Union[gpd.geodataframe.GeoDataFrame, List[ - shapely.geometry.linestring.LineString]], - extract_coordinates: bool = True) -> gpd.geodataframe.GeoDataFrame: +def calculate_orientations_from_cross_section( + profile_linestring: shapely.geometry.linestring.LineString, + orientation_linestrings: Union[ + gpd.geodataframe.GeoDataFrame, List[shapely.geometry.linestring.LineString] + ], + extract_coordinates: bool = True, +) -> gpd.geodataframe.GeoDataFrame: """Calculating orientations from a cross sections using multiple LineStrings Parameters @@ -5774,23 +6066,23 @@ def calculate_orientations_from_cross_section(profile_linestring: shapely.geomet # Checking that the LineString is a Shapely LineString if not isinstance(profile_linestring, shapely.geometry.linestring.LineString): - raise TypeError('Input geometry must be a Shapley LineString') + raise TypeError("Input geometry must be a Shapley LineString") # Checking that the LineString is valid if not profile_linestring.is_valid: - raise ValueError('LineString is not a valid object') + raise ValueError("LineString is not a valid object") # Checking that the LineString is not empty if profile_linestring.is_empty: - raise ValueError('LineString is an empty object') + raise ValueError("LineString is an empty object") # Checking that the input orientations are stored as list or GeoDataFrame if not isinstance(orientation_linestrings, (gpd.geodataframe.GeoDataFrame, list)): - raise TypeError('Orientations must be stored as a GeoDataFrame or in a list') + raise TypeError("Orientations must be stored as a GeoDataFrame or in a list") # Copying the GeoDataFrame Data if isinstance(orientation_linestrings, gpd.geodataframe.GeoDataFrame): - data = orientation_linestrings.copy(deep=True).drop('geometry', axis=1) + data = orientation_linestrings.copy(deep=True).drop("geometry", axis=1) else: data = None @@ -5799,37 +6091,50 @@ def calculate_orientations_from_cross_section(profile_linestring: shapely.geomet orientation_linestrings = orientation_linestrings.geometry.tolist() # Checking that all elements of the geometry column are LineStrings - if not all(isinstance(n, shapely.geometry.linestring.LineString) for n in orientation_linestrings): - raise TypeError('All geometry elements of the linestring_gdf must be Shapely LineStrings') + if not all( + isinstance(n, shapely.geometry.linestring.LineString) + for n in orientation_linestrings + ): + raise TypeError( + "All geometry elements of the linestring_gdf must be Shapely LineStrings" + ) # Checking that all LineStrings are valid if not all(i.is_valid for i in orientation_linestrings): - raise ValueError('Not all Shapely LineStrings are valid') + raise ValueError("Not all Shapely LineStrings are valid") # Checking that no LineStrings are empty if any(i.is_empty for i in orientation_linestrings): - raise ValueError('One or more LineString Objects are empty') + raise ValueError("One or more LineString Objects are empty") # Calculating the orientations - orientations_list = [calculate_orientation_from_bent_cross_section(profile_linestring, i) - for i in orientation_linestrings] + orientations_list = [ + calculate_orientation_from_bent_cross_section(profile_linestring, i) + for i in orientation_linestrings + ] # Creating a GeoDataFrame with the orientation data - gdf = gpd.GeoDataFrame(data=pd.DataFrame(data=[[orientations_list[i][1] for i in range(len(orientations_list))], - [orientations_list[i][2] for i in range(len(orientations_list))], - [orientations_list[i][3] for i in range(len(orientations_list))], - [orientations_list[i][4] for i in range(len(orientations_list))]]).T, - geometry=[orientations_list[i][0] for i in range(len(orientations_list))]) + gdf = gpd.GeoDataFrame( + data=pd.DataFrame( + data=[ + [orientations_list[i][1] for i in range(len(orientations_list))], + [orientations_list[i][2] for i in range(len(orientations_list))], + [orientations_list[i][3] for i in range(len(orientations_list))], + [orientations_list[i][4] for i in range(len(orientations_list))], + ] + ).T, + geometry=[orientations_list[i][0] for i in range(len(orientations_list))], + ) # Assigning column names - gdf.columns = ['Z', 'dip', 'azimuth', 'polarity', 'geometry'] + gdf.columns = ["Z", "dip", "azimuth", "polarity", "geometry"] # Extracting X and Y coordinates from point objects if extract_coordinates: gdf = extract_xy(gdf) # Sorting the columns - gdf = gdf[['X', 'Y', 'Z', 'dip', 'azimuth', 'polarity', 'geometry']] + gdf = gdf[["X", "Y", "Z", "dip", "azimuth", "polarity", "geometry"]] # If the input is a GeoDataFrame, append the remaining data to the orientations GeoDataFrame if data is not None: @@ -5838,9 +6143,11 @@ def calculate_orientations_from_cross_section(profile_linestring: shapely.geomet return gdf -def extract_orientations_from_cross_sections(profile_gdf: gpd.geodataframe.GeoDataFrame, - orientations_gdf: gpd.geodataframe.GeoDataFrame, - profile_name_column: str = 'name') -> gpd.geodataframe.GeoDataFrame: +def extract_orientations_from_cross_sections( + profile_gdf: gpd.geodataframe.GeoDataFrame, + orientations_gdf: gpd.geodataframe.GeoDataFrame, + profile_name_column: str = "name", +) -> gpd.geodataframe.GeoDataFrame: """Calculating orientations digitized from cross sections Parameters @@ -5905,69 +6212,89 @@ def extract_orientations_from_cross_sections(profile_gdf: gpd.geodataframe.GeoDa # Checking that the profile traces are provided as GeoDataFrame if not isinstance(profile_gdf, gpd.geodataframe.GeoDataFrame): - raise TypeError('Profile traces must be provided as GeoDataFrame') + raise TypeError("Profile traces must be provided as GeoDataFrame") # Checking that the input orientations are stored as GeoDataFrame if not isinstance(orientations_gdf, gpd.geodataframe.GeoDataFrame): - raise TypeError('Orientations must be provided as GeoDataFrame') + raise TypeError("Orientations must be provided as GeoDataFrame") # Checking that the column profile name column is present in the GeoDataFrame if profile_name_column not in profile_gdf: - raise ValueError('Column with profile names not found, provide profile_name_column') + raise ValueError( + "Column with profile names not found, provide profile_name_column" + ) # Checking that the column profile name column is present in the GeoDataFrame if profile_name_column not in orientations_gdf: - raise ValueError('Column with profile names not found, provide profile_name_column') + raise ValueError( + "Column with profile names not found, provide profile_name_column" + ) # Checking that all elements of the geometry column are LineStrings - if not all(isinstance(n, shapely.geometry.linestring.LineString) for n in profile_gdf.geometry.tolist()): - raise TypeError('All geometry elements of the profile_gdf must be Shapely LineStrings') + if not all( + isinstance(n, shapely.geometry.linestring.LineString) + for n in profile_gdf.geometry.tolist() + ): + raise TypeError( + "All geometry elements of the profile_gdf must be Shapely LineStrings" + ) # Checking that all elements of the geometry column are LineStrings - if not all(isinstance(n, shapely.geometry.linestring.LineString) for n in orientations_gdf.geometry.tolist()): - raise TypeError('All geometry elements of the orientations_gdf must be Shapely LineStrings') + if not all( + isinstance(n, shapely.geometry.linestring.LineString) + for n in orientations_gdf.geometry.tolist() + ): + raise TypeError( + "All geometry elements of the orientations_gdf must be Shapely LineStrings" + ) # Checking that all elements of the geometry column are valid if not all(n.is_valid for n in profile_gdf.geometry.tolist()): - raise ValueError('All Shapely LineStrings must be valid') + raise ValueError("All Shapely LineStrings must be valid") # Checking that all elements of the geometry column are not empty if any(n.is_empty for n in orientations_gdf.geometry.tolist()): - raise ValueError('One or more geometries are empty') + raise ValueError("One or more geometries are empty") # Checking that all elements of the geometry column are valid if not all(n.is_valid for n in profile_gdf.geometry.tolist()): - raise ValueError('All Shapely LineStrings must be valid') + raise ValueError("All Shapely LineStrings must be valid") # Checking that all elements of the geometry column are not empty if any(n.is_empty for n in orientations_gdf.geometry.tolist()): - raise ValueError('One or more geometries are empty') + raise ValueError("One or more geometries are empty") # Create list of GeoDataFrames containing orientation and location information for orientations on cross sections - list_gdf = [calculate_orientations_from_cross_section( - profile_gdf.geometry[i], - orientations_gdf[orientations_gdf[profile_name_column] == profile_gdf[profile_name_column][i]].reset_index()) - for i in range(len(profile_gdf))] + list_gdf = [ + calculate_orientations_from_cross_section( + profile_gdf.geometry[i], + orientations_gdf[ + orientations_gdf[profile_name_column] + == profile_gdf[profile_name_column][i] + ].reset_index(), + ) + for i in range(len(profile_gdf)) + ] # Merging the list of gdfs, resetting the index and dropping the index column gdf = pd.concat(list_gdf) # Dropping column if it is in the gdf - if 'level_0' in gdf: - gdf = gdf.drop('level_0', axis=1) + if "level_0" in gdf: + gdf = gdf.drop("level_0", axis=1) # Resetting index and dropping columns - gdf = gdf.reset_index().drop(['index', 'level_0'], axis=1) + gdf = gdf.reset_index().drop(["index", "level_0"], axis=1) # Creating GeoDataFrame - gdf = gpd.GeoDataFrame(data=gdf, - geometry=gdf['geometry'], - crs=orientations_gdf.crs) + gdf = gpd.GeoDataFrame(data=gdf, geometry=gdf["geometry"], crs=orientations_gdf.crs) return gdf -def calculate_orientation_for_three_point_problem(gdf: gpd.geodataframe.GeoDataFrame) -> gpd.geodataframe.GeoDataFrame: +def calculate_orientation_for_three_point_problem( + gdf: gpd.geodataframe.GeoDataFrame, +) -> gpd.geodataframe.GeoDataFrame: """Calculating the orientation for a three point problem Parameters @@ -6007,35 +6334,34 @@ def calculate_orientation_for_three_point_problem(gdf: gpd.geodataframe.GeoDataF # Checking that the points are provided as GeoDataFrame if not isinstance(gdf, gpd.geodataframe.GeoDataFrame): - raise TypeError('Profile traces must be provided as GeoDataFrame') + raise TypeError("Profile traces must be provided as GeoDataFrame") # Checking that the GeoDataFrame consists of points if not all(shapely.get_type_id(gdf.geometry) == 0): - raise TypeError('All elements must be of geometry type Point') + raise TypeError("All elements must be of geometry type Point") # Checking that the length of the GeoDataFrame is 3 if not len(gdf) == 3: - raise ValueError('GeoDataFrame must only contain three points') + raise ValueError("GeoDataFrame must only contain three points") # Extracting X and Y values - if not {'X', 'Y'}.issubset(gdf.columns): + if not {"X", "Y"}.issubset(gdf.columns): gdf = extract_xy(gdf=gdf) # Checking that the Z column is in the GeoDataFrame - if 'Z' not in gdf: - raise ValueError('Z values missing in GeoDataFrame') + if "Z" not in gdf: + raise ValueError("Z values missing in GeoDataFrame") # Sorting the points by altitude and reset index - gdf = gdf.sort_values(by='Z', ascending=True).reset_index(drop=True) + gdf = gdf.sort_values(by="Z", ascending=True).reset_index(drop=True) # Getting the point values - point1 = gdf[['X', 'Y', 'Z']].loc[0].values - point2 = gdf[['X', 'Y', 'Z']].loc[1].values - point3 = gdf[['X', 'Y', 'Z']].loc[2].values + point1 = gdf[["X", "Y", "Z"]].loc[0].values + point2 = gdf[["X", "Y", "Z"]].loc[1].values + point3 = gdf[["X", "Y", "Z"]].loc[2].values # Calculating the normal for the points - normal = np.cross(a=point3 - point2, - b=point1 - point2) + normal = np.cross(a=point3 - point2, b=point1 - point2) normal /= np.linalg.norm(normal) @@ -6050,16 +6376,36 @@ def calculate_orientation_for_three_point_problem(gdf: gpd.geodataframe.GeoDataF azimuth = 180 - azimuth # Calculate location of orientation - x = np.mean(gdf['X'].values) - y = np.mean(gdf['Y'].values) - z = np.mean(gdf['Z'].values) + x = np.mean(gdf["X"].values) + y = np.mean(gdf["Y"].values) + z = np.mean(gdf["Z"].values) # Creating GeoDataFrame - orientation = gpd.GeoDataFrame(data=pd.DataFrame( - [float(z), gdf['formation'].unique()[0], float(azimuth), float(dip), float(1), float(x), float(y)]).T, - geometry=gpd.points_from_xy(x=[x], y=[y]), - crs=gdf.crs) - orientation.columns = ['Z', 'formation', 'azimuth', 'dip', 'polarity', 'X', 'Y', 'geometry'] + orientation = gpd.GeoDataFrame( + data=pd.DataFrame( + [ + float(z), + gdf["formation"].unique()[0], + float(azimuth), + float(dip), + float(1), + float(x), + float(y), + ] + ).T, + geometry=gpd.points_from_xy(x=[x], y=[y]), + crs=gdf.crs, + ) + orientation.columns = [ + "Z", + "formation", + "azimuth", + "dip", + "polarity", + "X", + "Y", + "geometry", + ] return orientation @@ -6068,9 +6414,10 @@ def calculate_orientation_for_three_point_problem(gdf: gpd.geodataframe.GeoDataF ######################################### -def intersect_polygon_polygon(polygon1: shapely.geometry.polygon.Polygon, - polygon2: shapely.geometry.polygon.Polygon) \ - -> Union[shapely.geometry.linestring.LineString, shapely.geometry.polygon.Polygon]: +def intersect_polygon_polygon( + polygon1: shapely.geometry.polygon.Polygon, + polygon2: shapely.geometry.polygon.Polygon, +) -> Union[shapely.geometry.linestring.LineString, shapely.geometry.polygon.Polygon]: """Calculating the intersection between to Shapely Polygons Parameters @@ -6125,27 +6472,27 @@ def intersect_polygon_polygon(polygon1: shapely.geometry.polygon.Polygon, # Checking that the input polygon is a Shapely Polygon if not isinstance(polygon1, shapely.geometry.polygon.Polygon): - raise TypeError('Input Polygon1 must a be Shapely Polygon') + raise TypeError("Input Polygon1 must a be Shapely Polygon") # Checking that the input polygon is a Shapely Polygon if not isinstance(polygon2, shapely.geometry.polygon.Polygon): - raise TypeError('Input Polygon2 must a be Shapely Polygon') + raise TypeError("Input Polygon2 must a be Shapely Polygon") # Checking if input geometries are valid if not polygon1.is_valid: - raise ValueError('Input polygon 1 is an invalid input geometry') + raise ValueError("Input polygon 1 is an invalid input geometry") # Checking if input geometries are valid if not polygon2.is_valid: - raise ValueError('Input polygon 2 is an invalid input geometry') + raise ValueError("Input polygon 2 is an invalid input geometry") # Checking if input geometries are empty if polygon1.is_empty: - raise ValueError('Input polygon 1 is an empty input geometry') + raise ValueError("Input polygon 1 is an empty input geometry") # Checking if input geometries are empty if polygon2.is_empty: - raise ValueError('Input polygon 2 is an empty input geometry') + raise ValueError("Input polygon 2 is an empty input geometry") # Calculating the intersections intersection = polygon1.intersection(polygon2) @@ -6153,10 +6500,12 @@ def intersect_polygon_polygon(polygon1: shapely.geometry.polygon.Polygon, return intersection -def intersect_polygon_polygons(polygon1: shapely.geometry.polygon.Polygon, - polygons2: Union[ - gpd.geodataframe.GeoDataFrame, List[shapely.geometry.polygon.Polygon]]) \ - -> List[shapely.geometry.base.BaseGeometry]: +def intersect_polygon_polygons( + polygon1: shapely.geometry.polygon.Polygon, + polygons2: Union[ + gpd.geodataframe.GeoDataFrame, List[shapely.geometry.polygon.Polygon] + ], +) -> List[shapely.geometry.base.BaseGeometry]: """Calculating the intersections between one polygon and a list of polygons Parameters @@ -6222,19 +6571,19 @@ def intersect_polygon_polygons(polygon1: shapely.geometry.polygon.Polygon, # Checking that the input polygon is a Shapely Polygon if not isinstance(polygon1, shapely.geometry.polygon.Polygon): - raise TypeError('Input Polygon1 must a be Shapely Polygon') + raise TypeError("Input Polygon1 must a be Shapely Polygon") # Checking if input geometries are valid if not polygon1.is_valid: - raise ValueError('Input polygon 1 is an invalid input geometry') + raise ValueError("Input polygon 1 is an invalid input geometry") # Checking if input geometries are empty if polygon1.is_empty: - raise ValueError('Input polygon 1 is an empty input geometry') + raise ValueError("Input polygon 1 is an empty input geometry") # Checking that the input polygon is a list or a GeoDataFrame if not isinstance(polygons2, (gpd.geodataframe.GeoDataFrame, list)): - raise TypeError('Input Polygon2 must a be GeoDataFrame or list') + raise TypeError("Input Polygon2 must a be GeoDataFrame or list") # Converting the Polygons stored in the GeoDataFrame into a list and removing invalid geometries if isinstance(polygons2, gpd.geodataframe.GeoDataFrame): @@ -6243,27 +6592,33 @@ def intersect_polygon_polygons(polygon1: shapely.geometry.polygon.Polygon, # Checking that all elements of the geometry column are Polygons if not all(isinstance(n, shapely.geometry.polygon.Polygon) for n in polygons2): - raise TypeError('All geometry elements of polygons2 must be Shapely Polygons') + raise TypeError("All geometry elements of polygons2 must be Shapely Polygons") # Checking that all elements of the geometry column are valid if not all(n.is_valid for n in polygons2): - raise TypeError('All geometry elements of polygons2 must be valid') + raise TypeError("All geometry elements of polygons2 must be valid") # Checking that all elements of the geometry column are not empty if any(n.is_empty for n in polygons2): - raise TypeError('None of the geometry elements of polygons2 must be empty') + raise TypeError("None of the geometry elements of polygons2 must be empty") # Creating the list of intersection geometries - intersections = [intersect_polygon_polygon(polygon1=polygon1, - polygon2=polygon) for polygon in polygons2] + intersections = [ + intersect_polygon_polygon(polygon1=polygon1, polygon2=polygon) + for polygon in polygons2 + ] return intersections def intersect_polygons_polygons( - polygons1: Union[gpd.geodataframe.GeoDataFrame, List[shapely.geometry.polygon.Polygon]], - polygons2: Union[gpd.geodataframe.GeoDataFrame, List[shapely.geometry.polygon.Polygon]]) \ - -> List[shapely.geometry.base.BaseGeometry]: + polygons1: Union[ + gpd.geodataframe.GeoDataFrame, List[shapely.geometry.polygon.Polygon] + ], + polygons2: Union[ + gpd.geodataframe.GeoDataFrame, List[shapely.geometry.polygon.Polygon] + ], +) -> List[shapely.geometry.base.BaseGeometry]: """Calculating the intersections between a list of Polygons Parameters @@ -6341,7 +6696,7 @@ def intersect_polygons_polygons( # Checking that the input polygon is a list or a GeoDataFrame if not isinstance(polygons1, (gpd.geodataframe.GeoDataFrame, list)): - raise TypeError('Input Polygon2 must a be Shapely Polygon') + raise TypeError("Input Polygon2 must a be Shapely Polygon") # Converting the Polygons stored in the GeoDataFrame into a list if isinstance(polygons1, gpd.geodataframe.GeoDataFrame): @@ -6351,19 +6706,19 @@ def intersect_polygons_polygons( # Checking that all elements of the geometry column are Polygons if not all(isinstance(n, shapely.geometry.polygon.Polygon) for n in polygons1): - raise TypeError('All geometry elements of polygons2 must be Shapely Polygons') + raise TypeError("All geometry elements of polygons2 must be Shapely Polygons") # Checking that all elements of the geometry column are valid if not all(n.is_valid for n in polygons1): - raise TypeError('All geometry elements of polygons1 must be valid') + raise TypeError("All geometry elements of polygons1 must be valid") # Checking that all elements of the geometry column are not empty if any(n.is_empty for n in polygons1): - raise TypeError('None of the geometry elements of polygons1 must be empty') + raise TypeError("None of the geometry elements of polygons1 must be empty") # Checking that the input polygon is a list or a GeoDataFrame if not isinstance(polygons2, (gpd.geodataframe.GeoDataFrame, list)): - raise TypeError('Input Polygon2 must a be Shapely Polygon') + raise TypeError("Input Polygon2 must a be Shapely Polygon") # Converting the Polygons stored in the GeoDataFrame into a list if isinstance(polygons2, gpd.geodataframe.GeoDataFrame): @@ -6373,29 +6728,37 @@ def intersect_polygons_polygons( # Checking that all elements of the geometry column are Polygons if not all(isinstance(n, shapely.geometry.polygon.Polygon) for n in polygons2): - raise TypeError('All geometry elements of polygons2 must be Shapely Polygons') + raise TypeError("All geometry elements of polygons2 must be Shapely Polygons") # Checking that all elements of the geometry column are valid if not all(n.is_valid for n in polygons2): - raise TypeError('All geometry elements of polygons2 must be valid') + raise TypeError("All geometry elements of polygons2 must be valid") # Checking that all elements of the geometry column are not empty if any(n.is_empty for n in polygons2): - raise TypeError('None of the geometry elements of polygons2 must be empty') + raise TypeError("None of the geometry elements of polygons2 must be empty") # Creating list with lists of intersections - intersections = [intersect_polygon_polygons(polygon1=polygon, - polygons2=polygons2) for polygon in polygons1] + intersections = [ + intersect_polygon_polygons(polygon1=polygon, polygons2=polygons2) + for polygon in polygons1 + ] # Creating single list from list of lists - intersections = [intersections[i][j] for i in range(len(intersections)) for j in range(len(intersections[i]))] + intersections = [ + intersections[i][j] + for i in range(len(intersections)) + for j in range(len(intersections[i])) + ] return intersections -def extract_xy_from_polygon_intersections(gdf: gpd.geodataframe.GeoDataFrame, - extract_coordinates: bool = False, - drop_index: bool = True) -> gpd.geodataframe.GeoDataFrame: +def extract_xy_from_polygon_intersections( + gdf: gpd.geodataframe.GeoDataFrame, + extract_coordinates: bool = False, + drop_index: bool = True, +) -> gpd.geodataframe.GeoDataFrame: """Calculating the intersections between Polygons; the table must be sorted by stratigraphic age Parameters @@ -6461,45 +6824,64 @@ def extract_xy_from_polygon_intersections(gdf: gpd.geodataframe.GeoDataFrame, # Checking that the polygons of the geological map are provided as GeoDataFrame if not isinstance(gdf, gpd.geodataframe.GeoDataFrame): - raise TypeError('Input Geometries must be stored as GeoDataFrame') + raise TypeError("Input Geometries must be stored as GeoDataFrame") # Checking that the formation name is in the GeoDataFrame - if 'formation' not in gdf: - raise ValueError('No formation column found') + if "formation" not in gdf: + raise ValueError("No formation column found") # Removing invalid geometries and resetting the index gdf = gdf[gdf.geometry.is_valid].reset_index(drop=True) # Creating a list of GeoDataFrames with intersections - intersections = [intersect_polygons_polygons( - polygons1=gdf[gdf['formation'].isin([gdf['formation'].unique().tolist()[i]])], - polygons2=gdf[gdf['formation'].isin(gdf['formation'].unique().tolist()[i + 1:])]) - for i in range(len(gdf['formation'].unique().tolist()))] + intersections = [ + intersect_polygons_polygons( + polygons1=gdf[ + gdf["formation"].isin([gdf["formation"].unique().tolist()[i]]) + ], + polygons2=gdf[ + gdf["formation"].isin(gdf["formation"].unique().tolist()[i + 1 :]) + ], + ) + for i in range(len(gdf["formation"].unique().tolist())) + ] # Creating list from list of lists - intersections = [intersections[i][j] for i in range(len(intersections)) for j in range(len(intersections[i]))] + intersections = [ + intersections[i][j] + for i in range(len(intersections)) + for j in range(len(intersections[i])) + ] # Counting the number of different sections - counts = [len(gdf[gdf['formation'] == gdf['formation'].unique().tolist()[i]]) for - i in range(len(gdf['formation'].unique()))] + counts = [ + len(gdf[gdf["formation"] == gdf["formation"].unique().tolist()[i]]) + for i in range(len(gdf["formation"].unique())) + ] # Counting the number of different sections - values = [(len(gdf[gdf['formation'] != gdf['formation'].unique().tolist()[i]]) - len( - gdf[gdf['formation'].isin(gdf['formation'].unique().tolist()[:i])])) for i in - range(len(gdf['formation'].unique()))] + values = [ + ( + len(gdf[gdf["formation"] != gdf["formation"].unique().tolist()[i]]) + - len(gdf[gdf["formation"].isin(gdf["formation"].unique().tolist()[:i])]) + ) + for i in range(len(gdf["formation"].unique())) + ] # Create array with repeated values - repeated_values = np.concatenate([np.ones(counts[i]) * values[i] for i in range(len(counts))]).astype(int) + repeated_values = np.concatenate( + [np.ones(counts[i]) * values[i] for i in range(len(counts))] + ).astype(int) # Create DataFrame from input gdf df = pd.DataFrame(gdf.values.repeat(repeated_values, axis=0)) df.columns = gdf.columns # Create gdf with intersections - gdf = gpd.GeoDataFrame(data=df.drop('geometry', axis=1), - geometry=intersections, - crs=gdf.crs) - gdf = gdf[(gdf.geom_type != 'Point') & (gdf.geom_type != 'GeometryCollection')] + gdf = gpd.GeoDataFrame( + data=df.drop("geometry", axis=1), geometry=intersections, crs=gdf.crs + ) + gdf = gdf[(gdf.geom_type != "Point") & (gdf.geom_type != "GeometryCollection")] gdf = gdf[~gdf.is_empty].reset_index() # Extracting coordinates @@ -6507,9 +6889,8 @@ def extract_xy_from_polygon_intersections(gdf: gpd.geodataframe.GeoDataFrame, gdf = extract_xy(gdf=gdf) # Dropping index column - if 'index' in gdf and drop_index: - gdf = gdf.drop(columns='index', - axis=1) + if "index" in gdf and drop_index: + gdf = gdf.drop(columns="index", axis=1) return gdf @@ -6518,8 +6899,11 @@ def extract_xy_from_polygon_intersections(gdf: gpd.geodataframe.GeoDataFrame, ############################################ -def calculate_azimuth(gdf: Union[gpd.geodataframe.GeoDataFrame, - List[shapely.geometry.linestring.LineString]]) -> List[Union[float, int]]: +def calculate_azimuth( + gdf: Union[ + gpd.geodataframe.GeoDataFrame, List[shapely.geometry.linestring.LineString] + ] +) -> List[Union[float, int]]: """Calculating the azimuth for an orientation Geodataframe represented by LineStrings Parameters @@ -6577,33 +6961,36 @@ def calculate_azimuth(gdf: Union[gpd.geodataframe.GeoDataFrame, # Checking that gdf is a GeoDataFrame if not isinstance(gdf, (gpd.geodataframe.GeoDataFrame, list)): - raise TypeError('Data must be a GeoDataFrame or a list of LineStrings') + raise TypeError("Data must be a GeoDataFrame or a list of LineStrings") # Converting the LineStrings stored in the GeoDataFrame into a list if isinstance(gdf, gpd.geodataframe.GeoDataFrame): # Checking that the pd_series contains a linestring if not all(shapely.get_type_id(gdf.geometry) == 1): - raise TypeError('All elements must be of geometry type LineString') + raise TypeError("All elements must be of geometry type LineString") gdf = gdf.geometry.tolist() # Checking that all elements of the geometry column are valid if not all(n.is_valid for n in gdf): - raise ValueError('All Shapely LineStrings must be valid') + raise ValueError("All Shapely LineStrings must be valid") # Checking that all elements of the geometry column are not empty if any(n.is_empty for n in gdf): - raise ValueError('One or more geometries are empty') + raise ValueError("One or more geometries are empty") # Calculating the azimuths - azimuth_list = [calculate_strike_direction_straight_linestring(linestring=linestring) for linestring in gdf] + azimuth_list = [ + calculate_strike_direction_straight_linestring(linestring=linestring) + for linestring in gdf + ] return azimuth_list -def create_linestring_from_points(gdf: gpd.geodataframe.GeoDataFrame, - formation: str, - altitude: Union[int, float]) -> shapely.geometry.linestring.LineString: +def create_linestring_from_points( + gdf: gpd.geodataframe.GeoDataFrame, formation: str, altitude: Union[int, float] +) -> shapely.geometry.linestring.LineString: """Creating a LineString object from a GeoDataFrame containing surface points at a given altitude and for a given formation @@ -6664,34 +7051,34 @@ def create_linestring_from_points(gdf: gpd.geodataframe.GeoDataFrame, # Checking if gdf is of type GeoDataFrame if not isinstance(gdf, gpd.geodataframe.GeoDataFrame): - raise TypeError('gdf must be of type GeoDataFrame') + raise TypeError("gdf must be of type GeoDataFrame") # Checking geometry type of GeoDataFrame - if not all(gdf.geom_type == 'Point'): - raise ValueError('All objects of the GeoDataFrame must be of geom_type point') + if not all(gdf.geom_type == "Point"): + raise ValueError("All objects of the GeoDataFrame must be of geom_type point") # Checking if X and Y values are in column - if not {'formation', 'Z'}.issubset(gdf.columns): - raise ValueError('formation or Z column missing in GeoDataFrame') + if not {"formation", "Z"}.issubset(gdf.columns): + raise ValueError("formation or Z column missing in GeoDataFrame") # Checking if the formation is of type string if not isinstance(formation, str): - raise TypeError('formation must be of type string') + raise TypeError("formation must be of type string") # Checking that the formation is present in the GeoDataFrame - if formation not in gdf['formation'].unique().tolist(): - raise ValueError('Formation is not in GeoDataFrame') + if formation not in gdf["formation"].unique().tolist(): + raise ValueError("Formation is not in GeoDataFrame") # Checking if the altitude is of type int or float if not isinstance(altitude, (int, float)): - raise TypeError('Altitude must be of type int or float') + raise TypeError("Altitude must be of type int or float") # Creating a copy of the GeoDataFrame gdf_new = gdf.copy(deep=True) # Filtering GeoDataFrame by formation and altitude - gdf_new = gdf_new[gdf_new['formation'] == formation] - gdf_new = gdf_new[gdf_new['Z'] == altitude] + gdf_new = gdf_new[gdf_new["formation"] == formation] + gdf_new = gdf_new[gdf_new["Z"] == altitude] # Creating LineString from all available points linestring = geometry.LineString(gdf_new.geometry.to_list()) @@ -6699,7 +7086,9 @@ def create_linestring_from_points(gdf: gpd.geodataframe.GeoDataFrame, return linestring -def create_linestring_gdf(gdf: gpd.geodataframe.GeoDataFrame) -> gpd.geodataframe.GeoDataFrame: +def create_linestring_gdf( + gdf: gpd.geodataframe.GeoDataFrame, +) -> gpd.geodataframe.GeoDataFrame: """Creating LineStrings from Points Parameters @@ -6755,43 +7144,45 @@ def create_linestring_gdf(gdf: gpd.geodataframe.GeoDataFrame) -> gpd.geodatafram # Checking if gdf is of type GeoDataFrame if not isinstance(gdf, gpd.geodataframe.GeoDataFrame): - raise TypeError('gdf must be of type GeoDataFrame') + raise TypeError("gdf must be of type GeoDataFrame") # Checking geometry type of GeoDataFrame - if not all(gdf.geom_type == 'Point'): - raise ValueError('All objects of the GeoDataFrame must be of geom_type point') + if not all(gdf.geom_type == "Point"): + raise ValueError("All objects of the GeoDataFrame must be of geom_type point") # Checking if X and Y values are in column - if not {'formation', 'Z'}.issubset(gdf.columns): - raise ValueError('formation or Z column missing in GeoDataFrame') + if not {"formation", "Z"}.issubset(gdf.columns): + raise ValueError("formation or Z column missing in GeoDataFrame") # Create copy of gdf gdf_new = gdf.copy(deep=True) # Sort by Z values - gdf_new = gdf_new.sort_values('Z') + gdf_new = gdf_new.sort_values("Z") # Create empty LineString list linestrings = [] # Create LineStrings and append to list - for i in gdf_new['formation'].unique().tolist(): - for j in gdf_new['Z'].unique().tolist(): - linestring = create_linestring_from_points(gdf=gdf_new, - formation=i, - altitude=j) + for i in gdf_new["formation"].unique().tolist(): + for j in gdf_new["Z"].unique().tolist(): + linestring = create_linestring_from_points( + gdf=gdf_new, formation=i, altitude=j + ) linestrings.append(linestring) # Create gdf - gdf_linestrings = gpd.GeoDataFrame(data=gdf_new.drop_duplicates(subset='id').drop(labels='geometry', axis=1), - geometry=linestrings, - crs=gdf_new.crs) + gdf_linestrings = gpd.GeoDataFrame( + data=gdf_new.drop_duplicates(subset="id").drop(labels="geometry", axis=1), + geometry=linestrings, + crs=gdf_new.crs, + ) # Add Z values - gdf_linestrings['Z'] = gdf_new['Z'].unique() + gdf_linestrings["Z"] = gdf_new["Z"].unique() # Add formation name - gdf_linestrings['formation'] = gdf['formation'].unique()[0] + gdf_linestrings["formation"] = gdf["formation"].unique()[0] # Resetting Index gdf_linestrings = gdf_linestrings.reset_index() @@ -6799,8 +7190,9 @@ def create_linestring_gdf(gdf: gpd.geodataframe.GeoDataFrame) -> gpd.geodatafram return gdf_linestrings -def extract_orientations_from_map(gdf: gpd.geodataframe.GeoDataFrame, - dz: str = 'dZ') -> gpd.geodataframe.GeoDataFrame: +def extract_orientations_from_map( + gdf: gpd.geodataframe.GeoDataFrame, dz: str = "dZ" +) -> gpd.geodataframe.GeoDataFrame: """Calculating orientations from LineStrings Parameters @@ -6864,58 +7256,61 @@ def extract_orientations_from_map(gdf: gpd.geodataframe.GeoDataFrame, # Checking that gdf is a GeoDataFrame if not isinstance(gdf, gpd.geodataframe.GeoDataFrame): - raise TypeError('Data must be a GeoDataFrame') + raise TypeError("Data must be a GeoDataFrame") # Checking that the pd_series contains a linestring if not all(shapely.get_type_id(gdf.geometry) == 1): - raise TypeError('All elements must be of geometry type LineString') + raise TypeError("All elements must be of geometry type LineString") # Checking that all elements of the geometry column are valid if not all(n.is_valid for n in gdf.geometry.tolist()): - raise ValueError('All Shapely LineStrings must be valid') + raise ValueError("All Shapely LineStrings must be valid") # Checking that all elements of the geometry column are not empty if any(n.is_empty for n in gdf.geometry.tolist()): - raise ValueError('One or more geometries are empty') + raise ValueError("One or more geometries are empty") # Checking that the height difference column is of type str if not isinstance(dz, str): - raise TypeError('Height difference column must be of type str') + raise TypeError("Height difference column must be of type str") # Checking that the height difference column is in the gdf if dz not in gdf: - raise ValueError('Provide valid name for the height difference column dz') + raise ValueError("Provide valid name for the height difference column dz") # Copy gdf gdf = gdf.copy(deep=True) # Calculating the azimuths - gdf['azimuth'] = calculate_azimuth(gdf=gdf) + gdf["azimuth"] = calculate_azimuth(gdf=gdf) # Obtaining the lengths of LineStrings - gdf['length'] = gdf.geometry.length + gdf["length"] = gdf.geometry.length # Calculating the dip based on the height difference and length of the LineString - gdf['dip'] = np.rad2deg(np.arctan(gdf[dz] / gdf['length'])) + gdf["dip"] = np.rad2deg(np.arctan(gdf[dz] / gdf["length"])) # Calculating new geometry column - gdf['geometry'] = calculate_midpoints_linestrings(linestring_gdf=gdf) + gdf["geometry"] = calculate_midpoints_linestrings(linestring_gdf=gdf) # Recreating GeoDataFrame - gdf = gpd.GeoDataFrame(data=gdf.drop(labels=['dZ', 'length'], axis=1), geometry=gdf['geometry']) + gdf = gpd.GeoDataFrame( + data=gdf.drop(labels=["dZ", "length"], axis=1), geometry=gdf["geometry"] + ) # Extracting X and Y Coordinates - gdf = extract_xy(gdf=gdf, - reset_index=False) + gdf = extract_xy(gdf=gdf, reset_index=False) # Setting the polarity - gdf['polarity'] = 1 + gdf["polarity"] = 1 return gdf -def calculate_distance_linestrings(ls1: shapely.geometry.linestring.LineString, - ls2: shapely.geometry.linestring.LineString) -> float: +def calculate_distance_linestrings( + ls1: shapely.geometry.linestring.LineString, + ls2: shapely.geometry.linestring.LineString, +) -> float: """Calculating the minimal distance between two LineStrings Parameters @@ -6967,27 +7362,27 @@ def calculate_distance_linestrings(ls1: shapely.geometry.linestring.LineString, # Checking that ls1 is a Shapely LineString if not isinstance(ls1, shapely.geometry.linestring.LineString): - raise TypeError('Line Object must be a Shapely LineString') + raise TypeError("Line Object must be a Shapely LineString") # Checking that ls2 is a Shapely LineString if not isinstance(ls2, shapely.geometry.linestring.LineString): - raise TypeError('Line Object must be a Shapely LineString') + raise TypeError("Line Object must be a Shapely LineString") # Checking that the LineString is valid if not ls1.is_valid: - raise ValueError('LineString is not a valid object') + raise ValueError("LineString is not a valid object") # Checking that the LineString is not empty if ls1.is_empty: - raise ValueError('LineString is an empty object') + raise ValueError("LineString is an empty object") # Checking that the LineString is valid if not ls2.is_valid: - raise ValueError('LineString is not a valid object') + raise ValueError("LineString is not a valid object") # Checking that the LineString is not empty if ls2.is_empty: - raise ValueError('LineString is an empty object') + raise ValueError("LineString is an empty object") # Calculating the distance distance = ls1.distance(ls2) @@ -6995,7 +7390,9 @@ def calculate_distance_linestrings(ls1: shapely.geometry.linestring.LineString, return distance -def calculate_orientations_from_strike_lines(gdf: gpd.geodataframe.GeoDataFrame) -> gpd.geodataframe.GeoDataFrame: +def calculate_orientations_from_strike_lines( + gdf: gpd.geodataframe.GeoDataFrame, +) -> gpd.geodataframe.GeoDataFrame: """Calculating orientations based on LineStrings representing strike lines Parameters @@ -7059,68 +7456,85 @@ def calculate_orientations_from_strike_lines(gdf: gpd.geodataframe.GeoDataFrame) # Checking that gdf is a GeoDataFrame if not isinstance(gdf, gpd.geodataframe.GeoDataFrame): - raise TypeError('Data must be a GeoDataFrame') + raise TypeError("Data must be a GeoDataFrame") # Checking that the pd_series contains a linestring if not all(shapely.get_type_id(gdf.geometry) == 1): - raise TypeError('All elements must be of geometry type LineString') + raise TypeError("All elements must be of geometry type LineString") # Checking that all geometry objects are valid if not all(n.is_valid for n in gdf.geometry.tolist()): - raise ValueError('Not all geometry objects are valid') + raise ValueError("Not all geometry objects are valid") # Checking that no geometry object is empty if any(n.is_empty for n in gdf.geometry.tolist()): - raise ValueError('One or more geometry objects are empty') + raise ValueError("One or more geometry objects are empty") # Checking that the Z column is present in the GeoDataFrame - if 'Z' not in gdf: - raise ValueError('Z column not found in GeoDataFrame') + if "Z" not in gdf: + raise ValueError("Z column not found in GeoDataFrame") # Checking that the id column is present in the GeoDataFrame - if 'id' not in gdf: - raise ValueError('id column must be present in GeoDataFrame to assign order of LineStrings') + if "id" not in gdf: + raise ValueError( + "id column must be present in GeoDataFrame to assign order of LineStrings" + ) # Sorting values by Z value and resetting index - gdf = gdf.sort_values(by='Z', ascending=True).reset_index(drop=True) + gdf = gdf.sort_values(by="Z", ascending=True).reset_index(drop=True) # Calculating distances between strike lines - distances = [calculate_distance_linestrings(ls1=gdf.loc[i].geometry, - ls2=gdf.loc[i + 1].geometry) for i in range(len(gdf) - 1)] + distances = [ + calculate_distance_linestrings( + ls1=gdf.loc[i].geometry, ls2=gdf.loc[i + 1].geometry + ) + for i in range(len(gdf) - 1) + ] # Calculating midpoints of LineStrings midpoints = calculate_midpoints_linestrings(linestring_gdf=gdf) # Creating new LineStrings between strike lines - linestrings_new = [shapely.geometry.LineString([midpoints[i], midpoints[i + 1]]) for i in range(len(midpoints) - 1)] + linestrings_new = [ + shapely.geometry.LineString([midpoints[i], midpoints[i + 1]]) + for i in range(len(midpoints) - 1) + ] # Calculating the location of orientations as midpoints of new LineStrings - orientations_locations = calculate_midpoints_linestrings(linestring_gdf=linestrings_new) + orientations_locations = calculate_midpoints_linestrings( + linestring_gdf=linestrings_new + ) # Calculating dips of orientations based on the height difference and distance between LineStrings dips = np.abs( - [np.rad2deg(np.arctan((gdf.loc[i + 1]['Z'] - gdf.loc[i]['Z']) / distances[i])) for i in range(len(gdf) - 1)]) + [ + np.rad2deg( + np.arctan((gdf.loc[i + 1]["Z"] - gdf.loc[i]["Z"]) / distances[i]) + ) + for i in range(len(gdf) - 1) + ] + ) # Calculating altitudes of new orientations - altitudes = [(gdf.loc[i + 1]['Z'] + gdf.loc[i]['Z']) / 2 for i in range(len(gdf) - 1)] + altitudes = [ + (gdf.loc[i + 1]["Z"] + gdf.loc[i]["Z"]) / 2 for i in range(len(gdf) - 1) + ] # Extracting XY coordinates - gdf_new = extract_xy(gdf=gdf, - drop_id=False, - reset_index=False) + gdf_new = extract_xy(gdf=gdf, drop_id=False, reset_index=False) # Creating empty list to store orientation values azimuths = [] # Calculating azimuth values - for i in range(len(gdf_new['id'].unique()) - 1): + for i in range(len(gdf_new["id"].unique()) - 1): # Get values for the first and second height - gdf_new1 = gdf_new[gdf_new['id'] == i + 1 + (gdf_new['id'].unique()[0] - 1)] - gdf_new2 = gdf_new[gdf_new['id'] == i + 2 + (gdf_new['id'].unique()[0] - 1)] + gdf_new1 = gdf_new[gdf_new["id"] == i + 1 + (gdf_new["id"].unique()[0] - 1)] + gdf_new2 = gdf_new[gdf_new["id"] == i + 2 + (gdf_new["id"].unique()[0] - 1)] # Convert coordinates to lists - gdf_new1_array = gdf_new1[['X', 'Y', 'Z']].values.tolist() - gdf_new2_array = gdf_new2[['X', 'Y', 'Z']].values.tolist() + gdf_new1_array = gdf_new1[["X", "Y", "Z"]].values.tolist() + gdf_new2_array = gdf_new2[["X", "Y", "Z"]].values.tolist() # Merge lists of points points = gdf_new1_array + gdf_new2_array @@ -7132,27 +7546,30 @@ def calculate_orientations_from_strike_lines(gdf: gpd.geodataframe.GeoDataFrame) # Convert vector to dip and azimuth sign_z = 1 if z > 0 else -1 - azimuth = (np.degrees(np.arctan2(sign_z * x, sign_z * y)) % 360) + azimuth = np.degrees(np.arctan2(sign_z * x, sign_z * y)) % 360 azimuths.append(azimuth) # Create new GeoDataFrame - gdf_orient = gpd.GeoDataFrame(data=pd.DataFrame(list(zip(dips, azimuths, altitudes))), - geometry=orientations_locations, - crs=gdf.crs) + gdf_orient = gpd.GeoDataFrame( + data=pd.DataFrame(list(zip(dips, azimuths, altitudes))), + geometry=orientations_locations, + crs=gdf.crs, + ) # Renaming Columns - gdf_orient.columns = ['dip', 'azimuth', 'Z', 'geometry'] + gdf_orient.columns = ["dip", "azimuth", "Z", "geometry"] # Setting polarity value - gdf_orient['polarity'] = 1 + gdf_orient["polarity"] = 1 # Appending remaining data of original GeoDataFrame - gdf_orient = gdf_orient.join(other=gdf.drop(labels=['geometry', 'Z'], axis=1).drop(gdf.tail(1).index)) + gdf_orient = gdf_orient.join( + other=gdf.drop(labels=["geometry", "Z"], axis=1).drop(gdf.tail(1).index) + ) # Extracting x and y coordinates of midpoints representing the location of orientation values - gdf_orient = extract_xy(gdf=gdf_orient, - reset_index=True) + gdf_orient = extract_xy(gdf=gdf_orient, reset_index=True) return gdf_orient @@ -7160,8 +7577,8 @@ def calculate_orientations_from_strike_lines(gdf: gpd.geodataframe.GeoDataFrame) # Loading GPX Files ################### -def load_gpx(path: str, - layer: Union[int, str] = 'tracks') -> Collection: + +def load_gpx(path: str, layer: Union[int, str] = "tracks") -> Collection: """Loading a GPX file as collection Parameters @@ -7208,34 +7625,34 @@ def load_gpx(path: str, import fiona except ModuleNotFoundError: raise ModuleNotFoundError( - 'fiona package is not installed. Use pip install fiona to install the latest version') + "fiona package is not installed. Use pip install fiona to install the latest version" + ) # Checking that the path is of type string if not isinstance(path, str): - raise TypeError('The path must be provided as string') + raise TypeError("The path must be provided as string") # Checking that the layer is of type int or string if not isinstance(layer, (int, str)): - raise TypeError('Layer must be provided as integer index or as string') + raise TypeError("Layer must be provided as integer index or as string") # Getting the absolute path path = os.path.abspath(path=path) if not os.path.exists(path): - raise LookupError('Invalid path provided') + raise LookupError("Invalid path provided") # Checking that the file has the correct file ending if not path.endswith(".gpx"): raise TypeError("The data must be provided as gpx file") # Opening the file - gpx = fiona.open(path, mode='r', layer=layer) + gpx = fiona.open(path, mode="r", layer=layer) return gpx -def load_gpx_as_dict(path: str, - layer: Union[int, str] = 'tracks') -> Collection: +def load_gpx_as_dict(path: str, layer: Union[int, str] = "tracks") -> Collection: """Loading a GPX file as dict Parameters @@ -7302,28 +7719,29 @@ def load_gpx_as_dict(path: str, import fiona except ModuleNotFoundError: raise ModuleNotFoundError( - 'fiona package is not installed. Use pip install fiona to install the latest version') + "fiona package is not installed. Use pip install fiona to install the latest version" + ) # Checking that the path is of type string if not isinstance(path, str): - raise TypeError('The path must be provided as string') + raise TypeError("The path must be provided as string") # Checking that the layer is of type int or string if not isinstance(layer, (int, str)): - raise TypeError('Layer must be provided as integer index or as string') + raise TypeError("Layer must be provided as integer index or as string") # Getting the absolute path path = os.path.abspath(path=path) if not os.path.exists(path): - raise LookupError('Invalid path provided') + raise LookupError("Invalid path provided") # Checking that the file has the correct file ending if not path.endswith(".gpx"): raise TypeError("The data must be provided as gpx file") # Opening the file - gpx = fiona.open(path, mode='r', layer=layer) + gpx = fiona.open(path, mode="r", layer=layer) # Extracting dict from Collection gpx_dict = gpx[0] @@ -7331,8 +7749,9 @@ def load_gpx_as_dict(path: str, return gpx_dict -def load_gpx_as_geometry(path: str, - layer: Union[int, str] = 'tracks') -> shapely.geometry.base.BaseGeometry: +def load_gpx_as_geometry( + path: str, layer: Union[int, str] = "tracks" +) -> shapely.geometry.base.BaseGeometry: """Loading a GPX file as Shapely Geometry Parameters @@ -7380,50 +7799,57 @@ def load_gpx_as_geometry(path: str, import fiona except ModuleNotFoundError: raise ModuleNotFoundError( - 'fiona package is not installed. Use pip install fiona to install the latest version') + "fiona package is not installed. Use pip install fiona to install the latest version" + ) # Checking that the path is of type string if not isinstance(path, str): - raise TypeError('The path must be provided as string') + raise TypeError("The path must be provided as string") # Checking that the layer is of type int or string if not isinstance(layer, (int, str)): - raise TypeError('Layer must be provided as integer index or as string') + raise TypeError("Layer must be provided as integer index or as string") # Getting the absolute path path = os.path.abspath(path=path) if not os.path.exists(path): - raise LookupError('Invalid path provided') + raise LookupError("Invalid path provided") # Checking that the file has the correct file ending if not path.endswith(".gpx"): raise TypeError("The data must be provided as gpx file") # Opening the file - gpx = fiona.open(path, mode='r', layer=layer) + gpx = fiona.open(path, mode="r", layer=layer) # Extracting dict from Collection gpx_dict = gpx[0] # Extracting Geometry Data - data = {'type': gpx_dict['geometry']['type'], - 'coordinates': gpx_dict['geometry']['coordinates']} + data = { + "type": gpx_dict["geometry"]["type"], + "coordinates": gpx_dict["geometry"]["coordinates"], + } # Creating BaseGeometry shape = shapely.geometry.shape(data) return shape -#def load_gpx_as_gdf(): + +# def load_gpx_as_gdf(): # Miscellaneous Functions ######################### -def sort_by_stratigraphy(gdf: gpd.geodataframe.GeoDataFrame, - stratigraphy: List[str], - formation_column: str = 'formation') -> gpd.geodataframe.GeoDataFrame: + +def sort_by_stratigraphy( + gdf: gpd.geodataframe.GeoDataFrame, + stratigraphy: List[str], + formation_column: str = "formation", +) -> gpd.geodataframe.GeoDataFrame: """Sorting a GeoDataFrame by a provided list of Stratigraphic Units Parameters @@ -7484,33 +7910,37 @@ def sort_by_stratigraphy(gdf: gpd.geodataframe.GeoDataFrame, # Checking that the input data is provided as GeoDataFrame if not isinstance(gdf, gpd.geodataframe.GeoDataFrame): - raise TypeError('Input Geometries must be stored as GeoDataFrame') + raise TypeError("Input Geometries must be stored as GeoDataFrame") # Checking that all GeoDataFrame entries are of type polygon - if not all(gdf.geom_type == 'Polygon'): - raise TypeError('All GeoDataFrame entries must be of geom_type polygon') + if not all(gdf.geom_type == "Polygon"): + raise TypeError("All GeoDataFrame entries must be of geom_type polygon") # Checking that all geometry objects are valid if not all(n.is_valid for n in gdf.geometry.tolist()): - raise ValueError('Not all geometry objects are valid') + raise ValueError("Not all geometry objects are valid") # Checking that no geometry object is empty if any(n.is_empty for n in gdf.geometry.tolist()): - raise ValueError('One or more geometry objects are empty') + raise ValueError("One or more geometry objects are empty") if not isinstance(formation_column, str): - raise TypeError('Formation column name must be of type string') + raise TypeError("Formation column name must be of type string") # Checking that the formation column is in the GeoDataFrame if formation_column not in gdf: - raise ValueError('Formation_column not present in gdf') + raise ValueError("Formation_column not present in gdf") - gdf['formation_cat'] = pd.Categorical(values=gdf[formation_column], - categories=stratigraphy, - ordered=True) + gdf["formation_cat"] = pd.Categorical( + values=gdf[formation_column], categories=stratigraphy, ordered=True + ) - gdf = gdf[gdf['formation_cat'].notna()] - gdf_sorted = gdf.sort_values(by='formation_cat').reset_index(drop=True).drop('formation_cat', axis=1) + gdf = gdf[gdf["formation_cat"].notna()] + gdf_sorted = ( + gdf.sort_values(by="formation_cat") + .reset_index(drop=True) + .drop("formation_cat", axis=1) + ) return gdf_sorted @@ -7550,25 +7980,27 @@ def create_bbox(extent: List[Union[int, float]]) -> shapely.geometry.polygon.Pol # Checking if extent is a list if not isinstance(extent, list): - raise TypeError('Extent must be of type list') + raise TypeError("Extent must be of type list") # Checking that all values are either ints or floats if not all(isinstance(n, (int, float)) for n in extent): - raise TypeError('Bounds values must be of type int or float') + raise TypeError("Bounds values must be of type int or float") bbox = geometry.box(extent[0], extent[2], extent[1], extent[3]) return bbox -def set_dtype(gdf: gpd.geodataframe.GeoDataFrame, - dip: str = 'dip', - azimuth: str = 'azimuth', - formation: str = 'formation', - polarity: str = 'polarity', - x: str = 'X', - y: str = 'Y', - z: str = 'Z') -> gpd.geodataframe.GeoDataFrame: +def set_dtype( + gdf: gpd.geodataframe.GeoDataFrame, + dip: str = "dip", + azimuth: str = "azimuth", + formation: str = "formation", + polarity: str = "polarity", + x: str = "X", + y: str = "Y", + z: str = "Z", +) -> gpd.geodataframe.GeoDataFrame: """Checking and setting the dtypes of the input data GeoDataFrame Parameters @@ -7621,31 +8053,39 @@ def set_dtype(gdf: gpd.geodataframe.GeoDataFrame, # Input object must be a GeoDataFrame if not isinstance(gdf, gpd.geodataframe.GeoDataFrame): - raise TypeError('Loaded object is not a GeoDataFrame') + raise TypeError("Loaded object is not a GeoDataFrame") # Checking that all elements of the input data is of type point if not all(gdf.geom_type == "Point"): - raise TypeError('Geometry type of input data must be og geom_type Points, please convert data beforehand') + raise TypeError( + "Geometry type of input data must be og geom_type Points, please convert data beforehand" + ) # Checking that all Shapely Objects are valid if not all(shapely.is_valid(gdf.geometry)): - raise ValueError('Not all Shapely Objects are valid objects') + raise ValueError("Not all Shapely Objects are valid objects") # Checking that no empty Shapely Objects are present if any(shapely.is_empty(gdf.geometry)): - raise ValueError('One or more Shapely objects are empty') + raise ValueError("One or more Shapely objects are empty") # Checking that the dip, azimuth and polarity column names are provided as string - if not isinstance(dip, str) and not isinstance(azimuth, str) and not isinstance(polarity, str): - raise TypeError('Dip, azimuth and polarity column names must be provided as string') + if ( + not isinstance(dip, str) + and not isinstance(azimuth, str) + and not isinstance(polarity, str) + ): + raise TypeError( + "Dip, azimuth and polarity column names must be provided as string" + ) # Checking that the formation column name is provided as string if not isinstance(formation, str): - raise TypeError('Formation column name must be provided as string') + raise TypeError("Formation column name must be provided as string") # Checking that the X, Y, Z column names are provided as string if not isinstance(x, str) and not isinstance(y, str) and not isinstance(z, str): - raise TypeError('X, Y, Z column names must be provided as string') + raise TypeError("X, Y, Z column names must be provided as string") # Converting dip column to floats if dip in gdf and gdf[dip].dtype != float: @@ -7678,10 +8118,11 @@ def set_dtype(gdf: gpd.geodataframe.GeoDataFrame, return gdf -def create_polygons_from_faces(mesh: pv.core.pointset.PolyData, - crs: Union[str, pyproj.crs.crs.CRS], - return_gdf: bool = True, - ) -> Union[List[shapely.geometry.polygon.Polygon], gpd.geodataframe.GeoDataFrame]: +def create_polygons_from_faces( + mesh: pv.core.pointset.PolyData, + crs: Union[str, pyproj.crs.crs.CRS], + return_gdf: bool = True, +) -> Union[List[shapely.geometry.polygon.Polygon], gpd.geodataframe.GeoDataFrame]: """Extracting faces from PyVista PolyData as Shapely Polygons Parameters @@ -7740,15 +8181,15 @@ def create_polygons_from_faces(mesh: pv.core.pointset.PolyData, # Checking that the input mesh is a PyVista PolyData dataset if not isinstance(mesh, pv.core.pointset.PolyData): - raise TypeError('Input mesh must be a PyVista PolyData dataset') + raise TypeError("Input mesh must be a PyVista PolyData dataset") # Checking that the crs is of type string or a pyproj object if not isinstance(crs, (str, type(None), pyproj.crs.crs.CRS)): - raise TypeError('target_crs must be of type string or a pyproj object') + raise TypeError("target_crs must be of type string or a pyproj object") # Checking that return gdfs is of type bool if not isinstance(return_gdf, bool): - raise TypeError('Return_gdf argument must be of type bool') + raise TypeError("Return_gdf argument must be of type bool") # Reshaping the faces array and selecting index values faces_indices = mesh.faces.reshape(mesh.n_faces, 4)[:, 1:] @@ -7766,10 +8207,13 @@ def create_polygons_from_faces(mesh: pv.core.pointset.PolyData, return polygons -def unify_polygons(polygons: Union[List[shapely.geometry.polygon.Polygon], gpd.geodataframe.GeoDataFrame], - crs: Union[str, pyproj.crs.crs.CRS] = None, - return_gdf: bool = True, - ) -> Union[List[shapely.geometry.polygon.Polygon], gpd.geodataframe.GeoDataFrame]: +def unify_polygons( + polygons: Union[ + List[shapely.geometry.polygon.Polygon], gpd.geodataframe.GeoDataFrame + ], + crs: Union[str, pyproj.crs.crs.CRS] = None, + return_gdf: bool = True, +) -> Union[List[shapely.geometry.polygon.Polygon], gpd.geodataframe.GeoDataFrame]: """Unifying adjacent triangular polygons to form larger objects Parameters @@ -7822,36 +8266,38 @@ def unify_polygons(polygons: Union[List[shapely.geometry.polygon.Polygon], gpd.g # Checking that the polygons are of type list of a GeoDataFrame if not isinstance(polygons, (list, gpd.geodataframe.GeoDataFrame)): - raise TypeError('Polygons must be provided as list of Shapely Polygons or as GeoDataFrame') + raise TypeError( + "Polygons must be provided as list of Shapely Polygons or as GeoDataFrame" + ) # Checking GeoDataFrame if isinstance(polygons, gpd.geodataframe.GeoDataFrame): # Check that all entries of the gdf are of type Polygon - if not all(polygons.geom_type == 'Polygon'): - raise TypeError('All GeoDataFrame entries must be of geom_type Polygon') + if not all(polygons.geom_type == "Polygon"): + raise TypeError("All GeoDataFrame entries must be of geom_type Polygon") # Checking that all Shapely Objects are valid if not all(shapely.is_valid(polygons.geometry)): - raise ValueError('Not all Shapely Objects are valid objects') + raise ValueError("Not all Shapely Objects are valid objects") # Checking that no empty Shapely Objects are present if any(shapely.is_empty(polygons.geometry)): - raise ValueError('One or more Shapely objects are empty') + raise ValueError("One or more Shapely objects are empty") # Storing CRS crs = polygons.crs # Creating list of geometries - polygons = polygons['geometry'].tolist() + polygons = polygons["geometry"].tolist() # Checking that the crs is of type string or a pyproj object if not isinstance(crs, (str, type(None), pyproj.crs.crs.CRS)): - raise TypeError('target_crs must be of type string or a pyproj object') + raise TypeError("target_crs must be of type string or a pyproj object") # Checking that return gdfs is of type bool if not isinstance(return_gdf, bool): - raise TypeError('Return_gdf argument must be of type bool') + raise TypeError("Return_gdf argument must be of type bool") # Creating MultiPolygon from Polygons multi_polygons = geometry.MultiPolygon(polygons) @@ -7864,16 +8310,18 @@ def unify_polygons(polygons: Union[List[shapely.geometry.polygon.Polygon], gpd.g # Creating GeoDataFrame if return_gdf: - polygons_merged = gpd.GeoDataFrame(geometry=polygons_merged, - crs=crs) + polygons_merged = gpd.GeoDataFrame(geometry=polygons_merged, crs=crs) return polygons_merged -def unify_linestrings(linestrings: Union[List[shapely.geometry.linestring.LineString], gpd.geodataframe.GeoDataFrame], - crs: Union[str, pyproj.crs.crs.CRS] = None, - return_gdf: bool = True - ) -> Union[List[shapely.geometry.linestring.LineString], gpd.geodataframe.GeoDataFrame]: +def unify_linestrings( + linestrings: Union[ + List[shapely.geometry.linestring.LineString], gpd.geodataframe.GeoDataFrame + ], + crs: Union[str, pyproj.crs.crs.CRS] = None, + return_gdf: bool = True, +) -> Union[List[shapely.geometry.linestring.LineString], gpd.geodataframe.GeoDataFrame]: """Unifying adjacent LineStrings to form LineStrings with multiple vertices Parameters @@ -7926,36 +8374,38 @@ def unify_linestrings(linestrings: Union[List[shapely.geometry.linestring.LineSt # Checking that the linestrings are of type list of a GeoDataFrame if not isinstance(linestrings, (list, gpd.geodataframe.GeoDataFrame)): - raise TypeError('Polygons must be provided as list of Shapely Polygons or as GeoDataFrame') + raise TypeError( + "Polygons must be provided as list of Shapely Polygons or as GeoDataFrame" + ) # Checking GeoDataFrame if isinstance(linestrings, gpd.geodataframe.GeoDataFrame): # Check that all entries of the gdf are of type LineString - if not all(linestrings.geom_type == 'LineString'): - raise TypeError('All GeoDataFrame entries must be of geom_type LineString') + if not all(linestrings.geom_type == "LineString"): + raise TypeError("All GeoDataFrame entries must be of geom_type LineString") # Checking that all Shapely Objects are valid if not all(shapely.is_valid(linestrings.geometry)): - raise ValueError('Not all Shapely Objects are valid objects') + raise ValueError("Not all Shapely Objects are valid objects") # Checking that no empty Shapely Objects are present if any(shapely.is_empty(linestrings.geometry)): - raise ValueError('One or more Shapely objects are empty') + raise ValueError("One or more Shapely objects are empty") # Storing CRS crs = linestrings.crs # Creating list of geometries - linestrings = linestrings['geometry'].tolist() + linestrings = linestrings["geometry"].tolist() # Checking that the crs is of type string or a pyproj object if not isinstance(crs, (str, type(None), pyproj.crs.crs.CRS)): - raise TypeError('target_crs must be of type string or a pyproj object') + raise TypeError("target_crs must be of type string or a pyproj object") # Checking that return gdfs is of type bool if not isinstance(return_gdf, bool): - raise TypeError('Return_gdf argument must be of type bool') + raise TypeError("Return_gdf argument must be of type bool") # Unifying LineStrings unified_linestrings = ops.linemerge(lines=linestrings) @@ -7965,18 +8415,18 @@ def unify_linestrings(linestrings: Union[List[shapely.geometry.linestring.LineSt # Creating GeoDataFrame if return_gdf: - linestrings_merged = gpd.GeoDataFrame(geometry=linestrings_merged, - crs=crs) + linestrings_merged = gpd.GeoDataFrame(geometry=linestrings_merged, crs=crs) # Adding Z values as column - linestrings_merged['Z'] = [list(linestrings_merged.loc[i].geometry.coords)[0][2] for i in - range(len(linestrings_merged))] + linestrings_merged["Z"] = [ + list(linestrings_merged.loc[i].geometry.coords)[0][2] + for i in range(len(linestrings_merged)) + ] return linestrings_merged -def create_hexagon(center: shapely.geometry.Point, - radius: Union[int, float]): +def create_hexagon(center: shapely.geometry.Point, radius: Union[int, float]): """Function to create one hexagon Parameters @@ -8008,17 +8458,14 @@ def create_hexagon(center: shapely.geometry.Point, # Checking that the center point is provided as Shapely Point if not isinstance(center, geometry.Point): - raise TypeError('Center point of the hexagon must be provided as Shapely Point') + raise TypeError("Center point of the hexagon must be provided as Shapely Point") # Checking that the radius is of type int or float if not isinstance(radius, (int, float)): - raise TypeError('Radius of the hexagon must be provided as int or float') + raise TypeError("Radius of the hexagon must be provided as int or float") # Setting the hexagon angles - angles = np.linspace(start=0, - stop=2*np.pi, - num=6, - endpoint=False) + angles = np.linspace(start=0, stop=2 * np.pi, num=6, endpoint=False) # Calculating the coordinates of the hexagon's vertices x_coords = center.x + radius * np.cos(angles) @@ -8028,9 +8475,9 @@ def create_hexagon(center: shapely.geometry.Point, return geometry.Polygon(np.c_[x_coords, y_coords]) -def create_hexagon_grid(gdf: gpd.GeoDataFrame, - radius: Union[int, float], - crop_gdf: bool = True): +def create_hexagon_grid( + gdf: gpd.GeoDataFrame, radius: Union[int, float], crop_gdf: bool = True +): """Function to create a grid of hexagons based on a GeoDataFrame containing Polygons and a radius provided for the single hexagons Parameters @@ -8066,24 +8513,31 @@ def create_hexagon_grid(gdf: gpd.GeoDataFrame, # Checking that the gdf is of type GeoDataFrame if not isinstance(gdf, gpd.GeoDataFrame): - raise TypeError('gdf must be of type GeoDataFrame') + raise TypeError("gdf must be of type GeoDataFrame") # Checking - if not all(gdf.geom_type == 'Polygon'): - raise TypeError('All geometries in the gdf must be of geom_type Polygon') + if not all(gdf.geom_type == "Polygon"): + raise TypeError("All geometries in the gdf must be of geom_type Polygon") # Checking that the radius is of type int or float if not isinstance(radius, (int, float)): - raise TypeError('radius must be of type int or float') + raise TypeError("radius must be of type int or float") # Checking that crop_gdf is of type bool if not isinstance(crop_gdf, bool): - raise TypeError('crop_gdf must be either set to True or False') + raise TypeError("crop_gdf must be either set to True or False") # Calculating the number of rows and columns of the hexagon grid - columns = int(np.ceil((gdf.total_bounds[2] - gdf.total_bounds[0]) / (1.5 * radius)) + 1) + columns = int( + np.ceil((gdf.total_bounds[2] - gdf.total_bounds[0]) / (1.5 * radius)) + 1 + ) - rows = int(np.ceil((gdf.total_bounds[3] - gdf.total_bounds[1]) / (2 * np.sqrt(3) / 2 * radius)) + 1) + rows = int( + np.ceil( + (gdf.total_bounds[3] - gdf.total_bounds[1]) / (2 * np.sqrt(3) / 2 * radius) + ) + + 1 + ) # Creating emtpy lists to store the x and y coordinates of the centers of the hexagons x_coords = [] @@ -8097,33 +8551,35 @@ def create_hexagon_grid(gdf: gpd.GeoDataFrame, y_coord = gdf.total_bounds[3] - 2 * j * radius * (np.sqrt(3) / 2) else: x_coord = gdf.total_bounds[0] + i * radius * 1.5 - y_coord = gdf.total_bounds[3] - 2 * j * radius * (np.sqrt(3) / 2) - (np.sqrt(3) / 2) * radius + y_coord = ( + gdf.total_bounds[3] + - 2 * j * radius * (np.sqrt(3) / 2) + - (np.sqrt(3) / 2) * radius + ) # Appending coordinates to lists x_coords.append(x_coord) y_coords.append(y_coord) # Creating a list of Shapely Points representing the centers of the Hexagons - list_points = [geometry.Point(x, - y) for x, y in zip(x_coords, - y_coords)] + list_points = [geometry.Point(x, y) for x, y in zip(x_coords, y_coords)] # Creating the hexagon grid from the list of center points - list_hexagon = [create_hexagon(point, - radius) for point in list_points] + list_hexagon = [create_hexagon(point, radius) for point in list_points] # Creating GeoDataFrame from list of hexagons - hex_gdf = gpd.GeoDataFrame(geometry=list_hexagon, - crs=gdf.crs) + hex_gdf = gpd.GeoDataFrame(geometry=list_hexagon, crs=gdf.crs) # Cropping the GeoDataFrame to the outline if crop_gdf: - hex_gdf = hex_gdf.sjoin(gdf).reset_index()[['geometry']] + hex_gdf = hex_gdf.sjoin(gdf).reset_index()[["geometry"]] return hex_gdf -def create_voronoi_polygons(gdf: gpd.geodataframe.GeoDataFrame) -> gpd.geodataframe.GeoDataFrame: +def create_voronoi_polygons( + gdf: gpd.geodataframe.GeoDataFrame, +) -> gpd.geodataframe.GeoDataFrame: """Function to create Voronoi Polygons from Point GeoDataFrame using the SciPy Spatial Voronoi class (https://docs.scipy.org/doc/scipy/reference/generated/scipy.spatial.Voronoi.html#scipy.spatial.Voronoi) @@ -8155,25 +8611,26 @@ def create_voronoi_polygons(gdf: gpd.geodataframe.GeoDataFrame) -> gpd.geodatafr # Checking that the gdf is of type GeoDataFrame if not isinstance(gdf, gpd.geodataframe.GeoDataFrame): - raise TypeError('gdf must be provided as GeoDataFrame') + raise TypeError("gdf must be provided as GeoDataFrame") # Checking that all geometry objects of the GeoDataFrame are of type Point if not all(shapely.get_type_id(gdf.geometry) == 0): - raise TypeError('All GeoDataFrame entries must be of geom_type Point') + raise TypeError("All GeoDataFrame entries must be of geom_type Point") # Trying to import scipy but returning error if scipy is not installed try: from scipy.spatial import Voronoi except ModuleNotFoundError: raise ModuleNotFoundError( - 'SciPy package is not installed. Use pip install scipy to install the latest version') + "SciPy package is not installed. Use pip install scipy to install the latest version" + ) # Checking if X and Y coordinates are in GeoDataFrame - if not {'X', 'Y'}.issubset(gdf.columns): + if not {"X", "Y"}.issubset(gdf.columns): gdf = extract_xy(gdf) # Getting Points from GeoDataFrame - points = gdf[['X', 'Y']].values + points = gdf[["X", "Y"]].values # Creating Voronoi vertices and regions vor = Voronoi(points) @@ -8185,13 +8642,12 @@ def create_voronoi_polygons(gdf: gpd.geodataframe.GeoDataFrame) -> gpd.geodatafr polygons = [geometry.Polygon(vor.vertices[regions[i]]) for i in range(len(regions))] # Creating GeoDataFrame - gdf_polygons = gpd.GeoDataFrame(geometry=polygons, - crs=gdf.crs) + gdf_polygons = gpd.GeoDataFrame(geometry=polygons, crs=gdf.crs) # Removing empty Polygons gdf_polygons = gdf_polygons[~gdf_polygons.is_empty] # Calculating and appending area to GeoDataFrame - gdf_polygons['area'] = gdf_polygons.area + gdf_polygons["area"] = gdf_polygons.area return gdf_polygons From 43daf0d5b105a73edf126f2857828f900dd16588 Mon Sep 17 00:00:00 2001 From: AlexanderJuestel Date: Mon, 22 Jul 2024 20:48:09 +0200 Subject: [PATCH 17/63] Format utils.py --- gemgis/utils.py | 1149 ++++++++++++++++++++++++++++------------------- 1 file changed, 690 insertions(+), 459 deletions(-) diff --git a/gemgis/utils.py b/gemgis/utils.py index 43884219..4bc5f0d7 100644 --- a/gemgis/utils.py +++ b/gemgis/utils.py @@ -37,9 +37,11 @@ __all__ = [series, crs] -def to_section_dict(gdf: gpd.geodataframe.GeoDataFrame, - section_column: str = 'section_name', - resolution: List[int] = None) -> dict: +def to_section_dict( + gdf: gpd.geodataframe.GeoDataFrame, + section_column: str = "section_name", + resolution: List[int] = None, +) -> dict: """Converting custom sections stored in Shape files to GemPy section_dicts Parameters @@ -85,49 +87,73 @@ def to_section_dict(gdf: gpd.geodataframe.GeoDataFrame, # Checking if gdf is of type GeoDataFrame if not isinstance(gdf, gpd.geodataframe.GeoDataFrame): - raise TypeError('gdf must be of type GeoDataFrame') + raise TypeError("gdf must be of type GeoDataFrame") # Checking if the section_column is of type string if not isinstance(section_column, str): - raise TypeError('Name for section_column must be of type string') + raise TypeError("Name for section_column must be of type string") # Checking if resolution is of type list if not isinstance(resolution, (list, type(None))): - raise TypeError('Resolution must be of type list') + raise TypeError("Resolution must be of type list") # Setting resolution if resolution is None: resolution = [100, 80] # Checking if X and Y values are in column - if not {'X', 'Y'}.issubset(gdf.columns): + if not {"X", "Y"}.issubset(gdf.columns): gdf = vector.extract_xy(gdf) # Checking the length of the resolution list if len(resolution) != 2: - raise ValueError('resolution list must be of length two') + raise ValueError("resolution list must be of length two") # Extracting Section names section_names = gdf[section_column].unique() # Create section dicts for Point Shape Files if all(gdf.geom_type == "Point"): - section_dict = {i: ([gdf[gdf[section_column] == i].X.iloc[0], gdf[gdf[section_column] == i].Y.iloc[0]], - [gdf[gdf[section_column] == i].X.iloc[1], gdf[gdf[section_column] == i].Y.iloc[1]], - resolution) for i in section_names} + section_dict = { + i: ( + [ + gdf[gdf[section_column] == i].X.iloc[0], + gdf[gdf[section_column] == i].Y.iloc[0], + ], + [ + gdf[gdf[section_column] == i].X.iloc[1], + gdf[gdf[section_column] == i].Y.iloc[1], + ], + resolution, + ) + for i in section_names + } # Create section dicts for Line Shape Files else: - section_dict = {i: ([gdf[gdf[section_column] == i].X.iloc[0], gdf[gdf[section_column] == i].Y.iloc[0]], - [gdf[gdf[section_column] == i].X.iloc[1], gdf[gdf[section_column] == i].Y.iloc[1]], - resolution) for i in section_names} + section_dict = { + i: ( + [ + gdf[gdf[section_column] == i].X.iloc[0], + gdf[gdf[section_column] == i].Y.iloc[0], + ], + [ + gdf[gdf[section_column] == i].X.iloc[1], + gdf[gdf[section_column] == i].Y.iloc[1], + ], + resolution, + ) + for i in section_names + } return section_dict -def convert_to_gempy_df(gdf: gpd.geodataframe.GeoDataFrame, - dem: Union[rasterio.io.DatasetReader, np.ndarray] = None, - extent: List[Union[float, int]] = None) -> pd.DataFrame: +def convert_to_gempy_df( + gdf: gpd.geodataframe.GeoDataFrame, + dem: Union[rasterio.io.DatasetReader, np.ndarray] = None, + extent: List[Union[float, int]] = None, +) -> pd.DataFrame: """Converting a GeoDataFrame into a Pandas DataFrame ready to be read in for GemPy Parameters @@ -190,80 +216,81 @@ def convert_to_gempy_df(gdf: gpd.geodataframe.GeoDataFrame, # Checking if gdf is of type GeoDataFrame if not isinstance(gdf, gpd.geodataframe.GeoDataFrame): - raise TypeError('gdf must be of type GeoDataFrame') + raise TypeError("gdf must be of type GeoDataFrame") # Checking that the dem is a np.ndarray if not isinstance(dem, (np.ndarray, rasterio.io.DatasetReader, type(None))): - raise TypeError('DEM must be a numpy.ndarray or rasterio object') + raise TypeError("DEM must be a numpy.ndarray or rasterio object") # Checking if extent is a list if not isinstance(extent, (list, type(None))): - raise TypeError('Extent must be of type list') + raise TypeError("Extent must be of type list") # Extracting Coordinates from gdf - if not {'X', 'Y', 'Z'}.issubset(gdf.columns): + if not {"X", "Y", "Z"}.issubset(gdf.columns): # Extracting coordinates from array if isinstance(dem, np.ndarray): if not isinstance(extent, type(None)): - gdf = vector.extract_xyz(gdf=gdf, - dem=dem, - extent=extent) + gdf = vector.extract_xyz(gdf=gdf, dem=dem, extent=extent) else: - raise FileNotFoundError('Extent not provided') + raise FileNotFoundError("Extent not provided") # Extracting coordinates from raster elif isinstance(dem, rasterio.io.DatasetReader): - gdf = vector.extract_xyz(gdf=gdf, - dem=dem) + gdf = vector.extract_xyz(gdf=gdf, dem=dem) else: - raise FileNotFoundError('DEM not provided') + raise FileNotFoundError("DEM not provided") # Checking if the formation column is in the gdf and setting type - if 'formation' not in gdf: - raise ValueError('Formation names not defined') + if "formation" not in gdf: + raise ValueError("Formation names not defined") else: - gdf['formation'] = gdf['formation'].astype(str) + gdf["formation"] = gdf["formation"].astype(str) # Checking if the dip column is in the gdf and setting type - if 'dip' in gdf: - gdf['dip'] = gdf['dip'].astype(float) + if "dip" in gdf: + gdf["dip"] = gdf["dip"].astype(float) # Checking if the azimuth column is in the gdf and setting type - if 'azimuth' in gdf: - gdf['azimuth'] = gdf['azimuth'].astype(float) + if "azimuth" in gdf: + gdf["azimuth"] = gdf["azimuth"].astype(float) # Checking if DataFrame is an orientation or interfaces df - if 'dip' in gdf: + if "dip" in gdf: - if (gdf['dip'] > 90).any(): - raise ValueError('dip values exceed 90 degrees') - if 'azimuth' not in gdf: - raise ValueError('azimuth values not defined') - if (gdf['azimuth'] > 360).any(): - raise ValueError('azimuth values exceed 360 degrees') + if (gdf["dip"] > 90).any(): + raise ValueError("dip values exceed 90 degrees") + if "azimuth" not in gdf: + raise ValueError("azimuth values not defined") + if (gdf["azimuth"] > 360).any(): + raise ValueError("azimuth values exceed 360 degrees") # Create orientations dataframe - if 'polarity' not in gdf: - df = pd.DataFrame(gdf[['X', 'Y', 'Z', 'formation', 'dip', 'azimuth']]) - df['polarity'] = 1 + if "polarity" not in gdf: + df = pd.DataFrame(gdf[["X", "Y", "Z", "formation", "dip", "azimuth"]]) + df["polarity"] = 1 return df else: - df = pd.DataFrame(gdf[['X', 'Y', 'Z', 'formation', 'dip', 'azimuth', 'polarity']]) + df = pd.DataFrame( + gdf[["X", "Y", "Z", "formation", "dip", "azimuth", "polarity"]] + ) return df else: # Create interfaces dataframe - df = pd.DataFrame(gdf[['X', 'Y', 'Z', 'formation']]) + df = pd.DataFrame(gdf[["X", "Y", "Z", "formation"]]) return df -def set_extent(minx: Union[int, float] = 0, - maxx: Union[int, float] = 0, - miny: Union[int, float] = 0, - maxy: Union[int, float] = 0, - minz: Union[int, float] = 0, - maxz: Union[int, float] = 0, - gdf: gpd.geodataframe.GeoDataFrame = None) -> List[Union[int, float]]: +def set_extent( + minx: Union[int, float] = 0, + maxx: Union[int, float] = 0, + miny: Union[int, float] = 0, + maxy: Union[int, float] = 0, + minz: Union[int, float] = 0, + maxz: Union[int, float] = 0, + gdf: gpd.geodataframe.GeoDataFrame = None, +) -> List[Union[int, float]]: """Setting the extent for a model Parameters @@ -311,11 +338,13 @@ def set_extent(minx: Union[int, float] = 0, # Checking that the GeoDataFrame is a gdf or of type None if not isinstance(gdf, (type(None), gpd.geodataframe.GeoDataFrame)): - raise TypeError('gdf must be of type GeoDataFrame') + raise TypeError("gdf must be of type GeoDataFrame") # Checking if bounds are of type int or float - if not all(isinstance(i, (int, float)) for i in [minx, maxx, miny, maxy, minz, maxz]): - raise TypeError('bounds must be of type int or float') + if not all( + isinstance(i, (int, float)) for i in [minx, maxx, miny, maxy, minz, maxz] + ): + raise TypeError("bounds must be of type int or float") # Checking if the gdf is of type None if isinstance(gdf, type(None)): @@ -334,15 +363,17 @@ def set_extent(minx: Union[int, float] = 0, # Create extent from gdf of geom_type point or linestring else: bounds = gdf.bounds - extent = [round(bounds.minx.min(), 2), round(bounds.maxx.max(), 2), round(bounds.miny.min(), 2), - round(bounds.maxy.max(), 2)] + extent = [ + round(bounds.minx.min(), 2), + round(bounds.maxx.max(), 2), + round(bounds.miny.min(), 2), + round(bounds.maxy.max(), 2), + ] return extent -def set_resolution(x: int, - y: int, - z: int) -> List[int]: +def set_resolution(x: int, y: int, z: int) -> List[int]: """Setting the resolution for a model Parameters @@ -378,15 +409,15 @@ def set_resolution(x: int, # Checking if x is of type int if not isinstance(x, int): - raise TypeError('X must be of type int') + raise TypeError("X must be of type int") # Checking if y is of type int if not isinstance(y, int): - raise TypeError('Y must be of type int') + raise TypeError("Y must be of type int") # Checking if y is of type int if not isinstance(z, int): - raise TypeError('Z must be of type int') + raise TypeError("Z must be of type int") # Create list of resolution values resolution = [x, y, z] @@ -394,12 +425,14 @@ def set_resolution(x: int, return resolution -def read_csv_as_gdf(path: str, - crs: Union[str, pyproj.crs.crs.CRS], - x: str = 'X', - y: str = 'Y', - z: str = None, - delimiter: str = ',') -> gpd.geodataframe.GeoDataFrame: +def read_csv_as_gdf( + path: str, + crs: Union[str, pyproj.crs.crs.CRS], + x: str = "X", + y: str = "Y", + z: str = None, + delimiter: str = ",", +) -> gpd.geodataframe.GeoDataFrame: """Reading CSV files as GeoDataFrame Parameters @@ -449,7 +482,7 @@ def read_csv_as_gdf(path: str, # Checking that the path is of type string if not isinstance(path, str): - raise TypeError('Path must be provided as string') + raise TypeError("Path must be provided as string") # Getting the absolute path path = os.path.abspath(path=path) @@ -460,45 +493,43 @@ def read_csv_as_gdf(path: str, # Checking that the file exists if not os.path.exists(path): - raise FileNotFoundError('File not found') + raise FileNotFoundError("File not found") # Checking that the x column is of type string if not isinstance(x, str): - raise TypeError('X column name must be provided as string') + raise TypeError("X column name must be provided as string") # Checking that the y column is of type string if not isinstance(y, str): - raise TypeError('Y column name must be provided as string') + raise TypeError("Y column name must be provided as string") # Checking that the z column is of type string if not isinstance(z, (str, type(None))): - raise TypeError('Z column name must be provided as string') + raise TypeError("Z column name must be provided as string") # Checking that the crs is provided as string if not isinstance(crs, (str, pyproj.crs.crs.CRS)): - raise TypeError('CRS must be provided as string or pyproj CRS object') + raise TypeError("CRS must be provided as string or pyproj CRS object") # Checking that the delimiter is of type string if not isinstance(delimiter, str): - raise TypeError('Delimiter must be of type string') + raise TypeError("Delimiter must be of type string") # Loading the csv file - df = pd.read_csv(filepath_or_buffer=path, - sep=delimiter) + df = pd.read_csv(filepath_or_buffer=path, sep=delimiter) # Checking that the file loaded is a DataFrame if not isinstance(df, pd.DataFrame): - raise TypeError('df must be of type DataFrame') + raise TypeError("df must be of type DataFrame") # Create GeoDataFrame if (x in df) and (y in df): - gdf = gpd.GeoDataFrame(data=df, - geometry=gpd.points_from_xy(x=df[x], - y=df[y], - crs=crs)) + gdf = gpd.GeoDataFrame( + data=df, geometry=gpd.points_from_xy(x=df[x], y=df[y], crs=crs) + ) else: - raise ValueError('X and/or Y columns could not be found') + raise ValueError("X and/or Y columns could not be found") return gdf @@ -541,9 +572,9 @@ def show_number_of_data_points(geo_model): """ # Trying to import gempy but returning error if gempy is not installed - #try: + # try: # import gempy as gp - #except ModuleNotFoundError: + # except ModuleNotFoundError: # raise ModuleNotFoundError( # 'GemPy package is not installed. Use pip install gempy to install the latest version') @@ -553,26 +584,32 @@ def show_number_of_data_points(geo_model): # Store values of number of interfaces and orientations in list for i in geo_model.surfaces.df.surface.unique(): - length = len(geo_model.surface_points.df[geo_model.surface_points.df['surface'] == i]) + length = len( + geo_model.surface_points.df[geo_model.surface_points.df["surface"] == i] + ) no_int.append(length) - length = len(geo_model.orientations.df[geo_model.orientations.df['surface'] == i]) + length = len( + geo_model.orientations.df[geo_model.orientations.df["surface"] == i] + ) no_ori.append(length) # Copying GeoDataFrame gdf = geo_model.surfaces.df.copy(deep=True) # Add columns to geo_model surface table - gdf['No. of Interfaces'] = no_int - gdf['No. of Orientations'] = no_ori + gdf["No. of Interfaces"] = no_int + gdf["No. of Orientations"] = no_ori return gdf -def getfeatures(extent: Union[List[Union[int, float]], type(None)], - crs_raster: Union[str, dict], - crs_bbox: Union[str, dict], - bbox: shapely.geometry.polygon.Polygon = None) -> list: +def getfeatures( + extent: Union[List[Union[int, float]], type(None)], + crs_raster: Union[str, dict], + crs_bbox: Union[str, dict], + bbox: shapely.geometry.polygon.Polygon = None, +) -> list: """Creating a list containing a dict with keys and values to clip a raster Parameters @@ -603,23 +640,23 @@ def getfeatures(extent: Union[List[Union[int, float]], type(None)], # Checking if extent is of type list if not isinstance(extent, (list, type(None))): - raise TypeError('Extent must be of type list') + raise TypeError("Extent must be of type list") # Checking if bounds are of type int or float if not all(isinstance(n, (int, float)) for n in extent): - raise TypeError('Bounds must be of type int or float') + raise TypeError("Bounds must be of type int or float") # Checking if the raster crs is of type string or dict if not isinstance(crs_raster, (str, dict, rasterio.crs.CRS)): - raise TypeError('Raster CRS must be of type dict or string') + raise TypeError("Raster CRS must be of type dict or string") # Checking if the bbox crs is of type string or dict if not isinstance(crs_bbox, (str, dict, rasterio.crs.CRS)): - raise TypeError('Bbox CRS must be of type dict or string') + raise TypeError("Bbox CRS must be of type dict or string") # Checking if the bbox is of type none or a shapely polygon if not isinstance(bbox, (shapely.geometry.polygon.Polygon, type(None))): - raise TypeError('Bbox must be a shapely polygon') + raise TypeError("Bbox must be a shapely polygon") # Create bbox if bbox is not provided if isinstance(bbox, type(None)): @@ -628,7 +665,7 @@ def getfeatures(extent: Union[List[Union[int, float]], type(None)], # Checking if the bbox is a shapely box if not isinstance(bbox, shapely.geometry.polygon.Polygon): - raise TypeError('Bbox is not of type shapely box') + raise TypeError("Bbox is not of type shapely box") # Converting to dict if isinstance(crs_raster, rasterio.crs.CRS): @@ -639,17 +676,17 @@ def getfeatures(extent: Union[List[Union[int, float]], type(None)], # Converting raster crs to dict if isinstance(crs_raster, str): - crs_raster = {'init': crs_raster} + crs_raster = {"init": crs_raster} # Converting bbox raster to dict if isinstance(crs_bbox, str): - crs_bbox = {'init': crs_bbox} + crs_bbox = {"init": crs_bbox} # Creating GeoDataFrame - gdf = gpd.GeoDataFrame({'geometry': bbox}, index=[0], crs=crs_bbox) + gdf = gpd.GeoDataFrame({"geometry": bbox}, index=[0], crs=crs_bbox) gdf = gdf.to_crs(crs=crs_raster) - data = [json.loads(gdf.to_json())['features'][0]['geometry']] + data = [json.loads(gdf.to_json())["features"][0]["geometry"]] return data @@ -657,6 +694,7 @@ def getfeatures(extent: Union[List[Union[int, float]], type(None)], # Parsing QGIS Style Files ######################## + def parse_categorized_qml(qml_name: str) -> tuple: """Parsing a QGIS style file to retrieve surface color values @@ -714,11 +752,12 @@ def parse_categorized_qml(qml_name: str) -> tuple: import xmltodict except ModuleNotFoundError: raise ModuleNotFoundError( - 'xmltodict package is not installed. Use pip install xmltodict to install the latest version') + "xmltodict package is not installed. Use pip install xmltodict to install the latest version" + ) # Checking if the path was provided as string if not isinstance(qml_name, str): - raise TypeError('Path must be of type string') + raise TypeError("Path must be of type string") # Getting the absolute path path = os.path.abspath(path=qml_name) @@ -729,7 +768,7 @@ def parse_categorized_qml(qml_name: str) -> tuple: # Checking that the file exists if not os.path.exists(path): - raise FileNotFoundError('File not found') + raise FileNotFoundError("File not found") # Opening the file with open(qml_name, "rb") as f: @@ -740,15 +779,13 @@ def parse_categorized_qml(qml_name: str) -> tuple: # Extracting symbols symbols = { - symbol["@name"]: { - prop["@k"]: prop["@v"] for prop in symbol["layer"]["prop"] - } + symbol["@name"]: {prop["@k"]: prop["@v"] for prop in symbol["layer"]["prop"]} for symbol in qml["qgis"]["renderer-v2"]["symbols"]["symbol"] } # Extracting styles classes = { - category['@value']: symbols[category['@symbol']] + category["@value"]: symbols[category["@symbol"]] for category in qml["qgis"]["renderer-v2"]["categories"]["category"] } @@ -816,7 +853,7 @@ def build_style_dict(classes: dict) -> dict: # Checking if classes is of type dict if not isinstance(classes, dict): - raise TypeError('Classes must be of type dict') + raise TypeError("Classes must be of type dict") # Create empty styles dict styles_dict = {} @@ -832,14 +869,13 @@ def build_style_dict(classes: dict) -> dict: "opacity": opacity / 255, "weight": float(style["outline_width"]), "fillColor": f"#{fillColor[0]:02x}{fillColor[1]:02x}{fillColor[2]:02x}", - "fillOpacity": fill_opacity / 255 + "fillOpacity": fill_opacity / 255, } return styles_dict -def load_surface_colors(path: str, - gdf: gpd.geodataframe.GeoDataFrame) -> List[str]: +def load_surface_colors(path: str, gdf: gpd.geodataframe.GeoDataFrame) -> List[str]: """Loading surface colors from a QML file and storing the color values as list to be displayed with GeoPandas plots Parameters @@ -883,7 +919,7 @@ def load_surface_colors(path: str, # Checking that the path is of type str if not isinstance(path, str): - raise TypeError('path must be provided as string') + raise TypeError("path must be provided as string") # Getting the absolute path path = os.path.abspath(path=path) @@ -894,11 +930,11 @@ def load_surface_colors(path: str, # Checking that the file exists if not os.path.exists(path): - raise FileNotFoundError('File not found') + raise FileNotFoundError("File not found") # Checking that the gdf is of type GeoDataFrame if not isinstance(gdf, gpd.geodataframe.GeoDataFrame): - raise TypeError('object must be of type GeoDataFrame') + raise TypeError("object must be of type GeoDataFrame") # Parse qml column, classes = parse_categorized_qml(qml_name=path) @@ -919,7 +955,7 @@ def load_surface_colors(path: str, gdf_copy = gdf_copy.groupby([column], as_index=False).last() # Create list of remaining colors - cols = gdf_copy['Color'].to_list() + cols = gdf_copy["Color"].to_list() return cols @@ -961,7 +997,7 @@ def create_surface_color_dict(path: str) -> dict: # Checking that the path is of type str if not isinstance(path, str): - raise TypeError('path must be provided as string') + raise TypeError("path must be provided as string") # Getting the absolute path path = os.path.abspath(path=path) @@ -972,7 +1008,7 @@ def create_surface_color_dict(path: str) -> dict: # Checking that the file exists if not os.path.exists(path): - raise FileNotFoundError('File not found') + raise FileNotFoundError("File not found") # Parse qml columns, classes = parse_categorized_qml(qml_name=path) @@ -1030,11 +1066,12 @@ def get_location_coordinate(name: str): import geopy except ModuleNotFoundError: raise ModuleNotFoundError( - 'GeoPy package is not installed. Use pip install geopy to install the latest version') + "GeoPy package is not installed. Use pip install geopy to install the latest version" + ) # Checking that the location name is of type string if not isinstance(name, str): - raise TypeError('Location name must be of type string') + raise TypeError("Location name must be of type string") # Create geocoder for OpenStreetMap data geolocator = geopy.geocoders.Nominatim(user_agent=name) @@ -1045,8 +1082,9 @@ def get_location_coordinate(name: str): return coordinates -def transform_location_coordinate(coordinates, - crs: Union[str, pyproj.crs.crs.CRS]) -> dict: +def transform_location_coordinate( + coordinates, crs: Union[str, pyproj.crs.crs.CRS] +) -> dict: """Transforming coordinates of GeoPy Location Parameters @@ -1097,18 +1135,19 @@ def transform_location_coordinate(coordinates, import geopy except ModuleNotFoundError: raise ModuleNotFoundError( - 'GeoPy package is not installed. Use pip install geopy to install the latest version') + "GeoPy package is not installed. Use pip install geopy to install the latest version" + ) # Checking that coordinates object is a GeoPy location object if not isinstance(coordinates, geopy.location.Location): - raise TypeError('The location must be provided as GeoPy Location object') + raise TypeError("The location must be provided as GeoPy Location object") # Checking that the target crs is provided as string if not isinstance(crs, str): - raise TypeError('Target CRS must be of type string') + raise TypeError("Target CRS must be of type string") # Setting source and target projection - transformer = pyproj.Transformer.from_crs('EPSG:4326', crs) + transformer = pyproj.Transformer.from_crs("EPSG:4326", crs) # Transforming coordinate systems long, lat = transformer.transform(coordinates.latitude, coordinates.longitude) @@ -1164,21 +1203,27 @@ def create_polygon_from_location(coordinates) -> shapely.geometry.polygon.Polygo import geopy except ModuleNotFoundError: raise ModuleNotFoundError( - 'GeoPy package is not installed. Use pip install geopy to install the latest version') + "GeoPy package is not installed. Use pip install geopy to install the latest version" + ) # Checking that coordinates object is a GeoPy location object if not isinstance(coordinates, geopy.location.Location): - raise TypeError('The location must be provided as GeoPy Location object') + raise TypeError("The location must be provided as GeoPy Location object") # Create polygon from boundingbox - polygon = box(float(coordinates.raw['boundingbox'][0]), float(coordinates.raw['boundingbox'][2]), - float(coordinates.raw['boundingbox'][1]), float(coordinates.raw['boundingbox'][3])) + polygon = box( + float(coordinates.raw["boundingbox"][0]), + float(coordinates.raw["boundingbox"][2]), + float(coordinates.raw["boundingbox"][1]), + float(coordinates.raw["boundingbox"][3]), + ) return polygon -def get_locations(names: Union[list, str], - crs: Union[str, pyproj.crs.crs.CRS] = 'EPSG:4326') -> dict: +def get_locations( + names: Union[list, str], crs: Union[str, pyproj.crs.crs.CRS] = "EPSG:4326" +) -> dict: """Obtaining coordinates for one city or a list of given cities. A CRS other than 'EPSG:4326' can be passed to transform the coordinates @@ -1224,35 +1269,43 @@ def get_locations(names: Union[list, str], # Checking that the location names are provided as list of strings or as string for one location if not isinstance(names, (list, str)): - raise TypeError('Names must be provided as list of strings') + raise TypeError("Names must be provided as list of strings") # Checking that the target CRS is provided as string if not isinstance(crs, str): - raise TypeError('Target CRS must be of type string') + raise TypeError("Target CRS must be of type string") if isinstance(names, list): # Create list of GeoPy locations coordinates_list = [get_location_coordinate(name=i) for i in names] # Transform CRS and create result_dict - if crs != 'EPSG:4326': - dict_list = [transform_location_coordinate(coordinates=i, - crs=crs) for i in coordinates_list] + if crs != "EPSG:4326": + dict_list = [ + transform_location_coordinate(coordinates=i, crs=crs) + for i in coordinates_list + ] result_dict = {k: v for d in dict_list for k, v in d.items()} else: - result_dict = {coordinates_list[i].address: (coordinates_list[i].longitude, - coordinates_list[i].latitude) - for i in range(len(coordinates_list))} + result_dict = { + coordinates_list[i].address: ( + coordinates_list[i].longitude, + coordinates_list[i].latitude, + ) + for i in range(len(coordinates_list)) + } else: # Create GeoPy Object coordinates = get_location_coordinate(name=names) - if crs != 'EPSG:4326': - result_dict = transform_location_coordinate(coordinates=coordinates, - crs=crs) + if crs != "EPSG:4326": + result_dict = transform_location_coordinate( + coordinates=coordinates, crs=crs + ) else: - result_dict = {coordinates.address: (coordinates.longitude, - coordinates.latitude)} + result_dict = { + coordinates.address: (coordinates.longitude, coordinates.latitude) + } return result_dict @@ -1299,21 +1352,21 @@ def convert_location_dict_to_gdf(location_dict: dict) -> gpd.geodataframe.GeoDat # Checking that the input data is of type dict if not isinstance(location_dict, dict): - raise TypeError('Input data must be provided as dict') + raise TypeError("Input data must be provided as dict") # Creating GeoDataFrame gdf = gpd.GeoDataFrame(data=location_dict).T.reset_index() # Assigning column names - gdf.columns = ['City', 'X', 'Y'] + gdf.columns = ["City", "X", "Y"] # Split city names to only show the name of the city - gdf['City'] = [i.split(',')[0] for i in gdf['City'].to_list()] + gdf["City"] = [i.split(",")[0] for i in gdf["City"].to_list()] # Recreate GeoDataFrame and set coordinates as geometry objects - gdf = gpd.GeoDataFrame(data=gdf, - geometry=gpd.points_from_xy(x=gdf['X'], - y=gdf['Y'])) + gdf = gpd.GeoDataFrame( + data=gdf, geometry=gpd.points_from_xy(x=gdf["X"], y=gdf["Y"]) + ) return gdf @@ -1322,8 +1375,7 @@ def convert_location_dict_to_gdf(location_dict: dict) -> gpd.geodataframe.GeoDat ############################ -def assign_properties(lith_block: np.ndarray, - property_dict: dict) -> np.ndarray: +def assign_properties(lith_block: np.ndarray, property_dict: dict) -> np.ndarray: """Replacing lith block IDs with physical properties Parameters @@ -1366,11 +1418,11 @@ def assign_properties(lith_block: np.ndarray, # Checking that the lith block is a NumPy array if not isinstance(lith_block, np.ndarray): - raise TypeError('Lith block must be a NumPy Array') + raise TypeError("Lith block must be a NumPy Array") # Checking that the properties dict is a dict if not isinstance(property_dict, dict): - raise TypeError('Properties must be provided as dict') + raise TypeError("Properties must be provided as dict") # Store shape shape = lith_block.shape @@ -1449,26 +1501,27 @@ def get_nearest_neighbor(x: np.ndarray, y: np.ndarray) -> np.int64: from sklearn.neighbors import NearestNeighbors except ModuleNotFoundError: raise ModuleNotFoundError( - 'Scikit Learn package is not installed. Use pip install scikit-learn to install the latest version') + "Scikit Learn package is not installed. Use pip install scikit-learn to install the latest version" + ) # Checking that the point data set x is of type np.ndarray if not isinstance(x, np.ndarray): - raise TypeError('Point data set must be of type np.ndarray') + raise TypeError("Point data set must be of type np.ndarray") # Checking that the shape of the array is correct if x.shape[1] != 2: - raise ValueError('Only coordinate pairs are allowed') + raise ValueError("Only coordinate pairs are allowed") # Checking that point y is of type np.ndarray if not isinstance(y, np.ndarray): - raise TypeError('Point data set must be of type np.ndarray') + raise TypeError("Point data set must be of type np.ndarray") # Checking that the shape of the array is correct if y.shape != (2,): - raise ValueError('y point must be of shape (2,)') + raise ValueError("y point must be of shape (2,)") # Finding the nearest neighbor with ball_tree algorithm - nbrs = NearestNeighbors(n_neighbors=1, algorithm='ball_tree').fit(y.reshape(1, -1)) + nbrs = NearestNeighbors(n_neighbors=1, algorithm="ball_tree").fit(y.reshape(1, -1)) # Calculating the distances and indices for to find the nearest neighbor distances, indices = nbrs.kneighbors(x) @@ -1479,9 +1532,11 @@ def get_nearest_neighbor(x: np.ndarray, y: np.ndarray) -> np.int64: return index -def calculate_number_of_isopoints(gdf: Union[gpd.geodataframe.GeoDataFrame, pd.DataFrame], - increment: Union[float, int], - zcol: str = 'Z') -> int: +def calculate_number_of_isopoints( + gdf: Union[gpd.geodataframe.GeoDataFrame, pd.DataFrame], + increment: Union[float, int], + zcol: str = "Z", +) -> int: """Creating the number of isopoints to further interpolate strike lines Parameters @@ -1531,19 +1586,21 @@ def calculate_number_of_isopoints(gdf: Union[gpd.geodataframe.GeoDataFrame, pd.D # Checking if gdf is of type GeoDataFrame if not isinstance(gdf, (gpd.geodataframe.GeoDataFrame, pd.DataFrame)): - raise TypeError('gdf must be of type GeoDataFrame') + raise TypeError("gdf must be of type GeoDataFrame") # Checking if the increment is of type float or int if not isinstance(increment, (float, int)): - raise TypeError('The increment must be provided as float or int') + raise TypeError("The increment must be provided as float or int") # Checking that the name of the Z column is provided as string if not isinstance(zcol, str): - raise TypeError('Z column name must be provided as string') + raise TypeError("Z column name must be provided as string") # Checking that the Z column is in the GeoDataFrame if zcol not in gdf: - raise ValueError('Provide name of Z column as kwarg as Z column could not be recognized') + raise ValueError( + "Provide name of Z column as kwarg as Z column could not be recognized" + ) # Creating a list with the unique heights of the GeoDataFrame heights = gdf[zcol].sort_values().unique().tolist() @@ -1554,11 +1611,13 @@ def calculate_number_of_isopoints(gdf: Union[gpd.geodataframe.GeoDataFrame, pd.D return number -def calculate_lines(gdf: Union[gpd.geodataframe.GeoDataFrame, pd.DataFrame], - increment: Union[float, int], - xcol: str = 'X', - ycol: str = 'Y', - zcol: str = 'Z') -> gpd.geodataframe.GeoDataFrame: +def calculate_lines( + gdf: Union[gpd.geodataframe.GeoDataFrame, pd.DataFrame], + increment: Union[float, int], + xcol: str = "X", + ycol: str = "Y", + zcol: str = "Z", +) -> gpd.geodataframe.GeoDataFrame: """Function to interpolate strike lines Parameters @@ -1606,27 +1665,27 @@ def calculate_lines(gdf: Union[gpd.geodataframe.GeoDataFrame, pd.DataFrame], # Checking if gdf is of type GeoDataFrame if not isinstance(gdf, (gpd.geodataframe.GeoDataFrame, pd.DataFrame)): - raise TypeError('gdf must be of type GeoDataFrame') + raise TypeError("gdf must be of type GeoDataFrame") # Checking that all geometries are valid if not all(gdf.geometry.is_valid): - raise ValueError('Not all Shapely Objects are valid objects') + raise ValueError("Not all Shapely Objects are valid objects") # Checking if the increment is of type float or int if not isinstance(increment, (float, int)): - raise TypeError('The increment must be provided as float or int') + raise TypeError("The increment must be provided as float or int") # Checking that xcol is of type string if not isinstance(xcol, str): - raise TypeError('X column name must be of type string') + raise TypeError("X column name must be of type string") # Checking that ycol is of type string if not isinstance(ycol, str): - raise TypeError('Y column name must be of type string') + raise TypeError("Y column name must be of type string") # Checking that zcol is of type string if not isinstance(zcol, str): - raise TypeError('Z column name must be of type string') + raise TypeError("Z column name must be of type string") # Checking that the columns are in the GeoDataFrame # if not {xcol, ycol, zcol}.issubset(gdf.columns): @@ -1649,21 +1708,27 @@ def calculate_lines(gdf: Union[gpd.geodataframe.GeoDataFrame, pd.DataFrame], # Calculating vertices of lines for i in range(len(gdf[gdf[zcol] == minval])): # Getting index for nearest neighbor - index = get_nearest_neighbor(np.array(gdf[gdf[zcol] == minval][[xcol, ycol]].values.tolist()), - np.array([gdf[gdf[zcol] == minval][xcol].values.tolist()[i], - gdf[gdf[zcol] == minval][ycol].values.tolist()[i]])) + index = get_nearest_neighbor( + np.array(gdf[gdf[zcol] == minval][[xcol, ycol]].values.tolist()), + np.array( + [ + gdf[gdf[zcol] == minval][xcol].values.tolist()[i], + gdf[gdf[zcol] == minval][ycol].values.tolist()[i], + ] + ), + ) # Creating x and y points from existing gdf - x1 = gdf[gdf['Z'] == minval][xcol].tolist()[i] - y1 = gdf[gdf['Z'] == minval][ycol].tolist()[i] - x2 = gdf[gdf['Z'] == maxval][xcol].tolist()[index] - y2 = gdf[gdf['Z'] == maxval][ycol].tolist()[index] + x1 = gdf[gdf["Z"] == minval][xcol].tolist()[i] + y1 = gdf[gdf["Z"] == minval][ycol].tolist()[i] + x2 = gdf[gdf["Z"] == maxval][xcol].tolist()[index] + y2 = gdf[gdf["Z"] == maxval][ycol].tolist()[index] # Calculating vertices of lines for j in range(num): # Calculating vertices - pointx = ((j + 1) / (num + 1) * x2 + (1 - (j + 1) / (num + 1)) * x1) - pointy = ((j + 1) / (num + 1) * y2 + (1 - (j + 1) / (num + 1)) * y1) + pointx = (j + 1) / (num + 1) * x2 + (1 - (j + 1) / (num + 1)) * x1 + pointy = (j + 1) / (num + 1) * y2 + (1 - (j + 1) / (num + 1)) * y1 # Append vertices to list pointsx.append(pointx) @@ -1676,8 +1741,9 @@ def calculate_lines(gdf: Union[gpd.geodataframe.GeoDataFrame, pd.DataFrame], # Create linestring from point lists for i in range(0, int(len(pointsx) / 2)): # Creating linestrings - ls = LineString([Point(pointsx[i], pointsy[i]), - Point(pointsx[i + num], pointsy[i + num])]) + ls = LineString( + [Point(pointsx[i], pointsy[i]), Point(pointsx[i + num], pointsy[i + num])] + ) # Appending line strings ls_list.append(ls) heights.append(minval + i * increment + increment) @@ -1687,25 +1753,27 @@ def calculate_lines(gdf: Union[gpd.geodataframe.GeoDataFrame, pd.DataFrame], lines = gpd.GeoDataFrame(gpd.GeoSeries(ls_list), crs=gdf.crs) # Setting geometry column of GeoDataFrame - lines['geometry'] = ls_list + lines["geometry"] = ls_list # Extracting X and Y coordinate and deleting first entry lines = vector.extract_xy(lines) del lines[0] # Adding formation and height information to GeoDataFrame - lines['formation'] = gdf['formation'].unique().tolist()[0] - lines['Z'] = heights - lines['id'] = heights + lines["formation"] = gdf["formation"].unique().tolist()[0] + lines["Z"] = heights + lines["id"] = heights return lines -def interpolate_strike_lines(gdf: gpd.geodataframe.GeoDataFrame, - increment: Union[float, int], - xcol: str = 'X', - ycol: str = 'Y', - zcol: str = 'Z') -> gpd.geodataframe.GeoDataFrame: +def interpolate_strike_lines( + gdf: gpd.geodataframe.GeoDataFrame, + increment: Union[float, int], + xcol: str = "X", + ycol: str = "Y", + zcol: str = "Z", +) -> gpd.geodataframe.GeoDataFrame: """Interpolating strike lines to calculate orientations Parameters @@ -1738,70 +1806,88 @@ def interpolate_strike_lines(gdf: gpd.geodataframe.GeoDataFrame, # Checking if gdf is of type GeoDataFrame if not isinstance(gdf, gpd.geodataframe.GeoDataFrame): - raise TypeError('gdf must be of type GeoDataFrame') + raise TypeError("gdf must be of type GeoDataFrame") # Checking if the increment is of type float or int if not isinstance(increment, (float, int)): - raise TypeError('The increment must be provided as float or int') + raise TypeError("The increment must be provided as float or int") # Checking that xcol is of type string if not isinstance(xcol, str): - raise TypeError('X column name must be of type string') + raise TypeError("X column name must be of type string") # Checking that ycol is of type string if not isinstance(ycol, str): - raise TypeError('Y column name must be of type string') + raise TypeError("Y column name must be of type string") # Checking that zcol is of type string if not isinstance(zcol, str): - raise TypeError('Z column name must be of type string') + raise TypeError("Z column name must be of type string") # Create empty GeoDataFrame gdf_out = gpd.GeoDataFrame() # Extract vertices from gdf - gdf = vector.extract_xy(gdf, drop_id=False, reset_index=False).sort_values(by='id') + gdf = vector.extract_xy(gdf, drop_id=False, reset_index=False).sort_values(by="id") # Interpolate strike lines - for i in range(len(gdf['id'].unique().tolist()) - 1): + for i in range(len(gdf["id"].unique().tolist()) - 1): # Calculate distance between two strike lines in the original gdf - diff = gdf.loc[gdf.index.unique().values.tolist()[i]][zcol].values.tolist()[0] - \ - gdf.loc[gdf.index.unique().values.tolist()[i + 1]][zcol].values.tolist()[0] + diff = ( + gdf.loc[gdf.index.unique().values.tolist()[i]][zcol].values.tolist()[0] + - gdf.loc[gdf.index.unique().values.tolist()[i + 1]][zcol].values.tolist()[ + 0 + ] + ) # If the distance is larger than the increment, interpolate strike lines if np.abs(diff) > increment: gdf_strike = pd.concat( - [gdf.loc[gdf.index.unique().values.tolist()[i]], gdf.loc[gdf.index.unique().values.tolist()[i + 1]]]) + [ + gdf.loc[gdf.index.unique().values.tolist()[i]], + gdf.loc[gdf.index.unique().values.tolist()[i + 1]], + ] + ) # Calculate strike lines lines = calculate_lines(gdf_strike, increment) # Append interpolated lines to gdf that will be returned gdf_new = pd.concat( - [gdf.loc[gdf.index.unique().values.tolist()[i]], lines, - gdf.loc[gdf.index.unique().values.tolist()[i + 1]]]) + [ + gdf.loc[gdf.index.unique().values.tolist()[i]], + lines, + gdf.loc[gdf.index.unique().values.tolist()[i + 1]], + ] + ) gdf_out = gdf_out.append(gdf_new, ignore_index=True) # If the distance is equal to the increment, append line to the gdf that will be returned else: gdf_new = pd.concat( - [gdf.loc[gdf.index.unique().values.tolist()[i]], gdf.loc[gdf.index.unique().values.tolist()[i + 1]]]) + [ + gdf.loc[gdf.index.unique().values.tolist()[i]], + gdf.loc[gdf.index.unique().values.tolist()[i + 1]], + ] + ) gdf_out = gdf_out.append(gdf_new, ignore_index=True) # Drop duplicates - gdf_out = gdf_out.sort_values(by=['Y']).drop_duplicates('geometry') + gdf_out = gdf_out.sort_values(by=["Y"]).drop_duplicates("geometry") # Redefine ID column with interpolated strike lines - gdf_out['id'] = np.arange(1, len(gdf_out['id'].values.tolist()) + 1).tolist() + gdf_out["id"] = np.arange(1, len(gdf_out["id"].values.tolist()) + 1).tolist() return gdf_out -def convert_to_petrel_points_with_attributes(mesh: pv.core.pointset.PolyData, - path: str, - crs: Union[str, pyproj.crs.crs.CRS, type(None)] = None, - target_crs: Union[str, pyproj.crs.crs.CRS, type(None)] = None): +def convert_to_petrel_points_with_attributes( + mesh: pv.core.pointset.PolyData, + path: str, + crs: Union[str, pyproj.crs.crs.CRS, type(None)] = None, + target_crs: Union[str, pyproj.crs.crs.CRS, type(None)] = None, +): """Function to convert vertices of a PyVista Mesh to Petrel Points with Attributes Parameters @@ -1826,22 +1912,26 @@ def convert_to_petrel_points_with_attributes(mesh: pv.core.pointset.PolyData, # Checking that the mesh is a PyVista PolyData object if not isinstance(mesh, pv.core.pointset.PolyData): - raise TypeError('Mesh must be provided as PyVista PolyData object') + raise TypeError("Mesh must be provided as PyVista PolyData object") # Checking that the CRS is provided as proper type if not isinstance(crs, (str, pyproj.crs.crs.CRS, type(None))): - raise TypeError('CRS must be provided as string or pyproj CRS object') + raise TypeError("CRS must be provided as string or pyproj CRS object") # Checking that the target CRS is provided as proper type if not isinstance(target_crs, (str, pyproj.crs.crs.CRS, type(None))): - raise TypeError('CRS must be provided as string or pyproj CRS object') + raise TypeError("CRS must be provided as string or pyproj CRS object") # Selecting vertices vertices = np.array(mesh.points) # Creating GeoDataFrame from vertices - gdf = gpd.GeoDataFrame(geometry=gpd.points_from_xy(vertices[:, 0], vertices[:, 1]), data=vertices, - columns=['X', 'Y', 'Z'], crs=crs) + gdf = gpd.GeoDataFrame( + geometry=gpd.points_from_xy(vertices[:, 0], vertices[:, 1]), + data=vertices, + columns=["X", "Y", "Z"], + crs=crs, + ) # Reprojecting data and extracting X and Y coordinates if target_crs and target_crs != crs: @@ -1849,19 +1939,19 @@ def convert_to_petrel_points_with_attributes(mesh: pv.core.pointset.PolyData, gdf = vector.extract_xy(gdf=gdf) # Dropping Geometry Column - df = gdf.drop('geometry', axis=1) + df = gdf.drop("geometry", axis=1) - df.to_csv(fname=path, - index=False, - sep='\t') + df.to_csv(fname=path, index=False, sep="\t") - print('CSV-File successfully saved') + print("CSV-File successfully saved") -def ray_trace_one_surface(surface: Union[pv.core.pointset.PolyData, pv.core.pointset.UnstructuredGrid], - origin: Union[np.ndarray, list], - end_point: Union[np.ndarray, list], - first_point: bool = False) -> tuple: +def ray_trace_one_surface( + surface: Union[pv.core.pointset.PolyData, pv.core.pointset.UnstructuredGrid], + origin: Union[np.ndarray, list], + end_point: Union[np.ndarray, list], + first_point: bool = False, +) -> tuple: """Function to return the depth of one surface in one well using PyVista ray tracing Parameters @@ -1890,25 +1980,29 @@ def ray_trace_one_surface(surface: Union[pv.core.pointset.PolyData, pv.core.poin """ # Checking that the provided surface is of type PoyData or UnstructuredGrid - if not isinstance(surface, (pv.core.pointset.PolyData, pv.core.pointset.UnstructuredGrid)): - raise TypeError('Surface must be provided as PolyData or UnstructuredGrid') + if not isinstance( + surface, (pv.core.pointset.PolyData, pv.core.pointset.UnstructuredGrid) + ): + raise TypeError("Surface must be provided as PolyData or UnstructuredGrid") # Converting UnstructuredGrid to PolyData if isinstance(surface, pv.core.pointset.UnstructuredGrid): surface = surface.extract_surface() # Extracting the intersection between a PolyData set and a mesh - intersection_points, intersection_cells = surface.ray_trace(origin=origin, - end_point=end_point, - first_point=first_point) + intersection_points, intersection_cells = surface.ray_trace( + origin=origin, end_point=end_point, first_point=first_point + ) return intersection_points, intersection_cells -def ray_trace_multiple_surfaces(surfaces: list, - borehole_top: Union[np.ndarray, list], - borehole_bottom: Union[np.ndarray, list], - first_point: bool = False) -> list: +def ray_trace_multiple_surfaces( + surfaces: list, + borehole_top: Union[np.ndarray, list], + borehole_bottom: Union[np.ndarray, list], + first_point: bool = False, +) -> list: """Function to return the depth of multiple surfaces in one well using PyVista ray tracing Parameters @@ -1937,18 +2031,25 @@ def ray_trace_multiple_surfaces(surfaces: list, """ # Extracting multiple intersections from meshes - intersections = [ray_trace_one_surface(surface=surface, - origin=borehole_top, - end_point=borehole_bottom, - first_point=first_point) for surface in surfaces] + intersections = [ + ray_trace_one_surface( + surface=surface, + origin=borehole_top, + end_point=borehole_bottom, + first_point=first_point, + ) + for surface in surfaces + ] return intersections -def create_virtual_profile(names_surfaces: list, - surfaces: list, - borehole: pv.core.pointset.PolyData, - first_point: bool = False) -> pd.DataFrame: +def create_virtual_profile( + names_surfaces: list, + surfaces: list, + borehole: pv.core.pointset.PolyData, + first_point: bool = False, +) -> pd.DataFrame: """Function to filter and sort the resulting well tops Parameters @@ -1977,13 +2078,21 @@ def create_virtual_profile(names_surfaces: list, """ # Creating well segments - well_segments = [pv.Line(borehole.points[i], borehole.points[i + 1]) for i in range(len(borehole.points) - 1)] + well_segments = [ + pv.Line(borehole.points[i], borehole.points[i + 1]) + for i in range(len(borehole.points) - 1) + ] # Extracting well tops - well_tops = [ray_trace_multiple_surfaces(surfaces=surfaces, - borehole_top=segment.points[0], - borehole_bottom=segment.points[1], - first_point=first_point) for segment in well_segments] + well_tops = [ + ray_trace_multiple_surfaces( + surfaces=surfaces, + borehole_top=segment.points[0], + borehole_bottom=segment.points[1], + first_point=first_point, + ) + for segment in well_segments + ] # Flatten list well_tops = [item for sublist in well_tops for item in sublist] @@ -2023,14 +2132,18 @@ def create_virtual_profile(names_surfaces: list, # well_dict = dict(sorted(well_dict.items(), key=lambda item: item[1], reverse=True)) # Creating DataFrame - df = pd.DataFrame(list(zip(list_surfaces_filtered, z_values)), columns=['Surface', 'Z']) + df = pd.DataFrame( + list(zip(list_surfaces_filtered, z_values)), columns=["Surface", "Z"] + ) return df -def extract_zmap_data(surface: pv.core.pointset.PolyData, - cell_width: int, - nodata: Union[float, int] = -9999): +def extract_zmap_data( + surface: pv.core.pointset.PolyData, + cell_width: int, + nodata: Union[float, int] = -9999, +): """Function to extract a meshgrid of values from a PyVista mesh Parameters @@ -2071,29 +2184,40 @@ def extract_zmap_data(surface: pv.core.pointset.PolyData, y = np.arange(extent[2] + 0.5 * cell_width, extent[3], cell_width) # Calculating the intersections - intersections = [ray_trace_one_surface(surface=surface, - origin=[x_value, y_value, extent[4]], - end_point=[x_value, y_value, extent[5]], - first_point=True) for x_value in x for y_value in y] + intersections = [ + ray_trace_one_surface( + surface=surface, + origin=[x_value, y_value, extent[4]], + end_point=[x_value, y_value, extent[5]], + first_point=True, + ) + for x_value in x + for y_value in y + ] # Extracting the height values - z_values = np.flipud(np.array([z[0][2] if len(z[0]) == 3 else nodata for z in intersections]).reshape(x_no_cells, - y_no_cells).T) + z_values = np.flipud( + np.array([z[0][2] if len(z[0]) == 3 else nodata for z in intersections]) + .reshape(x_no_cells, y_no_cells) + .T + ) return z_values -def create_zmap_grid(surface: pv.core.pointset.PolyData, - cell_width: int, - comments: str = '', - name: str = 'ZMAP_Grid', - z_type: str = 'GRID', - nodes_per_line: int = 5, - field_width: int = 15, - nodata: Union[int, float] = -9999.00000, - nodata2: Union[int, float, str] = '', - decimal_places: int = 5, - start_column: int = 1): +def create_zmap_grid( + surface: pv.core.pointset.PolyData, + cell_width: int, + comments: str = "", + name: str = "ZMAP_Grid", + z_type: str = "GRID", + nodes_per_line: int = 5, + field_width: int = 15, + nodata: Union[int, float] = -9999.00000, + nodata2: Union[int, float, str] = "", + decimal_places: int = 5, + start_column: int = 1, +): """Function to write data to ZMAP Grid, This code is heavily inspired by https://github.com/abduhbm/zmapio Parameters @@ -2143,9 +2267,7 @@ def create_zmap_grid(surface: pv.core.pointset.PolyData, """ # Extracting z_values - z_values = extract_zmap_data(surface=surface, - cell_width=cell_width, - nodata=nodata) + z_values = extract_zmap_data(surface=surface, cell_width=cell_width, nodata=nodata) # Defining extent extent = surface.bounds @@ -2157,16 +2279,20 @@ def create_zmap_grid(surface: pv.core.pointset.PolyData, # Defining auxiliary function def chunks(x, n): for i in range(0, len(x), n): - yield x[i: i + n] + yield x[i : i + n] # Create list of lines with first comments - lines = ['!', '! This ZMAP Grid was created using the GemGIS Package', - '! See https://github.com/cgre-aachen/gemgis for more information', '!'] + lines = [ + "!", + "! This ZMAP Grid was created using the GemGIS Package", + "! See https://github.com/cgre-aachen/gemgis for more information", + "!", + ] # Appending comments to lines for comment in comments: - lines.append('! ' + comment) - lines.append('!') + lines.append("! " + comment) + lines.append("!") # Appending header information to lines lines.append("@{}, {}, {}".format(name, z_type, nodes_per_line)) @@ -2185,12 +2311,7 @@ def chunks(x, n): # Appending header information to lines lines.append( "{}, {}, {}, {}, {}, {}".format( - no_rows, - no_cols, - extent[0], - extent[1], - extent[2], - extent[3] + no_rows, no_cols, extent[0], extent[1], extent[2], extent[3] ) ) @@ -2203,15 +2324,21 @@ def chunks(x, n): for j in chunks(i, nodes_per_line): j_fmt = "0.{}f".format(decimal_places) j_fmt = "{0:" + j_fmt + "}" - j = [j_fmt.format(float(x)) if x is not np.nan else j_fmt.format(float(nodata)) for x in j] + j = [ + ( + j_fmt.format(float(x)) + if x is not np.nan + else j_fmt.format(float(nodata)) + ) + for x in j + ] line = "{:>" + "{}".format(field_width) + "}" lines.append("".join([line] * len(j)).format(*tuple(j))) return lines -def save_zmap_grid(zmap_grid: list, - path: str = 'ZMAP_Grid.dat'): +def save_zmap_grid(zmap_grid: list, path: str = "ZMAP_Grid.dat"): """Function to save ZMAP Grid information to file Parameters @@ -2228,20 +2355,22 @@ def save_zmap_grid(zmap_grid: list, """ # Writing the ZMAP Grid to file - with open(path, 'w') as f: + with open(path, "w") as f: f.write("\n".join(zmap_grid)) - print('ZMAP Grid successfully saved to file') + print("ZMAP Grid successfully saved to file") -def rotate_gempy_input_data(extent: Union[np.ndarray, shapely.geometry.Polygon, gpd.geodataframe.GeoDataFrame], - interfaces: Union[pd.DataFrame, gpd.geodataframe.GeoDataFrame], - orientations: Union[pd.DataFrame, gpd.geodataframe.GeoDataFrame], - zmin: Union[float, int] = None, - zmax: Union[float, int] = None, - rotate_reverse_direction: bool = False, - return_extent_gdf: bool = False, - manual_rotation_angle: Union[float, int] = None): +def rotate_gempy_input_data( + extent: Union[np.ndarray, shapely.geometry.Polygon, gpd.geodataframe.GeoDataFrame], + interfaces: Union[pd.DataFrame, gpd.geodataframe.GeoDataFrame], + orientations: Union[pd.DataFrame, gpd.geodataframe.GeoDataFrame], + zmin: Union[float, int] = None, + zmax: Union[float, int] = None, + rotate_reverse_direction: bool = False, + return_extent_gdf: bool = False, + manual_rotation_angle: Union[float, int] = None, +): """Function to rotate the GemPy Input Data horizontally or vertically Parameters @@ -2288,142 +2417,210 @@ def rotate_gempy_input_data(extent: Union[np.ndarray, shapely.geometry.Polygon, """ # Checking that the extent is of type list, Shapely Polygon, or GeoDataFrame - if not isinstance(extent, (np.ndarray, shapely.geometry.Polygon, gpd.geodataframe.GeoDataFrame)): - raise TypeError('The extent must be provided as NumPy array, Shapely Polygon oder GeoDataFrame') + if not isinstance( + extent, (np.ndarray, shapely.geometry.Polygon, gpd.geodataframe.GeoDataFrame) + ): + raise TypeError( + "The extent must be provided as NumPy array, Shapely Polygon oder GeoDataFrame" + ) # Checking the number of coordinates of the extent and convert extent to Shapely Polygpon if isinstance(extent, np.ndarray): if len(extent) != 4: - raise ValueError('Please only provide four corner coordinates as extent') + raise ValueError("Please only provide four corner coordinates as extent") extent_polygon = Polygon(extent) elif isinstance(extent, shapely.geometry.Polygon): - if not (len(list(extent.exterior.coords)) != 4) or (len(list(extent.exterior.coords)) != 5): - raise ValueError('Please only provide a polygon with four corner coordinates as extent') + if not (len(list(extent.exterior.coords)) != 4) or ( + len(list(extent.exterior.coords)) != 5 + ): + raise ValueError( + "Please only provide a polygon with four corner coordinates as extent" + ) extent_polygon = extent else: - if len(list(extent.iloc[0]['geometry'].exterior.coords)) != 5: - raise ValueError('Please only provide a polygon with four corner coordinates as extent') + if len(list(extent.iloc[0]["geometry"].exterior.coords)) != 5: + raise ValueError( + "Please only provide a polygon with four corner coordinates as extent" + ) - extent_polygon = extent.iloc[0]['geometry'] + extent_polygon = extent.iloc[0]["geometry"] # Checking that the interfaces are of type DataFrame or GeoDataFrame if not isinstance(interfaces, (pd.DataFrame, gpd.geodataframe.GeoDataFrame)): - raise TypeError('Interfaces must be provided as Pandas DataFrame or GeoPandas GeoDataFrame') + raise TypeError( + "Interfaces must be provided as Pandas DataFrame or GeoPandas GeoDataFrame" + ) # Extracting X, Y, Z coordinates if interfaces are of type GeoDataFrame - if (isinstance(interfaces, gpd.geodataframe.GeoDataFrame)) and (not {'X', 'Y', 'Z'}.issubset(interfaces.columns)): + if (isinstance(interfaces, gpd.geodataframe.GeoDataFrame)) and ( + not {"X", "Y", "Z"}.issubset(interfaces.columns) + ): interfaces = vector.extract_xy(interfaces) # Checking if X, Y, Z coordinates are in columns - if not {'X', 'Y', 'Z'}.issubset(interfaces.columns): - raise ValueError('Please provide all X, Y and Z coordinates in the Pandas DataFrame or GeoPandas GeoDataFrame') + if not {"X", "Y", "Z"}.issubset(interfaces.columns): + raise ValueError( + "Please provide all X, Y and Z coordinates in the Pandas DataFrame or GeoPandas GeoDataFrame" + ) # Checking that the orientations are of type DataFrame or GeoDataFrame if not isinstance(orientations, (pd.DataFrame, gpd.geodataframe.GeoDataFrame)): - raise TypeError('Orientations must be provided as Pandas DataFrame or GeoPandas GeoDataFrame') + raise TypeError( + "Orientations must be provided as Pandas DataFrame or GeoPandas GeoDataFrame" + ) # Extracting X, Y, Z coordinates if orientations are of type GeoDataFrame if (isinstance(orientations, gpd.geodataframe.GeoDataFrame)) and ( - not {'X', 'Y', 'Z'}.issubset(orientations.columns)): + not {"X", "Y", "Z"}.issubset(orientations.columns) + ): orientations = vector.extract_xy(orientations) # Checking if X, Y, Z coordinates are in columns - if not {'X', 'Y', 'Z'}.issubset(orientations.columns): - raise ValueError('Please provide all X, Y and Z coordinates in the Pandas DataFrame or GeoPandas GeoDataFrame') + if not {"X", "Y", "Z"}.issubset(orientations.columns): + raise ValueError( + "Please provide all X, Y and Z coordinates in the Pandas DataFrame or GeoPandas GeoDataFrame" + ) # Checking that zmin is of type float or int if not isinstance(zmin, (float, int)): - raise TypeError('zmin must be provided as float or int') + raise TypeError("zmin must be provided as float or int") # Checking that zmax is of type float or int if not isinstance(zmax, (float, int)): - raise TypeError('zmax must be provided as float or int') + raise TypeError("zmax must be provided as float or int") # Checking that rotate_reverse_direction is of type bool if not isinstance(rotate_reverse_direction, bool): - raise TypeError('rotate_reverse_direction must be of type bool') + raise TypeError("rotate_reverse_direction must be of type bool") # Checking that return_extent_gdf is of type bool if not isinstance(return_extent_gdf, bool): - raise TypeError('return_extent_gdf must be of type bool') + raise TypeError("return_extent_gdf must be of type bool") # Calculating the smallest angle to perform the rotation - min_angle = min([vector.calculate_angle(LineString((list(extent_polygon.exterior.coords)[i], - list(extent_polygon.exterior.coords)[i + 1]))) for i in - range(len(list(extent_polygon.exterior.coords)) - 1)]) + min_angle = min( + [ + vector.calculate_angle( + LineString( + ( + list(extent_polygon.exterior.coords)[i], + list(extent_polygon.exterior.coords)[i + 1], + ) + ) + ) + for i in range(len(list(extent_polygon.exterior.coords)) - 1) + ] + ) # Using the manual rotation angle if provided if manual_rotation_angle: min_angle = manual_rotation_angle # Creating GeoDataFrames from DataFrames - interfaces = gpd.GeoDataFrame(geometry=gpd.points_from_xy(x=interfaces['X'], - y=interfaces['Y']), - data=interfaces) + interfaces = gpd.GeoDataFrame( + geometry=gpd.points_from_xy(x=interfaces["X"], y=interfaces["Y"]), + data=interfaces, + ) - orientations = gpd.GeoDataFrame(geometry=gpd.points_from_xy(x=orientations['X'], - y=orientations['Y']), - data=orientations) + orientations = gpd.GeoDataFrame( + geometry=gpd.points_from_xy(x=orientations["X"], y=orientations["Y"]), + data=orientations, + ) # Creating Polygons from Interfaces and Orientations - interfaces_polygon = shapely.geometry.Polygon(interfaces['geometry']) - orientations_polygon = shapely.geometry.Polygon(orientations['geometry']) + interfaces_polygon = shapely.geometry.Polygon(interfaces["geometry"]) + orientations_polygon = shapely.geometry.Polygon(orientations["geometry"]) # Rotating extent to vertical or horizontal if not rotate_reverse_direction: # Rotating extent - extent_rotated = shapely.affinity.rotate(extent_polygon, -min_angle, 'center') + extent_rotated = shapely.affinity.rotate(extent_polygon, -min_angle, "center") # Rotating interfaces and orientations - interfaces_polygon_rotated = shapely.affinity.rotate(interfaces_polygon, - -min_angle, - (list(extent_polygon.centroid.coords)[0][0], - list(extent_polygon.centroid.coords)[0][1])) + interfaces_polygon_rotated = shapely.affinity.rotate( + interfaces_polygon, + -min_angle, + ( + list(extent_polygon.centroid.coords)[0][0], + list(extent_polygon.centroid.coords)[0][1], + ), + ) - orientations_polygon_rotated = shapely.affinity.rotate(orientations_polygon, - -min_angle, - (list(extent_polygon.centroid.coords)[0][0], - list(extent_polygon.centroid.coords)[0][1])) + orientations_polygon_rotated = shapely.affinity.rotate( + orientations_polygon, + -min_angle, + ( + list(extent_polygon.centroid.coords)[0][0], + list(extent_polygon.centroid.coords)[0][1], + ), + ) else: # Rotating extent - extent_rotated = shapely.affinity.rotate(extent_polygon, min_angle, 'center') + extent_rotated = shapely.affinity.rotate(extent_polygon, min_angle, "center") # Rotating interfaces and orientations - interfaces_polygon_rotated = shapely.affinity.rotate(interfaces_polygon, - min_angle, - (list(extent_polygon.centroid.coords)[0][0], - list(extent_polygon.centroid.coords)[0][1])) + interfaces_polygon_rotated = shapely.affinity.rotate( + interfaces_polygon, + min_angle, + ( + list(extent_polygon.centroid.coords)[0][0], + list(extent_polygon.centroid.coords)[0][1], + ), + ) - orientations_polygon_rotated = shapely.affinity.rotate(orientations_polygon, - min_angle, - (list(extent_polygon.centroid.coords)[0][0], - list(extent_polygon.centroid.coords)[0][1])) + orientations_polygon_rotated = shapely.affinity.rotate( + orientations_polygon, + min_angle, + ( + list(extent_polygon.centroid.coords)[0][0], + list(extent_polygon.centroid.coords)[0][1], + ), + ) # Creating Bounding Box - extent = [extent_rotated.bounds[0], - extent_rotated.bounds[2], - extent_rotated.bounds[1], - extent_rotated.bounds[3], - zmin, - zmax] + extent = [ + extent_rotated.bounds[0], + extent_rotated.bounds[2], + extent_rotated.bounds[1], + extent_rotated.bounds[3], + zmin, + zmax, + ] # Converting Polygons back to Points and extracting Points interfaces_rotated = gpd.GeoDataFrame( - geometry=gpd.points_from_xy(x=[coords[0] for coords in list(interfaces_polygon_rotated.exterior.coords)[:-1]], - y=[coords[1] for coords in list(interfaces_polygon_rotated.exterior.coords)[:-1]]), - data=interfaces) + geometry=gpd.points_from_xy( + x=[ + coords[0] + for coords in list(interfaces_polygon_rotated.exterior.coords)[:-1] + ], + y=[ + coords[1] + for coords in list(interfaces_polygon_rotated.exterior.coords)[:-1] + ], + ), + data=interfaces, + ) interfaces_rotated = vector.extract_xy(interfaces_rotated) orientations_rotated = gpd.GeoDataFrame( - geometry=gpd.points_from_xy(x=[coords[0] for coords in list(orientations_polygon_rotated.exterior.coords)[:-1]], - y=[coords[1] for coords in - list(orientations_polygon_rotated.exterior.coords)[:-1]]), - data=orientations) + geometry=gpd.points_from_xy( + x=[ + coords[0] + for coords in list(orientations_polygon_rotated.exterior.coords)[:-1] + ], + y=[ + coords[1] + for coords in list(orientations_polygon_rotated.exterior.coords)[:-1] + ], + ), + data=orientations, + ) orientations_rotated = vector.extract_xy(orientations_rotated) # Return extent gdf if needed @@ -2462,48 +2659,62 @@ def open_mpk(path_in: str): import py7zr except ModuleNotFoundError: raise ModuleNotFoundError( - 'py7zr package is not installed. Use pip install py7zr to install the latest version') + "py7zr package is not installed. Use pip install py7zr to install the latest version" + ) # Checking that the file path is of type string if not isinstance(path_in, str): - raise TypeError('The file path must be provided as string') + raise TypeError("The file path must be provided as string") # Renaming .mpk file to .zip file - path_out = path_in.split('.mpk')[0] - os.rename(path_in, path_out + '.zip') + path_out = path_in.split(".mpk")[0] + os.rename(path_in, path_out + ".zip") # Unzipping files - with py7zr.SevenZipFile(path_out + '.zip', - 'r') as archive: + with py7zr.SevenZipFile(path_out + ".zip", "r") as archive: archive.extractall(path=path_out) # Getting vector data files - files_vector_data = [os.path.join(path, name) for path, subdirs, files in os.walk(path_out) - for name in files if name.endswith(".shp")] + files_vector_data = [ + os.path.join(path, name) + for path, subdirs, files in os.walk(path_out) + for name in files + if name.endswith(".shp") + ] # Creating vector data dict - dict_vector_data = {file.rsplit('\\')[-1]: gpd.read_file(file) for file in files_vector_data} + dict_vector_data = { + file.rsplit("\\")[-1]: gpd.read_file(file) for file in files_vector_data + } # TODO: Add support for .tif files if case arises # Getting raster data files - files_raster_data_adf = [os.path.join(path, name) for path, subdirs, files in os.walk(path_out) for name in files if - (name.endswith(".adf")) & (name.startswith("w001001."))] + files_raster_data_adf = [ + os.path.join(path, name) + for path, subdirs, files in os.walk(path_out) + for name in files + if (name.endswith(".adf")) & (name.startswith("w001001.")) + ] # Creating raster data dict - dict_raster_data = {file.rsplit('\\')[-1]: rasterio.open(file) for file in files_raster_data_adf} + dict_raster_data = { + file.rsplit("\\")[-1]: rasterio.open(file) for file in files_raster_data_adf + } return dict_vector_data, dict_raster_data -def convert_crs_seismic_data(path_in: str, - path_out: str, - crs_in: Union[str, pyproj.crs.crs.CRS], - crs_out: Union[str, pyproj.crs.crs.CRS], - cdpx: int = 181, - cdpy: int = 185, - vert_domain: str = 'TWT', - coord_scalar: int = None): +def convert_crs_seismic_data( + path_in: str, + path_out: str, + crs_in: Union[str, pyproj.crs.crs.CRS], + crs_out: Union[str, pyproj.crs.crs.CRS], + cdpx: int = 181, + cdpy: int = 185, + vert_domain: str = "TWT", + coord_scalar: int = None, +): """Convert CDP coordinates of seismic data to a new CRS. Parameters @@ -2533,45 +2744,44 @@ def convert_crs_seismic_data(path_in: str, from segysak.segy import segy_loader, segy_writer except ModuleNotFoundError: raise ModuleNotFoundError( - 'segysak package is not installed. Use pip install segysak to install the latest version') + "segysak package is not installed. Use pip install segysak to install the latest version" + ) # Checking that path_in is of type string if not isinstance(path_in, str): - raise TypeError('path_in must be provided as string') + raise TypeError("path_in must be provided as string") # Checking that path_out is of type string if not isinstance(path_out, str): - raise TypeError('path_out must be provided as string') + raise TypeError("path_out must be provided as string") # Checking that crs_in is of type string or pyproj CRS if not isinstance(crs_in, (str, pyproj.crs.crs.CRS)): - raise TypeError('crs_in must be provided as string or pyproj CRS') + raise TypeError("crs_in must be provided as string or pyproj CRS") # Checking that crs_out is of type string or pyproj CRS if not isinstance(crs_out, (str, pyproj.crs.crs.CRS)): - raise TypeError('crs_out must be provided as string or pyproj CRS') + raise TypeError("crs_out must be provided as string or pyproj CRS") # Checking that vert_domain is of type str if not isinstance(vert_domain, str): - raise TypeError('vert_domain must be provided as string') + raise TypeError("vert_domain must be provided as string") # Checking that the coord_scalar is of type int or None if not isinstance(coord_scalar, (int, type(None))): - raise TypeError('coord_scalar must be provided as int') + raise TypeError("coord_scalar must be provided as int") # Loading seismic data - seismic = segy_loader(path_in, - vert_domain=vert_domain, - cdpx=cdpx, - cdpy=cdpy) + seismic = segy_loader(path_in, vert_domain=vert_domain, cdpx=cdpx, cdpy=cdpy) # Converting Seismic to DataFrame df_seismic = seismic.to_dataframe() # Checking that the CDP coordinates are in the DataFrame - if not {'cdp_x', 'cdp_y'}.issubset(df_seismic.columns): + if not {"cdp_x", "cdp_y"}.issubset(df_seismic.columns): raise ValueError( - 'No coordinates found, please provide the byte positions where the X and Y data of the CDPs is stored') + "No coordinates found, please provide the byte positions where the X and Y data of the CDPs is stored" + ) # Extracting the length of the samples to reduce computing time samples = len(df_seismic.index.get_level_values(1).unique()) @@ -2580,36 +2790,39 @@ def convert_crs_seismic_data(path_in: str, df_seismic_resampled = df_seismic[::samples] # Reprojecting Coordinates - df_seismic_reprojected = gpd.GeoDataFrame(geometry=gpd.points_from_xy(x=df_seismic_resampled['cdp_x'].values, - y=df_seismic_resampled['cdp_y'].values), - crs=crs_in).to_crs(crs_out) + df_seismic_reprojected = gpd.GeoDataFrame( + geometry=gpd.points_from_xy( + x=df_seismic_resampled["cdp_x"].values, + y=df_seismic_resampled["cdp_y"].values, + ), + crs=crs_in, + ).to_crs(crs_out) # Extracting reprojected coordinates x = df_seismic_reprojected.geometry.x.values y = df_seismic_reprojected.geometry.y.values # Assigning new coordinates - seismic['cdp_x'][:] = x - seismic['cdp_y'][:] = y + seismic["cdp_x"][:] = x + seismic["cdp_y"][:] = y # Optionally setting a new coord_scalar if coord_scalar: - seismic.attrs['coord_scalar'] = coord_scalar + seismic.attrs["coord_scalar"] = coord_scalar # Saving reprojected seismic data to file - segy_writer(seismic, - path_out, - trace_header_map=dict(cdp_x=181, - cdp_y=185)) + segy_writer(seismic, path_out, trace_header_map=dict(cdp_x=181, cdp_y=185)) - print('Seismic data was successfully reprojected and saved to file') + print("Seismic data was successfully reprojected and saved to file") -def get_cdp_linestring_of_seismic_data(path_in: str, - crs_in: Union[str, pyproj.crs.crs.CRS], - cdpx: int = 181, - cdpy: int = 185, - vert_domain: str = 'TWT'): +def get_cdp_linestring_of_seismic_data( + path_in: str, + crs_in: Union[str, pyproj.crs.crs.CRS], + cdpx: int = 181, + cdpy: int = 185, + vert_domain: str = "TWT", +): """Extracting the path of the seismic data as LineString. Parameters @@ -2638,33 +2851,32 @@ def get_cdp_linestring_of_seismic_data(path_in: str, from segysak.segy import segy_loader except ModuleNotFoundError: raise ModuleNotFoundError( - 'segysak package is not installed. Use pip install segysak to install the latest version') + "segysak package is not installed. Use pip install segysak to install the latest version" + ) # Checking that path_in is of type string if not isinstance(path_in, str): - raise TypeError('path_in must be provided as string') + raise TypeError("path_in must be provided as string") # Checking that crs_in is of type string or pyproj CRS if not isinstance(crs_in, (str, pyproj.crs.crs.CRS)): - raise TypeError('crs_in must be provided as string or pyproj CRS') + raise TypeError("crs_in must be provided as string or pyproj CRS") # Checking that vert_domain is of type str if not isinstance(vert_domain, str): - raise TypeError('vert_domain must be provided as string') + raise TypeError("vert_domain must be provided as string") # Loading seismic data - seismic = segy_loader(path_in, - vert_domain=vert_domain, - cdpx=cdpx, - cdpy=cdpy) + seismic = segy_loader(path_in, vert_domain=vert_domain, cdpx=cdpx, cdpy=cdpy) # Converting Seismic to DataFrame df_seismic = seismic.to_dataframe() # Checking that the CDP coordinates are in the DataFrame - if not {'cdp_x', 'cdp_y'}.issubset(df_seismic.columns): + if not {"cdp_x", "cdp_y"}.issubset(df_seismic.columns): raise ValueError( - 'No coordinates found, please provide the byte positions where the X and Y data of the CDPs is stored') + "No coordinates found, please provide the byte positions where the X and Y data of the CDPs is stored" + ) # Extracting the length of the samples to reduce computing time samples = len(df_seismic.index.get_level_values(1).unique()) @@ -2673,23 +2885,27 @@ def get_cdp_linestring_of_seismic_data(path_in: str, df_seismic_resampled = df_seismic[::samples] # Creating LineString from coordinates - linestring = LineString(np.c_[(df_seismic_resampled['cdp_x'].values, - df_seismic_resampled['cdp_y'].values)]) + linestring = LineString( + np.c_[ + (df_seismic_resampled["cdp_x"].values, df_seismic_resampled["cdp_y"].values) + ] + ) # Reprojecting Coordinates - gdf_seismic = gpd.GeoDataFrame(geometry=[linestring], - crs=crs_in) + gdf_seismic = gpd.GeoDataFrame(geometry=[linestring], crs=crs_in) return gdf_seismic -def get_cdp_points_of_seismic_data(path_in: str, - crs_in: Union[str, pyproj.crs.crs.CRS], - cdpx: int = 181, - cdpy: int = 185, - vert_domain: str = 'TWT', - filter: int = None, - n_meter: Union[int, float] = None): +def get_cdp_points_of_seismic_data( + path_in: str, + crs_in: Union[str, pyproj.crs.crs.CRS], + cdpx: int = 181, + cdpy: int = 185, + vert_domain: str = "TWT", + filter: int = None, + n_meter: Union[int, float] = None, +): """Extracting the path of the seismic data as LineString. Parameters @@ -2722,33 +2938,32 @@ def get_cdp_points_of_seismic_data(path_in: str, from segysak.segy import segy_loader except ModuleNotFoundError: raise ModuleNotFoundError( - 'segysak package is not installed. Use pip install segysak to install the latest version') + "segysak package is not installed. Use pip install segysak to install the latest version" + ) # Checking that path_in is of type string if not isinstance(path_in, str): - raise TypeError('path_in must be provided as string') + raise TypeError("path_in must be provided as string") # Checking that crs_in is of type string or pyproj CRS if not isinstance(crs_in, (str, pyproj.crs.crs.CRS)): - raise TypeError('crs_in must be provided as string or pyproj CRS') + raise TypeError("crs_in must be provided as string or pyproj CRS") # Checking that vert_domain is of type str if not isinstance(vert_domain, str): - raise TypeError('vert_domain must be provided as string') + raise TypeError("vert_domain must be provided as string") # Loading seismic data - seismic = segy_loader(path_in, - vert_domain=vert_domain, - cdpx=cdpx, - cdpy=cdpy) + seismic = segy_loader(path_in, vert_domain=vert_domain, cdpx=cdpx, cdpy=cdpy) # Converting Seismic to DataFrame df_seismic = seismic.to_dataframe() # Checking that the CDP coordinates are in the DataFrame - if not {'cdp_x', 'cdp_y'}.issubset(df_seismic.columns): + if not {"cdp_x", "cdp_y"}.issubset(df_seismic.columns): raise ValueError( - 'No coordinates found, please provide the byte positions where the X and Y data of the CDPs is stored') + "No coordinates found, please provide the byte positions where the X and Y data of the CDPs is stored" + ) # Extracting the length of the samples to reduce computing time samples = len(df_seismic.index.get_level_values(1).unique()) @@ -2759,28 +2974,44 @@ def get_cdp_points_of_seismic_data(path_in: str, if n_meter: # Creating LineString from coordinates - linestring = LineString(np.c_[(df_seismic_resampled['cdp_x'].values, - df_seismic_resampled['cdp_y'].values)]) + linestring = LineString( + np.c_[ + ( + df_seismic_resampled["cdp_x"].values, + df_seismic_resampled["cdp_y"].values, + ) + ] + ) # Defining number of samples samples = np.arange(0, round(linestring.length / n_meter) + 1, 1) # Getting points every n_meter - points = [shapely.line_interpolate_point(linestring, n_meter * sample) for sample in samples] + points = [ + shapely.line_interpolate_point(linestring, n_meter * sample) + for sample in samples + ] # Creating GeoDataFrame from points - gdf_seismic = gpd.GeoDataFrame(geometry=points, - crs=crs_in) + gdf_seismic = gpd.GeoDataFrame(geometry=points, crs=crs_in) # Appending distance - gdf_seismic['distance'] = samples * n_meter + gdf_seismic["distance"] = samples * n_meter else: # Creating Points from coordinates - gdf_seismic = gpd.GeoDataFrame(geometry=gpd.points_from_xy(x=df_seismic_resampled['cdp_x'].values, - y=df_seismic_resampled['cdp_y'].values), - data=df_seismic_resampled, - crs=crs_in).reset_index().drop(['twt', 'data'], axis=1) + gdf_seismic = ( + gpd.GeoDataFrame( + geometry=gpd.points_from_xy( + x=df_seismic_resampled["cdp_x"].values, + y=df_seismic_resampled["cdp_y"].values, + ), + data=df_seismic_resampled, + crs=crs_in, + ) + .reset_index() + .drop(["twt", "data"], axis=1) + ) # Returning only every nth point if filter: From 2a598bd4ecec866f9a774ccf1f9ba8cd7e433197 Mon Sep 17 00:00:00 2001 From: AlexanderJuestel Date: Mon, 22 Jul 2024 20:48:36 +0200 Subject: [PATCH 18/63] Format raster.py --- gemgis/raster.py | 1079 +++++++++++++++++++++++++++------------------- 1 file changed, 630 insertions(+), 449 deletions(-) diff --git a/gemgis/raster.py b/gemgis/raster.py index 20ed37cb..5eeb7e94 100644 --- a/gemgis/raster.py +++ b/gemgis/raster.py @@ -35,10 +35,12 @@ import pyproj -def sample_from_array(array: np.ndarray, - extent: Sequence[float], - point_x: Union[float, int, list, np.ndarray], - point_y: Union[float, int, list, np.ndarray], ) -> Union[np.ndarray, float]: +def sample_from_array( + array: np.ndarray, + extent: Sequence[float], + point_x: Union[float, int, list, np.ndarray], + point_y: Union[float, int, list, np.ndarray], +) -> Union[np.ndarray, float]: """Sampling the value of a np.ndarray at a given point and given the arrays true extent Parameters @@ -95,58 +97,58 @@ def sample_from_array(array: np.ndarray, # Checking is the array is a np.ndarray if not isinstance(array, np.ndarray): - raise TypeError('Object must be of type np.ndarray') + raise TypeError("Object must be of type np.ndarray") # Checking if the extent is a list if not isinstance(extent, Sequence): - raise TypeError('Extent must be of type list') + raise TypeError("Extent must be of type list") # Checking that the length of the list is either four or six if len(extent) not in [4, 6]: - raise ValueError('The extent must include only four or six values') + raise ValueError("The extent must include only four or six values") # Checking if the point coordinates are stored as a list if not isinstance(point_x, (list, np.ndarray, float, int)): - raise TypeError('Point_x must be of type list or np.ndarray') + raise TypeError("Point_x must be of type list or np.ndarray") # Checking if the point coordinates are stored as a list if not isinstance(point_y, (list, np.ndarray, float, int)): - raise TypeError('Point_y must be of type list or np.ndarray') + raise TypeError("Point_y must be of type list or np.ndarray") # Checking the length of the point list if not isinstance(point_x, (float, int)) and not isinstance(point_y, (float, int)): if len(point_x) != len(point_y): - raise ValueError('Length of both point lists/arrays must be equal') + raise ValueError("Length of both point lists/arrays must be equal") # Checking that all elements of the extent are of type int or float if not all(isinstance(n, (int, float)) for n in extent): - raise TypeError('Extent values must be of type int or float') + raise TypeError("Extent values must be of type int or float") # Checking that all elements of the point list are of type int or float if isinstance(point_x, (list, np.ndarray)): if not all(isinstance(n, (int, float, np.int32)) for n in point_x): - raise TypeError('Point values must be of type int or float') + raise TypeError("Point values must be of type int or float") # Checking that all elements of the point list are of type int or float if isinstance(point_y, (list, np.ndarray)): if not all(isinstance(n, (int, float)) for n in point_y): - raise TypeError('Point values must be of type int or float') + raise TypeError("Point values must be of type int or float") # Checking if the point is located within the provided extent if isinstance(point_x, (list, np.ndarray)): if any(x < extent[0] for x in point_x) or any(x > extent[1] for x in point_x): - raise ValueError('One or multiple points are located outside of the extent') + raise ValueError("One or multiple points are located outside of the extent") if isinstance(point_y, (list, np.ndarray)): if any(y < extent[2] for y in point_y) or any(y > extent[3] for y in point_y): - raise ValueError('One or multiple points are located outside of the extent') + raise ValueError("One or multiple points are located outside of the extent") # Checking if the point is located within the provided extent if isinstance(point_x, (float, int)): if point_x < extent[0] or point_x > extent[1]: - raise ValueError('One or multiple points are located outside of the extent') + raise ValueError("One or multiple points are located outside of the extent") if isinstance(point_y, (float, int)): if point_y < extent[2] or point_y > extent[3]: - raise ValueError('One or multiple points are located outside of the extent') + raise ValueError("One or multiple points are located outside of the extent") # Converting lists of coordinates to np.ndarrays if isinstance(point_x, list) and isinstance(point_y, list): @@ -154,15 +156,21 @@ def sample_from_array(array: np.ndarray, point_y = np.array(point_y) # Getting the column number based on the extent and shape of the array - column = np.int32(np.round((point_x - extent[0]) / (extent[1] - extent[0]) * array.shape[1])) + column = np.int32( + np.round((point_x - extent[0]) / (extent[1] - extent[0]) * array.shape[1]) + ) # Getting the row number based on the extent and shape of the array - row = np.int32(np.round((point_y - extent[2]) / (extent[3] - extent[2]) * array.shape[0])) + row = np.int32( + np.round((point_y - extent[2]) / (extent[3] - extent[2]) * array.shape[0]) + ) # Checking that all elements for the column and row numbers are of type int if isinstance(row, np.ndarray) and isinstance(column, np.ndarray): - if not all(isinstance(n, np.int32) for n in column) and not all(isinstance(n, np.int32) for n in row): - raise TypeError('Column and row values must be of type int for indexing') + if not all(isinstance(n, np.int32) for n in column) and not all( + isinstance(n, np.int32) for n in row + ): + raise TypeError("Column and row values must be of type int for indexing") # Flip array so that the column and row indices are correct array = np.flipud(array) @@ -178,11 +186,13 @@ def sample_from_array(array: np.ndarray, return sample -def sample_from_rasterio(raster: rasterio.io.DatasetReader, - point_x: Union[float, int, list, np.ndarray], - point_y: Union[float, int, list, np.ndarray], - sample_outside_extent: bool = True, - sample_all_bands: bool = False) -> Union[list, float]: +def sample_from_rasterio( + raster: rasterio.io.DatasetReader, + point_x: Union[float, int, list, np.ndarray], + point_y: Union[float, int, list, np.ndarray], + sample_outside_extent: bool = True, + sample_all_bands: bool = False, +) -> Union[list, float]: """Sampling the value of a rasterio object at a given point within the extent of the raster Parameters @@ -240,56 +250,68 @@ def sample_from_rasterio(raster: rasterio.io.DatasetReader, # Checking that the raster is a rasterio object if not isinstance(raster, rasterio.io.DatasetReader): - raise TypeError('Raster must be provided as rasterio object') + raise TypeError("Raster must be provided as rasterio object") # Checking if the point coordinates are stored as a list if not isinstance(point_x, (list, np.ndarray, float, int)): - raise TypeError('Point_x must be of type list or np.ndarray') + raise TypeError("Point_x must be of type list or np.ndarray") # Checking if the point coordinates are stored as a list if not isinstance(point_y, (list, np.ndarray, float, int)): - raise TypeError('Point_y must be of type list or np.ndarray') + raise TypeError("Point_y must be of type list or np.ndarray") # Checking the length of the point list if not isinstance(point_x, (float, int)) and not isinstance(point_y, (float, int)): if len(point_x) != len(point_y): - raise ValueError('Length of both point lists/arrays must be equal') + raise ValueError("Length of both point lists/arrays must be equal") # Checking that all elements of the point list are of type int or float if isinstance(point_x, (list, np.ndarray)): if not all(isinstance(n, (int, float, np.int32, np.float64)) for n in point_x): - raise TypeError('Point values must be of type int or float') + raise TypeError("Point values must be of type int or float") # Checking that all elements of the point list are of type int or float if isinstance(point_y, (list, np.ndarray)): if not all(isinstance(n, (int, float, np.int32, np.float64)) for n in point_y): - raise TypeError('Point values must be of type int or float') + raise TypeError("Point values must be of type int or float") # Checking that sample_outside_extent is of type bool if not isinstance(sample_outside_extent, bool): - raise TypeError('Sample_outside_extent argument must be of type bool') + raise TypeError("Sample_outside_extent argument must be of type bool") # Checking that sample_all_bands is of type bool if not isinstance(sample_all_bands, bool): - raise TypeError('Sample_all_bands argument must be of type bool') + raise TypeError("Sample_all_bands argument must be of type bool") # If sample_outside extent is true, a nodata value will be assigned if not sample_outside_extent: # Checking if the point is located within the provided raster extent if isinstance(point_x, (list, np.ndarray)): - if any(x < raster.bounds[0] for x in point_x) or any(x > raster.bounds[2] for x in point_x): - raise ValueError('One or multiple points are located outside of the extent') + if any(x < raster.bounds[0] for x in point_x) or any( + x > raster.bounds[2] for x in point_x + ): + raise ValueError( + "One or multiple points are located outside of the extent" + ) if isinstance(point_y, (list, np.ndarray)): - if any(y < raster.bounds[1] for y in point_y) or any(y > raster.bounds[3] for y in point_y): - raise ValueError('One or multiple points are located outside of the extent') + if any(y < raster.bounds[1] for y in point_y) or any( + y > raster.bounds[3] for y in point_y + ): + raise ValueError( + "One or multiple points are located outside of the extent" + ) # Checking if the point is located within the provided raster extent if isinstance(point_x, (float, int)): if point_x < raster.bounds[0] or point_x > raster.bounds[2]: - raise ValueError('One or multiple points are located outside of the extent') + raise ValueError( + "One or multiple points are located outside of the extent" + ) if isinstance(point_y, (float, int)): if point_y < raster.bounds[1] or point_y > raster.bounds[3]: - raise ValueError('One or multiple points are located outside of the extent') + raise ValueError( + "One or multiple points are located outside of the extent" + ) # Converting lists of coordinates to np.ndarrays if isinstance(point_x, list) and isinstance(point_y, list): @@ -314,10 +336,12 @@ def sample_from_rasterio(raster: rasterio.io.DatasetReader, return sample -def sample_randomly(raster: Union[np.ndarray, rasterio.io.DatasetReader], - n: int = 1, - extent: Optional[Sequence[float]] = None, - seed: int = None) -> tuple: +def sample_randomly( + raster: Union[np.ndarray, rasterio.io.DatasetReader], + n: int = 1, + extent: Optional[Sequence[float]] = None, + seed: int = None, +) -> tuple: """Sampling randomly from a raster (array or rasterio object) using sample_from_array or sample_from_rasterio and a randomly drawn point within the array/raster extent @@ -370,31 +394,31 @@ def sample_randomly(raster: Union[np.ndarray, rasterio.io.DatasetReader], # Checking if the array is of type np.ndarrays if not isinstance(raster, (np.ndarray, rasterio.io.DatasetReader)): - raise TypeError('Array must be of type np.ndarray') + raise TypeError("Array must be of type np.ndarray") # Checking that n is of type int if not isinstance(n, int): - raise TypeError('Number of samples n must be provided as int') + raise TypeError("Number of samples n must be provided as int") # Checking if seed is of type int if not isinstance(seed, (int, type(None))): - raise TypeError('Extent must be of type list') + raise TypeError("Extent must be of type list") # Checking that if a seed was provided that the seed is of type int if seed is not None: if not isinstance(seed, int): - raise TypeError('Seed must be of type int') + raise TypeError("Seed must be of type int") np.random.seed(seed) # Checking if extent is a list if not isinstance(extent, (list, type(None))): - raise TypeError('Extent must be of type list') + raise TypeError("Extent must be of type list") # Sampling from Array # Checking that all values are either ints or floats if isinstance(raster, np.ndarray): if not all(isinstance(n, (int, float)) for n in extent): - raise TypeError('Extent values must be of type int or float') + raise TypeError("Extent values must be of type int or float") # Drawing random values x and y within the provided extent x = np.random.uniform(extent[0], extent[1], n) @@ -402,9 +426,9 @@ def sample_randomly(raster: Union[np.ndarray, rasterio.io.DatasetReader], # Checking if the drawn values are floats if not isinstance(x, np.ndarray): - raise TypeError('x must be of type np.ndarray') + raise TypeError("x must be of type np.ndarray") if not isinstance(y, np.ndarray): - raise TypeError('y must be of type np.ndarray') + raise TypeError("y must be of type np.ndarray") # Sampling from the provided array and the random point sample = sample_from_array(array=raster, extent=extent, point_x=x, point_y=y) @@ -419,9 +443,9 @@ def sample_randomly(raster: Union[np.ndarray, rasterio.io.DatasetReader], # Checking if the drawn values are floats if not isinstance(x, np.ndarray): - raise TypeError('x must be of type np.ndarray') + raise TypeError("x must be of type np.ndarray") if not isinstance(y, np.ndarray): - raise TypeError('y must be of type np.ndarray') + raise TypeError("y must be of type np.ndarray") sample = sample_from_rasterio(raster=raster, point_x=x, point_y=y) @@ -434,15 +458,17 @@ def sample_randomly(raster: Union[np.ndarray, rasterio.io.DatasetReader], return sample, [x, y] -def sample_orientations(raster: Union[np.ndarray, rasterio.io.DatasetReader], - extent: List[Union[int, float]] = None, - point_x: Union[float, int, list, np.ndarray] = None, - point_y: Union[float, int, list, np.ndarray] = None, - random_samples: int = None, - formation: str = None, - seed: int = None, - sample_outside_extent: bool = False, - crs: Union[str, pyproj.crs.crs.CRS, rasterio.crs.CRS] = None) -> gpd.geodataframe.GeoDataFrame: +def sample_orientations( + raster: Union[np.ndarray, rasterio.io.DatasetReader], + extent: List[Union[int, float]] = None, + point_x: Union[float, int, list, np.ndarray] = None, + point_y: Union[float, int, list, np.ndarray] = None, + random_samples: int = None, + formation: str = None, + seed: int = None, + sample_outside_extent: bool = False, + crs: Union[str, pyproj.crs.crs.CRS, rasterio.crs.CRS] = None, +) -> gpd.geodataframe.GeoDataFrame: """Sampling orientations from a raster Parameters @@ -514,92 +540,115 @@ def sample_orientations(raster: Union[np.ndarray, rasterio.io.DatasetReader], # Checking if the rasterio of type np.ndarray or a rasterio object if not isinstance(raster, (np.ndarray, rasterio.io.DatasetReader)): - raise TypeError('Raster must be of type np.ndarray or a rasterio object') + raise TypeError("Raster must be of type np.ndarray or a rasterio object") # Checking if the extent is of type list if an array is provided if isinstance(raster, np.ndarray) and not isinstance(extent, list): - raise TypeError('Extent must be of type list when providing an array') + raise TypeError("Extent must be of type list when providing an array") # Checking that all elements of the extent are of type float or int - if isinstance(raster, np.ndarray) and not all(isinstance(n, (int, float)) for n in extent): - raise TypeError('Extent values must be of type int or float') + if isinstance(raster, np.ndarray) and not all( + isinstance(n, (int, float)) for n in extent + ): + raise TypeError("Extent values must be of type int or float") # Checking if the number of samples is of type int if point_x is None and point_y is None and not isinstance(random_samples, int): - raise TypeError('Number of samples must be of type int if no points are provided') + raise TypeError( + "Number of samples must be of type int if no points are provided" + ) # Checking if the points are of the correct type - if isinstance(random_samples, type(None)) and not isinstance(point_x, (float, int, list, np.ndarray)): - raise TypeError('Point_x must either be an int, float or a list or array of coordinates') + if isinstance(random_samples, type(None)) and not isinstance( + point_x, (float, int, list, np.ndarray) + ): + raise TypeError( + "Point_x must either be an int, float or a list or array of coordinates" + ) # Checking if the points are of the correct type - if isinstance(random_samples, type(None)) and not isinstance(point_y, (float, int, list, np.ndarray)): - raise TypeError('Point_y must either be an int, float or a list or array of coordinates') + if isinstance(random_samples, type(None)) and not isinstance( + point_y, (float, int, list, np.ndarray) + ): + raise TypeError( + "Point_y must either be an int, float or a list or array of coordinates" + ) # Checking if the seed is of type int if not isinstance(seed, (int, type(None))): - raise TypeError('Seed must be of type int') + raise TypeError("Seed must be of type int") # Checking that sampling outside extent is of type bool if not isinstance(sample_outside_extent, bool): - raise TypeError('Sampling_outside_extent must be of type bool') + raise TypeError("Sampling_outside_extent must be of type bool") # Checking that the crs is either a string or of type bool if not isinstance(crs, (str, pyproj.crs.crs.CRS, rasterio.crs.CRS, type(None))): - raise TypeError('CRS must be provided as string, pyproj or rasterio object') + raise TypeError("CRS must be provided as string, pyproj or rasterio object") # Calculate slope and aspect of raster - slope = calculate_slope(raster=raster, - extent=extent) + slope = calculate_slope(raster=raster, extent=extent) - aspect = calculate_aspect(raster=raster, - extent=extent) + aspect = calculate_aspect(raster=raster, extent=extent) # Sampling interfaces - gdf = sample_interfaces(raster=raster, - extent=extent, - point_x=point_x, - point_y=point_y, - random_samples=random_samples, - formation=formation, - seed=seed, - sample_outside_extent=sample_outside_extent, - crs=crs) + gdf = sample_interfaces( + raster=raster, + extent=extent, + point_x=point_x, + point_y=point_y, + random_samples=random_samples, + formation=formation, + seed=seed, + sample_outside_extent=sample_outside_extent, + crs=crs, + ) # Setting the array extent for the dip and azimuth sampling if isinstance(raster, rasterio.io.DatasetReader): - raster_extent = [raster.bounds[0], raster.bounds[2], raster.bounds[1], raster.bounds[3]] + raster_extent = [ + raster.bounds[0], + raster.bounds[2], + raster.bounds[1], + raster.bounds[3], + ] else: raster_extent = extent # Sampling dip and azimuth at the given locations - dip = sample_from_array(array=slope, - extent=raster_extent, - point_x=gdf['X'].values, - point_y=gdf['Y'].values) - - azimuth = sample_from_array(array=aspect, - extent=raster_extent, - point_x=gdf['X'].values, - point_y=gdf['Y'].values) + dip = sample_from_array( + array=slope, + extent=raster_extent, + point_x=gdf["X"].values, + point_y=gdf["Y"].values, + ) + + azimuth = sample_from_array( + array=aspect, + extent=raster_extent, + point_x=gdf["X"].values, + point_y=gdf["Y"].values, + ) # Adding columns to the GeoDataFrame - gdf['dip'] = dip - gdf['azimuth'] = azimuth - gdf['polarity'] = 1 + gdf["dip"] = dip + gdf["azimuth"] = azimuth + gdf["polarity"] = 1 return gdf -def sample_interfaces(raster: Union[np.ndarray, rasterio.io.DatasetReader], - extent: List[Union[int, float]] = None, - point_x: Union[float, int, list, np.ndarray] = None, - point_y: Union[float, int, list, np.ndarray] = None, - random_samples: int = None, - formation: str = None, - seed: int = None, - sample_outside_extent: bool = False, - crs: Union[str, pyproj.crs.crs.CRS, rasterio.crs.CRS] = None) -> gpd.geodataframe.GeoDataFrame: +def sample_interfaces( + raster: Union[np.ndarray, rasterio.io.DatasetReader], + extent: List[Union[int, float]] = None, + point_x: Union[float, int, list, np.ndarray] = None, + point_y: Union[float, int, list, np.ndarray] = None, + random_samples: int = None, + formation: str = None, + seed: int = None, + sample_outside_extent: bool = False, + crs: Union[str, pyproj.crs.crs.CRS, rasterio.crs.CRS] = None, +) -> gpd.geodataframe.GeoDataFrame: """Sampling interfaces from a raster Parameters @@ -671,60 +720,72 @@ def sample_interfaces(raster: Union[np.ndarray, rasterio.io.DatasetReader], # Checking if the rasterio of type np.ndarray or a rasterio object if not isinstance(raster, (np.ndarray, rasterio.io.DatasetReader)): - raise TypeError('Raster must be of type np.ndarray or a rasterio object') + raise TypeError("Raster must be of type np.ndarray or a rasterio object") # Checking if the extent is of type list if an array is provided if isinstance(raster, np.ndarray) and not isinstance(extent, list): - raise TypeError('Extent must be of type list when providing an array') + raise TypeError("Extent must be of type list when providing an array") # Checking that all elements of the extent are of type float or int - if isinstance(raster, np.ndarray) and not all(isinstance(n, (int, float)) for n in extent): - raise TypeError('Extent values must be of type int or float') + if isinstance(raster, np.ndarray) and not all( + isinstance(n, (int, float)) for n in extent + ): + raise TypeError("Extent values must be of type int or float") # Checking if the number of samples is of type int if point_x is None and point_y is None and not isinstance(random_samples, int): - raise TypeError('Number of samples must be of type int if no points are provided') + raise TypeError( + "Number of samples must be of type int if no points are provided" + ) # Checking if the points are of the correct type - if isinstance(random_samples, type(None)) and not isinstance(point_x, (float, int, list, np.ndarray)): - raise TypeError('Point_x must either be an int, float or a list or array of coordinates') + if isinstance(random_samples, type(None)) and not isinstance( + point_x, (float, int, list, np.ndarray) + ): + raise TypeError( + "Point_x must either be an int, float or a list or array of coordinates" + ) # Checking if the points are of the correct type - if isinstance(random_samples, type(None)) and not isinstance(point_y, (float, int, list, np.ndarray)): - raise TypeError('Point_y must either be an int, float or a list or array of coordinates') + if isinstance(random_samples, type(None)) and not isinstance( + point_y, (float, int, list, np.ndarray) + ): + raise TypeError( + "Point_y must either be an int, float or a list or array of coordinates" + ) # Checking if the seed is of type int if not isinstance(seed, (int, type(None))): - raise TypeError('Seed must be of type int') + raise TypeError("Seed must be of type int") # Checking that sampling outside extent is of type bool if not isinstance(sample_outside_extent, bool): - raise TypeError('Sampling_outside_extent must be of type bool') + raise TypeError("Sampling_outside_extent must be of type bool") # Checking that the crs is either a string or of type bool if not isinstance(crs, (str, pyproj.crs.crs.CRS, rasterio.crs.CRS, type(None))): - raise TypeError('CRS must be provided as string, pyproj CRS or rasterio CRS') + raise TypeError("CRS must be provided as string, pyproj CRS or rasterio CRS") # Sampling by points if random_samples is None and point_x is not None and point_y is not None: # Sampling from Raster if isinstance(raster, rasterio.io.DatasetReader): - z = sample_from_rasterio(raster=raster, - point_x=point_x, - point_y=point_y, - sample_outside_extent=sample_outside_extent) + z = sample_from_rasterio( + raster=raster, + point_x=point_x, + point_y=point_y, + sample_outside_extent=sample_outside_extent, + ) # Sampling from array else: - z = sample_from_array(array=raster, - extent=extent, - point_x=point_x, - point_y=point_y) + z = sample_from_array( + array=raster, extent=extent, point_x=point_x, point_y=point_y + ) # Sampling randomly elif random_samples is not None and point_x is None and point_y is None: - samples = sample_randomly(raster=raster, - n=random_samples, - extent=extent, - seed=seed) + samples = sample_randomly( + raster=raster, n=random_samples, extent=extent, seed=seed + ) # Assigning X, Y and Z values z = [i for i in samples[0]] @@ -734,31 +795,31 @@ def sample_interfaces(raster: Union[np.ndarray, rasterio.io.DatasetReader], point_y = [i for i in samples[1][1]] else: - raise TypeError('Either provide only lists or array of points or a number of random samples, not both.') + raise TypeError( + "Either provide only lists or array of points or a number of random samples, not both." + ) # Creating GeoDataFrame if isinstance(point_x, Iterable) and isinstance(point_y, Iterable): - gdf = gpd.GeoDataFrame(data=pd.DataFrame(data=[point_x, point_y, z]).T, - geometry=gpd.points_from_xy(x=point_x, - y=point_y, - crs=crs) - ) + gdf = gpd.GeoDataFrame( + data=pd.DataFrame(data=[point_x, point_y, z]).T, + geometry=gpd.points_from_xy(x=point_x, y=point_y, crs=crs), + ) else: - gdf = gpd.GeoDataFrame(data=pd.DataFrame(data=[point_x, point_y, z]).T, - geometry=gpd.points_from_xy(x=[point_x], - y=[point_y], - crs=crs) - ) + gdf = gpd.GeoDataFrame( + data=pd.DataFrame(data=[point_x, point_y, z]).T, + geometry=gpd.points_from_xy(x=[point_x], y=[point_y], crs=crs), + ) # Setting the column names - gdf.columns = ['X', 'Y', 'Z', 'geometry'] + gdf.columns = ["X", "Y", "Z", "geometry"] # Assigning formation name if formation is not None: if isinstance(formation, str): - gdf['formation'] = formation + gdf["formation"] = formation else: - raise TypeError('Formation must be provided as string or set to None') + raise TypeError("Formation must be provided as string or set to None") return gdf @@ -767,11 +828,13 @@ def sample_interfaces(raster: Union[np.ndarray, rasterio.io.DatasetReader], ############################### -def calculate_hillshades(raster: Union[np.ndarray, rasterio.io.DatasetReader], - extent: List[Union[int, float]] = None, - azdeg: Union[int, float] = 225, - altdeg: Union[int, float] = 45, - band_no: int = 1) -> np.ndarray: +def calculate_hillshades( + raster: Union[np.ndarray, rasterio.io.DatasetReader], + extent: List[Union[int, float]] = None, + azdeg: Union[int, float] = 225, + altdeg: Union[int, float] = 45, + band_no: int = 1, +) -> np.ndarray: """Calculating Hillshades based on digital elevation model/raster Parameters @@ -826,27 +889,27 @@ def calculate_hillshades(raster: Union[np.ndarray, rasterio.io.DatasetReader], # Checking that the raster is of type rasterio object or numpy array if not isinstance(raster, (np.ndarray, rasterio.io.DatasetReader)): - raise TypeError('Raster must be provided as rasterio object or NumPy array') + raise TypeError("Raster must be provided as rasterio object or NumPy array") # Checking if extent is of type list if not isinstance(extent, (type(None), list)): - raise TypeError('Extent must be of type list') + raise TypeError("Extent must be of type list") # Checking that altdeg is of type float or int if not isinstance(altdeg, (float, int)): - raise TypeError('altdeg must be of type int or float') + raise TypeError("altdeg must be of type int or float") # Checking that azdeg is of type float or int if not isinstance(azdeg, (float, int)): - raise TypeError('azdeg must be of type int or float') + raise TypeError("azdeg must be of type int or float") # Checking that altdeg is not out of bounds if altdeg > 90 or altdeg < 0: - raise ValueError('altdeg must be between 0 and 90 degrees') + raise ValueError("altdeg must be between 0 and 90 degrees") # Checking that azdeg is not out of bounds if azdeg > 360 or azdeg < 0: - raise ValueError('azdeg must be between 0 and 360 degrees') + raise ValueError("azdeg must be between 0 and 360 degrees") # Checking if object is rasterio object if isinstance(raster, rasterio.io.DatasetReader): @@ -861,24 +924,25 @@ def calculate_hillshades(raster: Union[np.ndarray, rasterio.io.DatasetReader], # Checking if object is of type np.ndarray if not isinstance(raster, np.ndarray): - raise TypeError('Input object must be of type np.ndarray') + raise TypeError("Input object must be of type np.ndarray") # Checking if dimension of array is correct if not raster.ndim == 2: - raise ValueError('Array must be of dimension 2') + raise ValueError("Array must be of dimension 2") # Calculate hillshades azdeg = 360 - azdeg x, y = np.gradient(raster) x = x / res[0] y = y / res[1] - slope = np.pi / 2. - np.arctan(np.sqrt(x * x + y * y)) + slope = np.pi / 2.0 - np.arctan(np.sqrt(x * x + y * y)) aspect = np.arctan2(-x, y) - azimuthrad = azdeg * np.pi / 180. - altituderad = altdeg * np.pi / 180. + azimuthrad = azdeg * np.pi / 180.0 + altituderad = altdeg * np.pi / 180.0 - shaded = np.sin(altituderad) * np.sin(slope) + np.cos(altituderad) * np.cos(slope) * np.cos( - (azimuthrad - np.pi / 2.) - aspect) + shaded = np.sin(altituderad) * np.sin(slope) + np.cos(altituderad) * np.cos( + slope + ) * np.cos((azimuthrad - np.pi / 2.0) - aspect) # Calculate color values hillshades = 255 * (shaded + 1) / 2 @@ -886,9 +950,11 @@ def calculate_hillshades(raster: Union[np.ndarray, rasterio.io.DatasetReader], return hillshades -def calculate_slope(raster: Union[np.ndarray, rasterio.io.DatasetReader], - extent: List[Union[int, float]] = None, - band_no: int = 1) -> np.ndarray: +def calculate_slope( + raster: Union[np.ndarray, rasterio.io.DatasetReader], + extent: List[Union[int, float]] = None, + band_no: int = 1, +) -> np.ndarray: """Calculating the slope based on digital elevation model/raster Parameters @@ -937,11 +1003,11 @@ def calculate_slope(raster: Union[np.ndarray, rasterio.io.DatasetReader], # Checking that the raster is of type rasterio object or numpy array if not isinstance(raster, (np.ndarray, rasterio.io.DatasetReader)): - raise TypeError('Raster must be provided as rasterio object or NumPy array') + raise TypeError("Raster must be provided as rasterio object or NumPy array") # Checking if extent is of type list if not isinstance(extent, (type(None), list)): - raise TypeError('Extent must be of type list') + raise TypeError("Extent must be of type list") # Checking if object is rasterio object if isinstance(raster, rasterio.io.DatasetReader): @@ -956,11 +1022,11 @@ def calculate_slope(raster: Union[np.ndarray, rasterio.io.DatasetReader], # Checking if object is of type np.ndarray if not isinstance(raster, np.ndarray): - raise TypeError('Input object must be of type np.ndarray') + raise TypeError("Input object must be of type np.ndarray") # Checking if dimension of array is correct if not raster.ndim == 2: - raise ValueError('Array must be of dimension 2') + raise ValueError("Array must be of dimension 2") # Calculate slope y, x = np.gradient(raster) @@ -972,9 +1038,11 @@ def calculate_slope(raster: Union[np.ndarray, rasterio.io.DatasetReader], return slope -def calculate_aspect(raster: Union[np.ndarray, rasterio.io.DatasetReader], - extent: List[Union[int, float]] = None, - band_no: int = 1) -> np.ndarray: +def calculate_aspect( + raster: Union[np.ndarray, rasterio.io.DatasetReader], + extent: List[Union[int, float]] = None, + band_no: int = 1, +) -> np.ndarray: """Calculating the aspect based on a digital elevation model/raster Parameters @@ -1023,11 +1091,11 @@ def calculate_aspect(raster: Union[np.ndarray, rasterio.io.DatasetReader], # Checking that the raster is of type rasterio object or numpy array if not isinstance(raster, (np.ndarray, rasterio.io.DatasetReader)): - raise TypeError('Raster must be provided as rasterio object or NumPy array') + raise TypeError("Raster must be provided as rasterio object or NumPy array") # Checking if extent is of type list if not isinstance(extent, (type(None), list)): - raise TypeError('Extent must be of type list') + raise TypeError("Extent must be of type list") # Checking if object is rasterio object if isinstance(raster, rasterio.io.DatasetReader): @@ -1042,11 +1110,11 @@ def calculate_aspect(raster: Union[np.ndarray, rasterio.io.DatasetReader], # Checking if object is of type np.ndarray if not isinstance(raster, np.ndarray): - raise TypeError('Input object must be of type np.ndarray') + raise TypeError("Input object must be of type np.ndarray") # Checking if dimension of array is correct if not raster.ndim == 2: - raise ValueError('Array must be of dimension 2') + raise ValueError("Array must be of dimension 2") # Calculate aspect y, x = np.gradient(raster) @@ -1059,9 +1127,11 @@ def calculate_aspect(raster: Union[np.ndarray, rasterio.io.DatasetReader], return aspect -def calculate_difference(raster1: Union[np.ndarray, rasterio.io.DatasetReader], - raster2: Union[np.ndarray, rasterio.io.DatasetReader], - flip_array: bool = False) -> np.ndarray: +def calculate_difference( + raster1: Union[np.ndarray, rasterio.io.DatasetReader], + raster2: Union[np.ndarray, rasterio.io.DatasetReader], + flip_array: bool = False, +) -> np.ndarray: """Calculating the difference between two rasters Parameters @@ -1111,14 +1181,16 @@ def calculate_difference(raster1: Union[np.ndarray, rasterio.io.DatasetReader], # Checking if array1 is of type np.ndarray or a rasterio object if not isinstance(raster1, (np.ndarray, rasterio.io.DatasetReader)): - raise TypeError('Raster1 must be of type np.ndarray or a rasterio object') + raise TypeError("Raster1 must be of type np.ndarray or a rasterio object") # Checking if array2 is of type np.ndarray or a rasterio object if not isinstance(raster2, (np.ndarray, rasterio.io.DatasetReader)): - raise TypeError('Raster2 must be of type np.ndarray or a rasterio object') + raise TypeError("Raster2 must be of type np.ndarray or a rasterio object") # Subtracting rasterio objects - if isinstance(raster1, rasterio.io.DatasetReader) and isinstance(raster2, rasterio.io.DatasetReader): + if isinstance(raster1, rasterio.io.DatasetReader) and isinstance( + raster2, rasterio.io.DatasetReader + ): array_diff = raster1.read() - raster2.read() else: @@ -1126,8 +1198,7 @@ def calculate_difference(raster1: Union[np.ndarray, rasterio.io.DatasetReader], if raster1.shape != raster2.shape: # Rescale array - array_rescaled = resize_by_array(raster=raster2, - array=raster1) + array_rescaled = resize_by_array(raster=raster2, array=raster1) # Flip array if flip_array is True if flip_array: array_rescaled = np.flipud(array_rescaled) @@ -1150,13 +1221,15 @@ def calculate_difference(raster1: Union[np.ndarray, rasterio.io.DatasetReader], ###################### -def clip_by_bbox(raster: Union[rasterio.io.DatasetReader, np.ndarray], - bbox: List[Union[int, float]], - raster_extent: List[Union[int, float]] = None, - save_clipped_raster: bool = False, - path: str = 'raster_clipped.tif', - overwrite_file: bool = False, - create_directory: bool = False) -> np.ndarray: +def clip_by_bbox( + raster: Union[rasterio.io.DatasetReader, np.ndarray], + bbox: List[Union[int, float]], + raster_extent: List[Union[int, float]] = None, + save_clipped_raster: bool = False, + path: str = "raster_clipped.tif", + overwrite_file: bool = False, + create_directory: bool = False, +) -> np.ndarray: """Clipping a rasterio raster or np.ndarray by a given extent Parameters @@ -1224,23 +1297,23 @@ def clip_by_bbox(raster: Union[rasterio.io.DatasetReader, np.ndarray], # Checking that the raster is of type np.ndarray or a rasterio object if not isinstance(raster, (np.ndarray, rasterio.io.DatasetReader)): - raise TypeError('Raster must be of type np.ndarray or a rasterio object') + raise TypeError("Raster must be of type np.ndarray or a rasterio object") # Checking that the extent is of type list if not isinstance(bbox, list): - raise TypeError('Extent must be of type list') + raise TypeError("Extent must be of type list") # Checking that all values are either ints or floats if not all(isinstance(n, (int, float)) for n in bbox): - raise TypeError('Bounds values must be of type int or float') + raise TypeError("Bounds values must be of type int or float") # Checking that save_clipped_raster is of type bool if not isinstance(save_clipped_raster, bool): - raise TypeError('save_clipped_raster must either be True or False') + raise TypeError("save_clipped_raster must either be True or False") # Checking that the path is of type string if not isinstance(path, str): - raise TypeError('The path must be provided as string') + raise TypeError("The path must be provided as string") # Getting the absolute path path = os.path.abspath(path=path) @@ -1257,75 +1330,110 @@ def clip_by_bbox(raster: Union[rasterio.io.DatasetReader, np.ndarray], if create_directory: os.makedirs(path_dir) else: - raise LookupError('Directory not found. Pass create_directory=True to create a new directory') + raise LookupError( + "Directory not found. Pass create_directory=True to create a new directory" + ) if not overwrite_file: if os.path.exists(path): - raise FileExistsError("The file already exists. Pass overwrite_file=True to overwrite the existing file") + raise FileExistsError( + "The file already exists. Pass overwrite_file=True to overwrite the existing file" + ) # Checking if raster is rasterio object if isinstance(raster, rasterio.io.DatasetReader): - raster_clipped, raster_transform = rasterio.mask.mask(dataset=raster, - shapes=[Polygon([(bbox[0], bbox[2]), - (bbox[1], bbox[2]), - (bbox[1], bbox[3]), - (bbox[0], bbox[3])])], - crop=True, - filled=False, - pad=False, - pad_width=0) + raster_clipped, raster_transform = rasterio.mask.mask( + dataset=raster, + shapes=[ + Polygon( + [ + (bbox[0], bbox[2]), + (bbox[1], bbox[2]), + (bbox[1], bbox[3]), + (bbox[0], bbox[3]), + ] + ) + ], + crop=True, + filled=False, + pad=False, + pad_width=0, + ) # Saving the raster if save_clipped_raster: # Updating meta data raster_clipped_meta = raster.meta - raster_clipped_meta.update({"driver": "GTiff", - "height": raster_clipped.shape[1], - "width": raster_clipped.shape[2], - "transform": raster_transform}) + raster_clipped_meta.update( + { + "driver": "GTiff", + "height": raster_clipped.shape[1], + "width": raster_clipped.shape[2], + "transform": raster_transform, + } + ) # Writing the file with rasterio.open(path, "w", **raster_clipped_meta) as dest: dest.write(raster_clipped) # Swap axes and remove dimension - raster_clipped = np.flipud(np.rot90(np.swapaxes(raster_clipped, 0, 2)[:, :, 0], 1)) + raster_clipped = np.flipud( + np.rot90(np.swapaxes(raster_clipped, 0, 2)[:, :, 0], 1) + ) else: # Checking that the extent is provided as list if not isinstance(raster_extent, list): - raise TypeError('The raster extent must be provided as list of corner values') + raise TypeError( + "The raster extent must be provided as list of corner values" + ) # Checking that all values are either ints or floats if not all(isinstance(n, (int, float)) for n in raster_extent): - raise TypeError('Bounds values must be of type int or float') + raise TypeError("Bounds values must be of type int or float") # Create column and row indices for clipping - column1 = int((bbox[0] - raster_extent[0]) / (raster_extent[1] - raster_extent[0]) * raster.shape[1]) - row1 = int((bbox[1] - raster_extent[2]) / (raster_extent[3] - raster_extent[2]) * raster.shape[0]) - column2 = int((bbox[2] - raster_extent[0]) / (raster_extent[1] - raster_extent[0]) * raster.shape[1]) - row2 = int((bbox[3] - raster_extent[2]) / (raster_extent[3] - raster_extent[2]) * raster.shape[0]) + column1 = int( + (bbox[0] - raster_extent[0]) + / (raster_extent[1] - raster_extent[0]) + * raster.shape[1] + ) + row1 = int( + (bbox[1] - raster_extent[2]) + / (raster_extent[3] - raster_extent[2]) + * raster.shape[0] + ) + column2 = int( + (bbox[2] - raster_extent[0]) + / (raster_extent[1] - raster_extent[0]) + * raster.shape[1] + ) + row2 = int( + (bbox[3] - raster_extent[2]) + / (raster_extent[3] - raster_extent[2]) + * raster.shape[0] + ) # Clip raster raster_clipped = raster[column1:row1, column2:row2] # Save raster if save_clipped_raster: - save_as_tiff(raster=raster_clipped, - path=path, - extent=bbox, - crs='EPSG:4326') + save_as_tiff(raster=raster_clipped, path=path, extent=bbox, crs="EPSG:4326") return raster_clipped -def clip_by_polygon(raster: Union[rasterio.io.DatasetReader, np.ndarray], - polygon: shapely.geometry.polygon.Polygon, - raster_extent: List[Union[int, float]] = None, - save_clipped_raster: bool = False, - path: str = 'raster_clipped.tif', - overwrite_file: bool = False, - create_directory: bool = False) -> np.ndarray: +def clip_by_polygon( + raster: Union[rasterio.io.DatasetReader, np.ndarray], + polygon: shapely.geometry.polygon.Polygon, + raster_extent: List[Union[int, float]] = None, + save_clipped_raster: bool = False, + path: str = "raster_clipped.tif", + overwrite_file: bool = False, + create_directory: bool = False, +) -> np.ndarray: """Clipping/masking a rasterio raster or np.ndarray by a given shapely Polygon Parameters @@ -1394,19 +1502,19 @@ def clip_by_polygon(raster: Union[rasterio.io.DatasetReader, np.ndarray], # Checking that the raster is of type np.ndarray or a rasterio object if not isinstance(raster, (np.ndarray, rasterio.io.DatasetReader)): - raise TypeError('Raster must be of type np.ndarray or a rasterio object') + raise TypeError("Raster must be of type np.ndarray or a rasterio object") # Checking that the polygon is a Shapely Polygon if not isinstance(polygon, shapely.geometry.polygon.Polygon): - raise TypeError('Polygon must be a Shapely Polygon') + raise TypeError("Polygon must be a Shapely Polygon") # Checking that save_clipped_raster is of type bool if not isinstance(save_clipped_raster, bool): - raise TypeError('save_clipped_raster must either be True or False') + raise TypeError("save_clipped_raster must either be True or False") # Checking that the path is of type string if not isinstance(path, str): - raise TypeError('The path must be provided as string') + raise TypeError("The path must be provided as string") # Getting the absolute path path = os.path.abspath(path=path) @@ -1423,48 +1531,66 @@ def clip_by_polygon(raster: Union[rasterio.io.DatasetReader, np.ndarray], if create_directory: os.makedirs(path_dir) else: - raise LookupError('Directory not found. Pass create_directory=True to create a new directory') + raise LookupError( + "Directory not found. Pass create_directory=True to create a new directory" + ) if not overwrite_file: if os.path.exists(path): raise FileExistsError( - "The file already exists. Pass overwrite_file=True to overwrite the existing file") + "The file already exists. Pass overwrite_file=True to overwrite the existing file" + ) # Masking raster if isinstance(raster, rasterio.io.DatasetReader): - raster_clipped, raster_transform = rasterio.mask.mask(dataset=raster, - shapes=[polygon], - crop=True, - filled=False, - pad=False, - pad_width=0) + raster_clipped, raster_transform = rasterio.mask.mask( + dataset=raster, + shapes=[polygon], + crop=True, + filled=False, + pad=False, + pad_width=0, + ) # Saving the raster if save_clipped_raster: # Updating meta data raster_clipped_meta = raster.meta - raster_clipped_meta.update({"driver": "GTiff", - "height": raster_clipped.shape[1], - "width": raster_clipped.shape[2], - "transform": raster_transform}) + raster_clipped_meta.update( + { + "driver": "GTiff", + "height": raster_clipped.shape[1], + "width": raster_clipped.shape[2], + "transform": raster_transform, + } + ) # Writing the raster to file with rasterio.open(path, "w", **raster_clipped_meta) as dest: dest.write(raster_clipped) # Swap axes and remove dimension - raster_clipped = np.flipud(np.rot90(np.swapaxes(raster_clipped, 0, 2)[:, :, 0], 1)) + raster_clipped = np.flipud( + np.rot90(np.swapaxes(raster_clipped, 0, 2)[:, :, 0], 1) + ) else: # Converting the polygon to a rectangular bbox - bbox = [polygon.bounds[0], polygon.bounds[2], polygon.bounds[1], polygon.bounds[2]] + bbox = [ + polygon.bounds[0], + polygon.bounds[2], + polygon.bounds[1], + polygon.bounds[2], + ] # Clipping raster - raster_clipped = clip_by_bbox(raster=raster, - bbox=bbox, - raster_extent=raster_extent, - save_clipped_raster=save_clipped_raster, - path=path) + raster_clipped = clip_by_bbox( + raster=raster, + bbox=bbox, + raster_extent=raster_extent, + save_clipped_raster=save_clipped_raster, + path=path, + ) return raster_clipped @@ -1473,8 +1599,10 @@ def clip_by_polygon(raster: Union[rasterio.io.DatasetReader, np.ndarray], ###################### -def resize_by_array(raster: Union[np.ndarray, rasterio.io.DatasetReader], - array: Union[np.ndarray, rasterio.io.DatasetReader]) -> np.ndarray: +def resize_by_array( + raster: Union[np.ndarray, rasterio.io.DatasetReader], + array: Union[np.ndarray, rasterio.io.DatasetReader], +) -> np.ndarray: """Rescaling raster to the size of another raster Parameters @@ -1524,23 +1652,23 @@ def resize_by_array(raster: Union[np.ndarray, rasterio.io.DatasetReader], # Checking if array1 is of type np.ndarray if not isinstance(raster, (np.ndarray, rasterio.io.DatasetReader)): - raise TypeError('Raster must be of type np.ndarray or a rasterio object') + raise TypeError("Raster must be of type np.ndarray or a rasterio object") # Checking if array2 is of type np.ndarray if not isinstance(array, (np.ndarray, rasterio.io.DatasetReader)): - raise TypeError('array must be of type np.ndarray or a rasterio object') + raise TypeError("array must be of type np.ndarray or a rasterio object") # Resize raster by shape of array - array_resized = resize_raster(raster=raster, - width=array.shape[1], - height=array.shape[0]) + array_resized = resize_raster( + raster=raster, width=array.shape[1], height=array.shape[0] + ) return array_resized -def resize_raster(raster: Union[np.ndarray, rasterio.io.DatasetReader], - width: int, - height: int) -> np.ndarray: +def resize_raster( + raster: Union[np.ndarray, rasterio.io.DatasetReader], width: int, height: int +) -> np.ndarray: """Resizing raster to given dimensions Parameters @@ -1591,11 +1719,12 @@ def resize_raster(raster: Union[np.ndarray, rasterio.io.DatasetReader], from skimage.transform import resize except ModuleNotFoundError: raise ModuleNotFoundError( - 'Scikit Image package is not installed. Use pip install scikit-image to install the latest version') + "Scikit Image package is not installed. Use pip install scikit-image to install the latest version" + ) # Checking if array1 is of type np.ndarray if not isinstance(raster, (np.ndarray, rasterio.io.DatasetReader)): - raise TypeError('Raster must be of type np.ndarray') + raise TypeError("Raster must be of type np.ndarray") # Converting rasterio object to array if isinstance(raster, rasterio.io.DatasetReader): @@ -1603,14 +1732,13 @@ def resize_raster(raster: Union[np.ndarray, rasterio.io.DatasetReader], # Checking if dimensions are of type int if not isinstance(width, int): - raise TypeError('Width must be of type int') + raise TypeError("Width must be of type int") if not isinstance(height, int): - raise TypeError('Height must be of type int') + raise TypeError("Height must be of type int") # Resizing the array - array_resized = resize(image=raster, - output_shape=(height, width)) + array_resized = resize(image=raster, output_shape=(height, width)) return array_resized @@ -1619,10 +1747,7 @@ def resize_raster(raster: Union[np.ndarray, rasterio.io.DatasetReader], ############################################# # Defining dtype Conversion -dtype_conversion = { - "Integer": np.int32, - "Double": np.float64 -} +dtype_conversion = {"Integer": np.int32, "Double": np.float64} def read_msh(path: Union[str, Path]) -> Dict[str, np.ndarray]: @@ -1675,7 +1800,7 @@ def read_msh(path: Union[str, Path]) -> Dict[str, np.ndarray]: # Checking that the path is of type string or a path if not isinstance(path, (str, Path)): - raise TypeError('Path must be of type string') + raise TypeError("Path must be of type string") # Getting the absolute path path = os.path.abspath(path=path) @@ -1693,15 +1818,16 @@ def read_msh(path: Union[str, Path]) -> Dict[str, np.ndarray]: f.seek(header_end + 0x14) # Extracting data from each line - for line in chunk[chunk.find(b"[index]") + 8:header_end].decode("utf-8").strip().split("\n"): + for line in ( + chunk[chunk.find(b"[index]") + 8 : header_end] + .decode("utf-8") + .strip() + .split("\n") + ): name, dtype, *shape = line.strip().rstrip(";").split() shape = list(map(int, reversed(shape))) dtype = dtype_conversion[dtype] - data[name] = np.fromfile( - f, - dtype, - np.prod(shape) - ).reshape(shape) + data[name] = np.fromfile(f, dtype, np.prod(shape)).reshape(shape) return data @@ -1760,7 +1886,7 @@ def read_ts(path: Union[str, Path]) -> Tuple[list, list]: # Checking that the path is of type string or a path if not isinstance(path, (str, Path)): - raise TypeError('Path must be of type string') + raise TypeError("Path must be of type string") # Getting the absolute path path = os.path.abspath(path=path) @@ -1867,7 +1993,7 @@ def read_asc(path: Union[str, Path]) -> dict: # Checking that the path is of type string or a path if not isinstance(path, (str, Path)): - raise TypeError('Path must be of type string') + raise TypeError("Path must be of type string") # Getting the absolute path path = os.path.abspath(path=path) @@ -1882,21 +2008,21 @@ def read_asc(path: Union[str, Path]) -> dict: if not line.strip(): continue line_value, *values = line.split() - if line_value == 'ncols': + if line_value == "ncols": ncols = int(values[0]) - if line_value == 'nrows': + if line_value == "nrows": nrows = int(values[0]) - if line_value == 'xllcenter': + if line_value == "xllcenter": xllcenter = float(values[0]) - if line_value == 'yllcenter': + if line_value == "yllcenter": yllcenter = float(values[0]) - if line_value == 'cellsize': + if line_value == "cellsize": res = float(values[0]) - if line_value == 'xllcorner': + if line_value == "xllcorner": xllcenter = float(values[0]) + 0.5 * res - if line_value == 'yllcorner': + if line_value == "yllcorner": yllcenter = float(values[0]) + 0.5 * res - if line_value == 'NODATA_value' or line_value == 'nodata_value': + if line_value == "NODATA_value" or line_value == "nodata_value": nodata_val = float(values[0]) # Load data and replace nodata_values with np.nan @@ -1904,10 +2030,17 @@ def read_asc(path: Union[str, Path]) -> dict: data[data == nodata_val] = np.nan # Creating dict and store data - data = {'Data': data, - 'Extent': [xllcenter, xllcenter + res * ncols, yllcenter, yllcenter + res * nrows], - 'Resolution': res, - 'Nodata_val': np.nan} + data = { + "Data": data, + "Extent": [ + xllcenter, + xllcenter + res * ncols, + yllcenter, + yllcenter + res * nrows, + ], + "Resolution": res, + "Nodata_val": np.nan, + } return data @@ -1965,7 +2098,7 @@ def read_zmap(path: Union[str, Path]) -> dict: # Checking that the path is of type string or a path if not isinstance(path, (str, Path)): - raise TypeError('Path must be of type string') + raise TypeError("Path must be of type string") # Getting the absolute path path = os.path.abspath(path=path) @@ -2006,20 +2139,22 @@ def read_zmap(path: Union[str, Path]) -> dict: # Getting array data data = [ - (float(d) if d.strip() != nodata else np.nan) for line in f for d in line.split() + (float(d) if d.strip() != nodata else np.nan) + for line in f + for d in line.split() ] # Creating dict for data data = { - 'Data': np.array(data).reshape((nrows, ncols), order="F"), - 'Extent': extent, - 'Resolution': resolution, - 'Nodata_val': float(nodata), - 'Dimensions': (nrows, ncols), - 'CRS': crs, - 'Creation_date': creation_date, - 'Creation_time': creation_time, - 'File_name': zmap_file_name + "Data": np.array(data).reshape((nrows, ncols), order="F"), + "Extent": extent, + "Resolution": resolution, + "Nodata_val": float(nodata), + "Dimensions": (nrows, ncols), + "CRS": crs, + "Creation_date": creation_date, + "Creation_time": creation_time, + "File_name": zmap_file_name, } return data @@ -2029,14 +2164,16 @@ def read_zmap(path: Union[str, Path]) -> dict: ################################ -def save_as_tiff(raster: np.ndarray, - path: str, - extent: Union[List[Union[int, float]], Tuple[Union[int, float]]], - crs: Union[str, pyproj.crs.crs.CRS, rasterio.crs.CRS], - nodata: Union[float, int] = None, - transform=None, - overwrite_file: bool = False, - create_directory: bool = False): +def save_as_tiff( + raster: np.ndarray, + path: str, + extent: Union[List[Union[int, float]], Tuple[Union[int, float]]], + crs: Union[str, pyproj.crs.crs.CRS, rasterio.crs.CRS], + nodata: Union[float, int] = None, + transform=None, + overwrite_file: bool = False, + create_directory: bool = False, +): """Saving a np.array as tif file Parameters @@ -2091,7 +2228,7 @@ def save_as_tiff(raster: np.ndarray, # Checking if path is of type string if not isinstance(path, str): - raise TypeError('Path must be of type string') + raise TypeError("Path must be of type string") # Checking that the file has the correct file ending if not path.endswith(".tif"): @@ -2108,57 +2245,62 @@ def save_as_tiff(raster: np.ndarray, if create_directory: os.makedirs(path_dir) else: - raise LookupError('Directory not found. Pass create_directory=True to create a new directory') + raise LookupError( + "Directory not found. Pass create_directory=True to create a new directory" + ) if not overwrite_file: if os.path.exists(path): raise FileExistsError( - "The file already exists. Pass overwrite_file=True to overwrite the existing file") + "The file already exists. Pass overwrite_file=True to overwrite the existing file" + ) # Checking if the array is of type np.ndarray if not isinstance(raster, np.ndarray): - raise TypeError('array must be of type np.ndarray') + raise TypeError("array must be of type np.ndarray") # Checking if the extent is of type list if not isinstance(extent, (list, tuple)): - raise TypeError('Extent must be of type list or be a tuple') + raise TypeError("Extent must be of type list or be a tuple") # Checking that all values are either ints or floats if not all(isinstance(n, (int, float)) for n in extent): - raise TypeError('Bound values must be of type int or float') + raise TypeError("Bound values must be of type int or float") # Checking if the crs is of type string if not isinstance(crs, (str, pyproj.crs.crs.CRS, rasterio.crs.CRS, dict)): - raise TypeError('CRS must be of type string, dict, rasterio CRS or pyproj CRS') + raise TypeError("CRS must be of type string, dict, rasterio CRS or pyproj CRS") # Extracting the bounds minx, miny, maxx, maxy = extent[0], extent[2], extent[1], extent[3] # Creating the transform if not transform: - transform = rasterio.transform.from_bounds(minx, miny, maxx, maxy, raster.shape[1], raster.shape[0]) + transform = rasterio.transform.from_bounds( + minx, miny, maxx, maxy, raster.shape[1], raster.shape[0] + ) # Creating and saving the array as tiff with rasterio.open( - path, - 'w', - driver='GTiff', - height=raster.shape[0], - width=raster.shape[1], - count=1, - dtype=raster.dtype, - crs=crs, - transform=transform, - nodata=nodata + path, + "w", + driver="GTiff", + height=raster.shape[0], + width=raster.shape[1], + count=1, + dtype=raster.dtype, + crs=crs, + transform=transform, + nodata=nodata, ) as dst: dst.write(np.flipud(raster), 1) - print('Raster successfully saved') + print("Raster successfully saved") -def create_filepaths(dirpath: str, - search_criteria: str, - create_directory: bool = False) -> List[str]: +def create_filepaths( + dirpath: str, search_criteria: str, create_directory: bool = False +) -> List[str]: """Retrieving the file paths of the tiles to load and to process them later Parameters @@ -2204,7 +2346,7 @@ def create_filepaths(dirpath: str, # Checking if dirpath is of type string if not isinstance(dirpath, str): - raise TypeError('Path to directory must be of type string') + raise TypeError("Path to directory must be of type string") # Getting the absolute path dirpath = os.path.abspath(path=dirpath) @@ -2217,11 +2359,13 @@ def create_filepaths(dirpath: str, if create_directory: os.makedirs(path_dir) else: - raise LookupError('Directory not found. Pass create_directory=True to create a new directory') + raise LookupError( + "Directory not found. Pass create_directory=True to create a new directory" + ) # Checking that the search criterion is of type string if not isinstance(search_criteria, str): - raise TypeError('Search Criterion must be of Type string') + raise TypeError("Search Criterion must be of Type string") # Join paths to form path to files source = os.path.join(dirpath, search_criteria) @@ -2232,10 +2376,12 @@ def create_filepaths(dirpath: str, return filepaths -def create_src_list(dirpath: str = '', - search_criteria: str = '', - filepaths: List[str] = None, - create_directory: bool = False) -> List[rasterio.io.DatasetReader]: +def create_src_list( + dirpath: str = "", + search_criteria: str = "", + filepaths: List[str] = None, + create_directory: bool = False, +) -> List[rasterio.io.DatasetReader]: """Creating a list of source files Parameters @@ -2292,7 +2438,7 @@ def create_src_list(dirpath: str = '', # Checking if dirpath is of type string if not isinstance(dirpath, str): - raise TypeError('Path to directory must be of type string') + raise TypeError("Path to directory must be of type string") # Getting the absolute path dirpath = os.path.abspath(path=dirpath) @@ -2305,24 +2451,27 @@ def create_src_list(dirpath: str = '', if create_directory: os.makedirs(path_dir) else: - raise LookupError('Directory not found. Pass create_directory=True to create a new directory') + raise LookupError( + "Directory not found. Pass create_directory=True to create a new directory" + ) # Checking that the search criterion is of type string if not isinstance(search_criteria, str): - raise TypeError('Search Criterion must be of Type string') + raise TypeError("Search Criterion must be of Type string") # Checking that the filepaths are of type list if not isinstance(filepaths, (list, type(None))): - raise TypeError('Filepaths must be of type list') + raise TypeError("Filepaths must be of type list") # Retrieving the file paths of the tiles - if not dirpath == '': - if not search_criteria == '': + if not dirpath == "": + if not search_criteria == "": if not filepaths: - filepaths = create_filepaths(dirpath=dirpath, - search_criteria=search_criteria) + filepaths = create_filepaths( + dirpath=dirpath, search_criteria=search_criteria + ) else: - raise ValueError('Either provide a file path or a list of filepaths') + raise ValueError("Either provide a file path or a list of filepaths") # Create empty list for source files src_files = [] @@ -2337,13 +2486,15 @@ def create_src_list(dirpath: str = '', return src_files -def merge_tiles(src_files: List[rasterio.io.DatasetReader], - extent: List[Union[float, int]] = None, - res: int = None, - nodata: Union[float, int] = None, - precision: int = None, - indices: int = None, - method: str = 'first') -> Tuple[np.ndarray, affine.Affine]: +def merge_tiles( + src_files: List[rasterio.io.DatasetReader], + extent: List[Union[float, int]] = None, + res: int = None, + nodata: Union[float, int] = None, + precision: int = None, + indices: int = None, + method: str = "first", +) -> Tuple[np.ndarray, affine.Affine]: """Merging downloaded tiles to mosaic Parameters @@ -2433,45 +2584,47 @@ def merge_tiles(src_files: List[rasterio.io.DatasetReader], # Checking if source files are stored in a list if not isinstance(src_files, list): - raise TypeError('Files must be stored as list') + raise TypeError("Files must be stored as list") # Checking if extent is a list if not isinstance(extent, (list, type(None))): - raise TypeError('Extent must be of type list') + raise TypeError("Extent must be of type list") # Checking that all values are either ints or floats if extent: if not all(isinstance(n, (int, float)) for n in extent): - raise TypeError('Extent values must be of type int or float') + raise TypeError("Extent values must be of type int or float") # Checking that the resolution is of type int if not isinstance(res, (int, type(None))): - raise TypeError('Resolution must be of type int') + raise TypeError("Resolution must be of type int") # Checking that the nodata value is of type int or float if not isinstance(nodata, (int, float, type(None))): - raise TypeError('Nodata value must be of type int or float') + raise TypeError("Nodata value must be of type int or float") # Checking that the precision is of type int if not isinstance(precision, (int, type(None))): - raise TypeError('Precision value must be of type int') + raise TypeError("Precision value must be of type int") # Checking that the indices for the bands are of type int if not isinstance(indices, (int, type(None))): - raise TypeError('Band indices must be of type int') + raise TypeError("Band indices must be of type int") # Checking that the method is of type string if not isinstance(method, (str, type(None))): - raise TypeError('Type of method must be provided as string') + raise TypeError("Type of method must be provided as string") # Merging tiles - mosaic, transformation = merge(src_files, - bounds=extent, - res=res, - nodata=nodata, - precision=precision, - indexes=indices, - method=method) + mosaic, transformation = merge( + src_files, + bounds=extent, + res=res, + nodata=nodata, + precision=precision, + indexes=indices, + method=method, + ) # Swap axes and remove dimension mosaic = np.flipud(np.rot90(np.swapaxes(mosaic, 0, 2)[:, 0:, 0], 1)) @@ -2479,11 +2632,13 @@ def merge_tiles(src_files: List[rasterio.io.DatasetReader], return mosaic, transformation -def reproject_raster(path_in: str, - path_out: str, - dst_crs: Union[str, pyproj.crs.crs.CRS, rasterio.crs.CRS], - overwrite_file: bool = False, - create_directory: bool = False): +def reproject_raster( + path_in: str, + path_out: str, + dst_crs: Union[str, pyproj.crs.crs.CRS, rasterio.crs.CRS], + overwrite_file: bool = False, + create_directory: bool = False, +): """Reprojecting a raster into different CRS Parameters @@ -2524,7 +2679,7 @@ def reproject_raster(path_in: str, # Checking that the path_in is of type string if not isinstance(path_in, str): - raise TypeError('The path of the source file must be of type string') + raise TypeError("The path of the source file must be of type string") # Getting the absolute path path_in = os.path.abspath(path=path_in) @@ -2535,7 +2690,7 @@ def reproject_raster(path_in: str, # Checking that the path_out is type string if not isinstance(path_out, str): - raise TypeError('The path of the destination file must be of type string') + raise TypeError("The path of the destination file must be of type string") # Getting the absolute path path_out = os.path.abspath(path=path_out) @@ -2552,31 +2707,34 @@ def reproject_raster(path_in: str, if create_directory: os.makedirs(path_dir_out) else: - raise LookupError('Directory not found. Pass create_directory=True to create a new directory') + raise LookupError( + "Directory not found. Pass create_directory=True to create a new directory" + ) if not overwrite_file: if os.path.exists(path_out): raise FileExistsError( - "The file already exists. Pass overwrite_file=True to overwrite the existing file") + "The file already exists. Pass overwrite_file=True to overwrite the existing file" + ) # Checking that the dst_crs is of type string or a pyproj object if not isinstance(dst_crs, (str, pyproj.crs.crs.CRS, rasterio.crs.CRS)): - raise TypeError('The destination CRS must be of type string, pyproj CRS or rasterio CRS') + raise TypeError( + "The destination CRS must be of type string, pyproj CRS or rasterio CRS" + ) # Opening the Source DataSet with rasterio.open(path_in) as src: transform, width, height = calculate_default_transform( - src.crs, dst_crs, src.width, src.height, *src.bounds) + src.crs, dst_crs, src.width, src.height, *src.bounds + ) kwargs = src.meta.copy() - kwargs.update({ - 'crs': dst_crs, - 'transform': transform, - 'width': width, - 'height': height - }) + kwargs.update( + {"crs": dst_crs, "transform": transform, "width": width, "height": height} + ) # Writing the Destination DataSet - with rasterio.open(path_out, 'w', **kwargs) as dst: + with rasterio.open(path_out, "w", **kwargs) as dst: for i in range(1, src.count + 1): reproject( source=rasterio.band(src, i), @@ -2585,14 +2743,16 @@ def reproject_raster(path_in: str, src_crs=src.crs, dst_transform=transform, dst_crs=dst_crs, - resampling=Resampling.nearest) + resampling=Resampling.nearest, + ) -def extract_contour_lines_from_raster(raster: Union[rasterio.io.DatasetReader, np.ndarray, str], - interval: int, - extent: Union[Optional[Sequence[float]], Optional[Sequence[int]]] = None, - target_crs: Union[ - str, pyproj.crs.crs.CRS, rasterio.crs.CRS] = None) -> gpd.GeoDataFrame: +def extract_contour_lines_from_raster( + raster: Union[rasterio.io.DatasetReader, np.ndarray, str], + interval: int, + extent: Union[Optional[Sequence[float]], Optional[Sequence[int]]] = None, + target_crs: Union[str, pyproj.crs.crs.CRS, rasterio.crs.CRS] = None, +) -> gpd.GeoDataFrame: """Extracting contour lines from raster with a provided interval. Parameters @@ -2624,11 +2784,14 @@ def extract_contour_lines_from_raster(raster: Union[rasterio.io.DatasetReader, n from skimage import measure except ModuleNotFoundError: raise ModuleNotFoundError( - 'skimage package is not installed. Use pip install skimage to install the latest version') + "skimage package is not installed. Use pip install skimage to install the latest version" + ) # Checking if provided raster is either a file loaded with rasterio, an np.ndarray or a path directing to a .tif file if not isinstance(raster, (rasterio.io.DatasetReader, np.ndarray, str)): - raise TypeError("Raster must be a raster loaded with rasterio or a path directing to a .tif file") + raise TypeError( + "Raster must be a raster loaded with rasterio or a path directing to a .tif file" + ) # Checking if provided raster is of type str. If provided raster is a path (directing to a .tif file), load the file with rasterio if isinstance(raster, str): @@ -2638,26 +2801,35 @@ def extract_contour_lines_from_raster(raster: Union[rasterio.io.DatasetReader, n if isinstance(raster, np.ndarray): if extent is None: raise UnboundLocalError( - "For np.ndarray an extent must be provided to extract contour lines from an array") + "For np.ndarray an extent must be provided to extract contour lines from an array" + ) if extent is not None and not isinstance(extent, Sequence): raise TypeError("extent values must be of type float or int") if len(extent) != 4: - raise TypeError("Not enough arguments in extent to extract contour lines from an array") + raise TypeError( + "Not enough arguments in extent to extract contour lines from an array" + ) if target_crs is None: raise UnboundLocalError("For np.ndarray a target crs must be provided") - if target_crs is not None and not isinstance(target_crs, (str, pyproj.crs.crs.CRS, rasterio.crs.CRS)): - raise TypeError("target_crs must be of type string, pyproj CRS or rasterio CRS") - - save_as_tiff(raster=np.flipud(raster), - path='input_raster.tif', - extent=extent, - crs=target_crs, - overwrite_file=True) - raster = rasterio.open('input_raster.tif') + if target_crs is not None and not isinstance( + target_crs, (str, pyproj.crs.crs.CRS, rasterio.crs.CRS) + ): + raise TypeError( + "target_crs must be of type string, pyproj CRS or rasterio CRS" + ) + + save_as_tiff( + raster=np.flipud(raster), + path="input_raster.tif", + extent=extent, + crs=target_crs, + overwrite_file=True, + ) + raster = rasterio.open("input_raster.tif") # Checking if provided interval is of type int if not isinstance(interval, int): @@ -2672,15 +2844,16 @@ def extract_contour_lines_from_raster(raster: Union[rasterio.io.DatasetReader, n values = [] # Calculating minimum and maximum value from the given raster value - min_val = int(interval * round(np.amin(raster.read(1)[~np.isnan(raster.read(1))]) / interval)) - max_val = int(interval * round(np.amax(raster.read(1)[~np.isnan(raster.read(1))]) / interval)) + min_val = int( + interval * round(np.amin(raster.read(1)[~np.isnan(raster.read(1))]) / interval) + ) + max_val = int( + interval * round(np.amax(raster.read(1)[~np.isnan(raster.read(1))]) / interval) + ) # Extracting contour lines and appending to lists - for value in range(min_val, - max_val, - interval): - contour = measure.find_contours(np.fliplr(raster.read(1).T), - value) + for value in range(min_val, max_val, interval): + contour = measure.find_contours(np.fliplr(raster.read(1).T), value) contours.append(contour) values.extend([value for i in range(len(contour))]) @@ -2695,28 +2868,32 @@ def extract_contour_lines_from_raster(raster: Union[rasterio.io.DatasetReader, n x_left, y_bottom, x_right, y_top = raster.bounds # Transforming and defining the coordinates of contours based on raster extent - x_new = [x_left + (x_right - x_left) / columns * contours_new[i][:, 0] for i in range(len(contours_new))] - y_new = [y_bottom + (y_top - y_bottom) / rows * contours_new[i][:, 1] for i in range(len(contours_new))] + x_new = [ + x_left + (x_right - x_left) / columns * contours_new[i][:, 0] + for i in range(len(contours_new)) + ] + y_new = [ + y_bottom + (y_top - y_bottom) / rows * contours_new[i][:, 1] + for i in range(len(contours_new)) + ] # Converting the contours to lines (LineStrings - Shapely) - lines = [LineString(np.array([x_new[i], - y_new[i]]).T) for i in range(len(x_new))] + lines = [LineString(np.array([x_new[i], y_new[i]]).T) for i in range(len(x_new))] # Creating GeoDataFrame from lines - gdf_lines = gpd.GeoDataFrame(geometry=lines, - crs=raster.crs) + gdf_lines = gpd.GeoDataFrame(geometry=lines, crs=raster.crs) # Adding value column to GeoDataframe - gdf_lines['Z'] = values + gdf_lines["Z"] = values return gdf_lines -def read_raster_gdb(path: str, - crs: Union[str, - pyproj.crs.crs.CRS, - rasterio.crs.CRS] = None, - path_out: str = ''): +def read_raster_gdb( + path: str, + crs: Union[str, pyproj.crs.crs.CRS, rasterio.crs.CRS] = None, + path_out: str = "", +): """Read Raster from OpenFileGDB. Parameters @@ -2735,15 +2912,15 @@ def read_raster_gdb(path: str, try: from osgeo import gdal, osr except ModuleNotFoundError: - raise ModuleNotFoundError('osgeo package is not installed') + raise ModuleNotFoundError("osgeo package is not installed") # Checking that the path is of type string if not isinstance(path, str): - raise TypeError('Path to the OpenFileGDB must be provided as string') + raise TypeError("Path to the OpenFileGDB must be provided as string") # Checking that the output path is of type string if not isinstance(path_out, str): - raise TypeError('Output path must be provided as string') + raise TypeError("Output path must be provided as string") # Opening Database ds = gdal.Open(path) @@ -2765,26 +2942,30 @@ def read_raster_gdb(path: str, # Creating CRS from projection or manually if dataset.GetProjection(): proj = osr.SpatialReference(wkt=dataset.GetProjection()) - epsg = proj.GetAttrValue('AUTHORITY', 1) - crs = 'EPSG:' + epsg + epsg = proj.GetAttrValue("AUTHORITY", 1) + crs = "EPSG:" + epsg else: if not crs: raise ValueError( - 'Raster does not have a projection, please provide a valid coordinate reference system') + "Raster does not have a projection, please provide a valid coordinate reference system" + ) # Saving raster to file with rasterio.open( - path_out + ds.GetSubDatasets()[i][1].replace(' ', '') + '.tif', - 'w', - driver='GTiff', - height=raster.shape[0], - width=raster.shape[1], - count=1, - dtype=raster.dtype, - crs=crs, - transform=affine.Affine.from_gdal(*dataset.GetGeoTransform()), - nodata=raster_band.GetNoDataValue() + path_out + ds.GetSubDatasets()[i][1].replace(" ", "") + ".tif", + "w", + driver="GTiff", + height=raster.shape[0], + width=raster.shape[1], + count=1, + dtype=raster.dtype, + crs=crs, + transform=affine.Affine.from_gdal(*dataset.GetGeoTransform()), + nodata=raster_band.GetNoDataValue(), ) as dst: dst.write(raster, 1) - print(ds.GetSubDatasets()[i][1].replace(' ', '') + '.tif successfully saved to file') \ No newline at end of file + print( + ds.GetSubDatasets()[i][1].replace(" ", "") + + ".tif successfully saved to file" + ) From 64938a14448c9b9a1390f6c79390985eb08e76fa Mon Sep 17 00:00:00 2001 From: AlexanderJuestel Date: Mon, 22 Jul 2024 20:48:58 +0200 Subject: [PATCH 19/63] Format postprocessing.py --- gemgis/postprocessing.py | 1068 +++++++++++++++++++++----------------- 1 file changed, 604 insertions(+), 464 deletions(-) diff --git a/gemgis/postprocessing.py b/gemgis/postprocessing.py index e0086fbd..4031cc8e 100644 --- a/gemgis/postprocessing.py +++ b/gemgis/postprocessing.py @@ -30,9 +30,9 @@ import xml -def extract_lithologies(geo_model, - extent: list, - crs: Union[str, pyproj.crs.crs.CRS]) -> gpd.geodataframe.GeoDataFrame: +def extract_lithologies( + geo_model, extent: list, crs: Union[str, pyproj.crs.crs.CRS] +) -> gpd.geodataframe.GeoDataFrame: """Extracting the geological map as GeoDataFrame Parameters @@ -61,12 +61,13 @@ def extract_lithologies(geo_model, import matplotlib.pyplot as plt except ModuleNotFoundError: raise ModuleNotFoundError( - 'Matplotlib package is not installed. Use pip install matplotlib to install the latest version') + "Matplotlib package is not installed. Use pip install matplotlib to install the latest version" + ) ## Trying to import gempy but returning error if gempy is not installed - #try: + # try: # import gempy as gp - #except ModuleNotFoundError: + # except ModuleNotFoundError: # raise ModuleNotFoundError( # 'GemPy package is not installed. Use pip install gempy to install the latest version') @@ -92,8 +93,9 @@ def extract_lithologies(geo_model, fm = [] geo = [] for col, fm_name in zip( - contours.collections, - geo_model.surfaces.df.sort_values(by="order_surfaces", ascending=False).surface): + contours.collections, + geo_model.surfaces.df.sort_values(by="order_surfaces", ascending=False).surface, + ): # Loop through all polygons that have the same intensity level for contour_path in col.get_paths(): @@ -114,17 +116,17 @@ def extract_lithologies(geo_model, fm.append(fm_name) geo.append(poly) - lith = gpd.GeoDataFrame({"formation": fm}, - geometry=geo, - crs=crs) + lith = gpd.GeoDataFrame({"formation": fm}, geometry=geo, crs=crs) return lith -def extract_borehole(geo_model, #: gp.core.model.Project, - geo_data: gemgis.GemPyData, - loc: List[Union[int, float]], - **kwargs): +def extract_borehole( + geo_model, #: gp.core.model.Project, + geo_data: gemgis.GemPyData, + loc: List[Union[int, float]], + **kwargs, +): """Extracting a borehole at a provided location from a recalculated GemPy Model Parameters @@ -166,55 +168,62 @@ def extract_borehole(geo_model, #: gp.core.model.Project, from matplotlib.colors import ListedColormap except ModuleNotFoundError: raise ModuleNotFoundError( - 'Matplotlib package is not installed. Use pip install matplotlib to install the latest version') + "Matplotlib package is not installed. Use pip install matplotlib to install the latest version" + ) try: import gempy as gp except ModuleNotFoundError: raise ModuleNotFoundError( - 'GemPy package is not installed. Use pip install gempy to install the latest version') + "GemPy package is not installed. Use pip install gempy to install the latest version" + ) # Checking if geo_model is a GemPy geo_model if not isinstance(geo_model, gp.core.model.Project): - raise TypeError('geo_model must be a GemPy geo_model') + raise TypeError("geo_model must be a GemPy geo_model") # Checking if geo_data is a GemGIS GemPy Data Class if not isinstance(geo_data, gemgis.GemPyData): - raise TypeError('geo_data must be a GemPy Data object') + raise TypeError("geo_data must be a GemPy Data object") # Checking if loc is of type list if not isinstance(loc, list): - raise TypeError('Borehole location must be provided as a list of a x- and y- coordinate') + raise TypeError( + "Borehole location must be provided as a list of a x- and y- coordinate" + ) # Checking if elements of loc are of type int or float if not all(isinstance(n, (int, float)) for n in loc): - raise TypeError('Location values must be provided as integers or floats') + raise TypeError("Location values must be provided as integers or floats") # Selecting DataFrame columns and create deep copy of DataFrame - orientations_df = geo_model.orientations.df[['X', 'Y', 'Z', 'surface', 'dip', 'azimuth', 'polarity']].copy( - deep=True) + orientations_df = geo_model.orientations.df[ + ["X", "Y", "Z", "surface", "dip", "azimuth", "polarity"] + ].copy(deep=True) - interfaces_df = geo_model.surface_points.df[['X', 'Y', 'Z', 'surface']].copy(deep=True) + interfaces_df = geo_model.surface_points.df[["X", "Y", "Z", "surface"]].copy( + deep=True + ) # Creating formation column - orientations_df['formation'] = orientations_df['surface'] - interfaces_df['formation'] = interfaces_df['surface'] + orientations_df["formation"] = orientations_df["surface"] + interfaces_df["formation"] = interfaces_df["surface"] # Deleting surface column - del orientations_df['surface'] - del interfaces_df['surface'] + del orientations_df["surface"] + del interfaces_df["surface"] # Getting maximum depth and resolution - zmax = kwargs.get('zmax', geo_model.grid.regular_grid.extent[5]) - res = kwargs.get('res', geo_model.grid.regular_grid.resolution[2]) + zmax = kwargs.get("zmax", geo_model.grid.regular_grid.extent[5]) + res = kwargs.get("res", geo_model.grid.regular_grid.resolution[2]) # Checking if zmax is of type int or float if not isinstance(zmax, (int, float)): - raise TypeError('Maximum depth must be of type int or float') + raise TypeError("Maximum depth must be of type int or float") # Checking if res is of type int if not isinstance(res, (int, float, np.int32)): - raise TypeError('Resolution must be of type int') + raise TypeError("Resolution must be of type int") # Creating variable for maximum depth z = geo_model.grid.regular_grid.extent[5] - zmax @@ -223,118 +232,189 @@ def extract_borehole(geo_model, #: gp.core.model.Project, # sys.stdout = open(os.devnull, 'w') # Create GemPy Model - well_model = gp.create_model('Well_Model') + well_model = gp.create_model("Well_Model") # Initiate Data for GemPy Model - gp.init_data(well_model, - extent=[loc[0] - 5, loc[0] + 5, loc[1] - 5, loc[1] + 5, geo_model.grid.regular_grid.extent[4], - geo_model.grid.regular_grid.extent[5] - z], - resolution=[5, 5, res], - orientations_df=orientations_df.dropna(), - surface_points_df=interfaces_df.dropna(), - default_values=False) + gp.init_data( + well_model, + extent=[ + loc[0] - 5, + loc[0] + 5, + loc[1] - 5, + loc[1] + 5, + geo_model.grid.regular_grid.extent[4], + geo_model.grid.regular_grid.extent[5] - z, + ], + resolution=[5, 5, res], + orientations_df=orientations_df.dropna(), + surface_points_df=interfaces_df.dropna(), + default_values=False, + ) # Map Stack to surfaces - gp.map_stack_to_surfaces(well_model, - geo_data.stack, - remove_unused_series=True) + gp.map_stack_to_surfaces(well_model, geo_data.stack, remove_unused_series=True) # Add Basement surface - well_model.add_surfaces('basement') + well_model.add_surfaces("basement") # Change colors of surfaces well_model.surfaces.colors.change_colors(geo_data.surface_colors) # Set Interpolator - gp.set_interpolator(well_model, - compile_theano=True, - theano_optimizer='fast_run', dtype='float64', - update_kriging=False, - verbose=[]) + gp.set_interpolator( + well_model, + compile_theano=True, + theano_optimizer="fast_run", + dtype="float64", + update_kriging=False, + verbose=[], + ) # Set faults active - for i in geo_model.surfaces.df[geo_model.surfaces.df['isFault'] == True]['surface'].values.tolist(): + for i in geo_model.surfaces.df[geo_model.surfaces.df["isFault"] == True][ + "surface" + ].values.tolist(): well_model.set_is_fault([i]) # Compute Model sol = gp.compute_model(well_model, compute_mesh=False) # Reshape lith_block - well = sol.lith_block.reshape(well_model.grid.regular_grid.resolution[0], - well_model.grid.regular_grid.resolution[1], - well_model.grid.regular_grid.resolution[2]) + well = sol.lith_block.reshape( + well_model.grid.regular_grid.resolution[0], + well_model.grid.regular_grid.resolution[1], + well_model.grid.regular_grid.resolution[2], + ) # Select colors for plotting color_dict = well_model.surfaces.colors.colordict surface = well_model.surfaces.df.copy(deep=True) - surfaces = surface[~surface['id'].isin(np.unique(np.round(sol.lith_block)))] - for key in surfaces['surface'].values.tolist(): + surfaces = surface[~surface["id"].isin(np.unique(np.round(sol.lith_block)))] + for key in surfaces["surface"].values.tolist(): color_dict.pop(key) cols = list(color_dict.values()) # Calculate boundaries boundaries = np.where(np.round(well.T[:, 1])[:-1] != np.round(well.T[:, 1])[1:])[0][ - ::well_model.grid.regular_grid.resolution[0]] + :: well_model.grid.regular_grid.resolution[0] + ] # Create Plot plt.figure(figsize=(3, 10)) - plt.imshow(np.rot90(np.round(well.T[:, 1]), 2), - cmap=ListedColormap(cols), - extent=(0, - (well_model.grid.regular_grid.extent[5] - well_model.grid.regular_grid.extent[4]) / 8, - well_model.grid.regular_grid.extent[4], - well_model.grid.regular_grid.extent[5]), - ) + plt.imshow( + np.rot90(np.round(well.T[:, 1]), 2), + cmap=ListedColormap(cols), + extent=( + 0, + ( + well_model.grid.regular_grid.extent[5] + - well_model.grid.regular_grid.extent[4] + ) + / 8, + well_model.grid.regular_grid.extent[4], + well_model.grid.regular_grid.extent[5], + ), + ) list_values = np.unique(np.round(well.T[:, 1])[:, 0]).tolist() # Display depths of layer boundaries for i in boundaries: - plt.text((well_model.grid.regular_grid.extent[5] - well_model.grid.regular_grid.extent[4]) / 7, - i * geo_model.grid.regular_grid.dz + geo_model.grid.regular_grid.extent[ - 4] + geo_model.grid.regular_grid.dz, - '%d m' % (i * geo_model.grid.regular_grid.dz + geo_model.grid.regular_grid.extent[4]), fontsize=14) + plt.text( + ( + well_model.grid.regular_grid.extent[5] + - well_model.grid.regular_grid.extent[4] + ) + / 7, + i * geo_model.grid.regular_grid.dz + + geo_model.grid.regular_grid.extent[4] + + geo_model.grid.regular_grid.dz, + "%d m" + % ( + i * geo_model.grid.regular_grid.dz + + geo_model.grid.regular_grid.extent[4] + ), + fontsize=14, + ) del list_values[list_values.index(np.round(well.T[:, 1])[:, 0][i + 1])] # Plot last depth - plt.text((well_model.grid.regular_grid.extent[5] - well_model.grid.regular_grid.extent[4]) / 7, - geo_model.grid.regular_grid.extent[4] + geo_model.grid.regular_grid.dz, - '%d m' % (geo_model.grid.regular_grid.extent[4]), fontsize=14) + plt.text( + ( + well_model.grid.regular_grid.extent[5] + - well_model.grid.regular_grid.extent[4] + ) + / 7, + geo_model.grid.regular_grid.extent[4] + geo_model.grid.regular_grid.dz, + "%d m" % (geo_model.grid.regular_grid.extent[4]), + fontsize=14, + ) list_values = np.unique(np.round(well.T[:, 1])[:, 0]).tolist() # Display lithology IDs for i in boundaries: - plt.text((well_model.grid.regular_grid.extent[5] - well_model.grid.regular_grid.extent[4]) / 24, - i * geo_model.grid.regular_grid.dz + geo_model.grid.regular_grid.extent[ - 4] + 2 * geo_model.grid.regular_grid.dz, - 'ID: %d' % (np.round(well.T[:, 1])[:, 0][i + 1]), fontsize=14) + plt.text( + ( + well_model.grid.regular_grid.extent[5] + - well_model.grid.regular_grid.extent[4] + ) + / 24, + i * geo_model.grid.regular_grid.dz + + geo_model.grid.regular_grid.extent[4] + + 2 * geo_model.grid.regular_grid.dz, + "ID: %d" % (np.round(well.T[:, 1])[:, 0][i + 1]), + fontsize=14, + ) del list_values[list_values.index(np.round(well.T[:, 1])[:, 0][i + 1])] # Plot last ID - plt.text((well_model.grid.regular_grid.extent[5] - well_model.grid.regular_grid.extent[4]) / 24, - geo_model.grid.regular_grid.extent[4] + 1 * geo_model.grid.regular_grid.dz, 'ID: %d' % (list_values[0]), - fontsize=14) + plt.text( + ( + well_model.grid.regular_grid.extent[5] + - well_model.grid.regular_grid.extent[4] + ) + / 24, + geo_model.grid.regular_grid.extent[4] + 1 * geo_model.grid.regular_grid.dz, + "ID: %d" % (list_values[0]), + fontsize=14, + ) # Set legend handles patches = [ - mpatches.Patch(color=cols[i], label="{formation}".format( - formation=surface[surface['id'].isin(np.unique(np.round(sol.lith_block)))].surface.to_list()[i])) - for i in range(len(surface[surface['id'].isin(np.unique(np.round(sol.lith_block)))].surface.to_list()))] + mpatches.Patch( + color=cols[i], + label="{formation}".format( + formation=surface[ + surface["id"].isin(np.unique(np.round(sol.lith_block))) + ].surface.to_list()[i] + ), + ) + for i in range( + len( + surface[ + surface["id"].isin(np.unique(np.round(sol.lith_block))) + ].surface.to_list() + ) + ) + ] # Remove xticks - plt.tick_params(axis='x', labelsize=0, length=0) + plt.tick_params(axis="x", labelsize=0, length=0) # Set ylabel - plt.ylabel('Depth [m]') + plt.ylabel("Depth [m]") # Set legend plt.legend(handles=patches, bbox_to_anchor=(3, 1)) # Create depth dict - depth_dict = {int(np.round(well.T[:, 1])[:, 0][i + 1]): i * geo_model.grid.regular_grid.dz + - geo_model.grid.regular_grid.extent[4] for i in boundaries} + depth_dict = { + int(np.round(well.T[:, 1])[:, 0][i + 1]): i * geo_model.grid.regular_grid.dz + + geo_model.grid.regular_grid.extent[4] + for i in boundaries + } depth_dict[int(list_values[0])] = geo_model.grid.regular_grid.extent[4] depth_dict = dict(sorted(depth_dict.items())) @@ -342,7 +422,7 @@ def extract_borehole(geo_model, #: gp.core.model.Project, def save_model(geo_model, path): - """ Function to save the model parameters to files + """Function to save the model parameters to files Parameters ___________ @@ -363,15 +443,16 @@ def save_model(geo_model, path): import gempy as gp except ModuleNotFoundError: raise ModuleNotFoundError( - 'GemPy package is not installed. Use pip install gempy to install the latest version') + "GemPy package is not installed. Use pip install gempy to install the latest version" + ) # Checking if the geo_model is a GemPy Geo Model if not isinstance(geo_model, gp.core.model.Project): - raise TypeError('Geo Model must be a GemPy Geo Model') + raise TypeError("Geo Model must be a GemPy Geo Model") # Checking if the path is of type string if not isinstance(path, str): - raise TypeError('Path must be of type string') + raise TypeError("Path must be of type string") project_name = open(path + "01_project_name.txt", "w") project_name.write(geo_model.meta.project_name) @@ -381,8 +462,9 @@ def save_model(geo_model, path): np.save(path + "03_resolution.npy", geo_model.grid.regular_grid.resolution) -def extract_orientations_from_mesh(mesh: pv.core.pointset.PolyData, - crs: Union[str, pyproj.crs.crs.CRS]) -> gpd.geodataframe.GeoDataFrame: +def extract_orientations_from_mesh( + mesh: pv.core.pointset.PolyData, crs: Union[str, pyproj.crs.crs.CRS] +) -> gpd.geodataframe.GeoDataFrame: """Extracting orientations (dip and azimuth) from PyVista Mesh Parameters @@ -406,43 +488,51 @@ def extract_orientations_from_mesh(mesh: pv.core.pointset.PolyData, # Checking that the provided mesh is of type Polydata if not isinstance(mesh, pv.core.pointset.PolyData): - raise TypeError('Mesh must be provided as PyVista Polydata') + raise TypeError("Mesh must be provided as PyVista Polydata") # Checking that the provided mesh if of type string or a pyproj CRS object if not isinstance(crs, (str, pyproj.crs.crs.CRS)): - raise TypeError('CRS must be provided as string or pyproj CRS object') + raise TypeError("CRS must be provided as string or pyproj CRS object") # Computing the normals of the mesh mesh_normals = mesh.compute_normals() # Calculating the dips - dips = [90 - np.rad2deg(-np.arcsin(mesh_normals['Normals'][i][2])) * (-1) for i in - range(len(mesh_normals['Normals']))] + dips = [ + 90 - np.rad2deg(-np.arcsin(mesh_normals["Normals"][i][2])) * (-1) + for i in range(len(mesh_normals["Normals"])) + ] # Calculating the azimuths - azimuths = [np.rad2deg(np.arctan(mesh_normals['Normals'][i][0] / mesh_normals['Normals'][i][1])) + 180 for i in - range(len(mesh_normals['Normals']))] + azimuths = [ + np.rad2deg( + np.arctan(mesh_normals["Normals"][i][0] / mesh_normals["Normals"][i][1]) + ) + + 180 + for i in range(len(mesh_normals["Normals"])) + ] # Getting cell centers points_z = [geometry.Point(point) for point in mesh.cell_centers().points] # Creating GeoDataFrame - gdf_orientations = gpd.GeoDataFrame(geometry=points_z, - crs=crs) + gdf_orientations = gpd.GeoDataFrame(geometry=points_z, crs=crs) # Appending X, Y, Z Locations - gdf_orientations['X'] = mesh.cell_centers().points[:, 0] - gdf_orientations['Y'] = mesh.cell_centers().points[:, 1] - gdf_orientations['Z'] = mesh.cell_centers().points[:, 2] + gdf_orientations["X"] = mesh.cell_centers().points[:, 0] + gdf_orientations["Y"] = mesh.cell_centers().points[:, 1] + gdf_orientations["Z"] = mesh.cell_centers().points[:, 2] # Appending dips and azimuths - gdf_orientations['dip'] = dips - gdf_orientations['azimuth'] = azimuths + gdf_orientations["dip"] = dips + gdf_orientations["azimuth"] = azimuths return gdf_orientations -def calculate_dip_and_azimuth_from_mesh(mesh: pv.core.pointset.PolyData) -> pv.core.pointset.PolyData: +def calculate_dip_and_azimuth_from_mesh( + mesh: pv.core.pointset.PolyData, +) -> pv.core.pointset.PolyData: """Calculating dip and azimuth values for a mesh and setting them as scalars for subsequent plotting Parameters @@ -463,22 +553,26 @@ def calculate_dip_and_azimuth_from_mesh(mesh: pv.core.pointset.PolyData) -> pv.c # Checking that the provided mesh is of type Polydata if not isinstance(mesh, pv.core.pointset.PolyData): - raise TypeError('Mesh must be provided as PyVista Polydata') + raise TypeError("Mesh must be provided as PyVista Polydata") # Computing the normals of the mesh mesh.compute_normals(inplace=True) # Calculating the dips - dips = [90 - np.rad2deg(-np.arcsin(mesh['Normals'][i][2])) * (-1) for i in - range(len(mesh['Normals']))] + dips = [ + 90 - np.rad2deg(-np.arcsin(mesh["Normals"][i][2])) * (-1) + for i in range(len(mesh["Normals"])) + ] # Calculating the azimuths - azimuths = [np.rad2deg(np.arctan(mesh['Normals'][i][0] / mesh['Normals'][i][1])) + 180 for i in - range(len(mesh['Normals']))] + azimuths = [ + np.rad2deg(np.arctan(mesh["Normals"][i][0] / mesh["Normals"][i][1])) + 180 + for i in range(len(mesh["Normals"])) + ] # Assigning dips and azimuths to scalars - mesh['Dips [°]'] = dips - mesh['Azimuths [°]'] = azimuths + mesh["Dips [°]"] = dips + mesh["Azimuths [°]"] = azimuths return mesh @@ -502,9 +596,9 @@ def crop_block_to_topography(geo_model) -> pv.core.pointset.UnstructuredGrid: """ # Trying to import GemPy - #try: + # try: # import gempy as gp - #except ModuleNotFoundError: + # except ModuleNotFoundError: # raise ModuleNotFoundError( # 'GemPy package is not installed. Use pip install gempy to install the latest version') @@ -513,30 +607,43 @@ def crop_block_to_topography(geo_model) -> pv.core.pointset.UnstructuredGrid: from PVGeo.grids import ExtractTopography except ModuleNotFoundError: raise ModuleNotFoundError( - 'PVGeo package is not installed. Use pip install pvgeo to install the lastest version') + "PVGeo package is not installed. Use pip install pvgeo to install the lastest version" + ) # Creating StructuredGrid grid = pv.UniformGrid() # Setting Grid Dimensions - grid.dimensions = np.array(geo_model.solutions.lith_block.reshape(geo_model.grid.regular_grid.resolution).shape) + 1 + grid.dimensions = ( + np.array( + geo_model.solutions.lith_block.reshape( + geo_model.grid.regular_grid.resolution + ).shape + ) + + 1 + ) # Setting Grid Origin - grid.origin = (geo_model.grid.regular_grid.extent[0], - geo_model.grid.regular_grid.extent[2], - geo_model.grid.regular_grid.extent[4]) + grid.origin = ( + geo_model.grid.regular_grid.extent[0], + geo_model.grid.regular_grid.extent[2], + geo_model.grid.regular_grid.extent[4], + ) # Setting Grid Spacing - grid.spacing = ((geo_model.grid.regular_grid.extent[1] - geo_model.grid.regular_grid.extent[0]) / - geo_model.grid.regular_grid.resolution[0], - (geo_model.grid.regular_grid.extent[3] - geo_model.grid.regular_grid.extent[2]) / - geo_model.grid.regular_grid.resolution[1], - (geo_model.grid.regular_grid.extent[5] - geo_model.grid.regular_grid.extent[4]) / - geo_model.grid.regular_grid.resolution[2]) + grid.spacing = ( + (geo_model.grid.regular_grid.extent[1] - geo_model.grid.regular_grid.extent[0]) + / geo_model.grid.regular_grid.resolution[0], + (geo_model.grid.regular_grid.extent[3] - geo_model.grid.regular_grid.extent[2]) + / geo_model.grid.regular_grid.resolution[1], + (geo_model.grid.regular_grid.extent[5] - geo_model.grid.regular_grid.extent[4]) + / geo_model.grid.regular_grid.resolution[2], + ) # Setting Cell Data - grid.cell_data['values'] = geo_model.solutions.lith_block.reshape(geo_model.grid.regular_grid.resolution).flatten( - order='F') + grid.cell_data["values"] = geo_model.solutions.lith_block.reshape( + geo_model.grid.regular_grid.resolution + ).flatten(order="F") # Creating Polydata Dataset topo = pv.PolyData(geo_model._grid.topography.values) @@ -544,14 +651,12 @@ def crop_block_to_topography(geo_model) -> pv.core.pointset.UnstructuredGrid: # Interpolating topography topo.delaunay_2d(inplace=True) - extracted = ExtractTopography(tolerance=5, - remove=True).apply(grid, topo) + extracted = ExtractTopography(tolerance=5, remove=True).apply(grid, topo) return extracted -def create_attributes(keys: list, - values: list) -> list: +def create_attributes(keys: list, values: list) -> list: """Creating a list of attribute dicts @@ -576,19 +681,19 @@ def create_attributes(keys: list, # Checking that the keys are of type list if not isinstance(keys, list): - raise TypeError('keys must be provided as list') + raise TypeError("keys must be provided as list") # Checking that all elements of the keys are of type str if not all(isinstance(n, str) for n in keys): - raise TypeError('key values must be of type str') + raise TypeError("key values must be of type str") # Checking that all elements of the values are of type list if not all(isinstance(n, list) for n in values): - raise TypeError('values must be of type list') + raise TypeError("values must be of type list") # Checking that the values are provided as list if not isinstance(values, list): - raise TypeError('values must be provided as list') + raise TypeError("values must be provided as list") # Resorting the values values = [[value[i] for value in values] for i in range(len(values[0]))] @@ -599,9 +704,7 @@ def create_attributes(keys: list, return dicts -def create_subelement(parent: xml.etree.ElementTree.Element, - name: str, - attrib: dict): +def create_subelement(parent: xml.etree.ElementTree.Element, name: str, attrib: dict): """Creating Subelement Parameters @@ -624,31 +727,31 @@ def create_subelement(parent: xml.etree.ElementTree.Element, try: import xml.etree.cElementTree as ET except ModuleNotFoundError: - raise ModuleNotFoundError('xml package is not installed') + raise ModuleNotFoundError("xml package is not installed") # Checking that the parent is a XML element if not isinstance(parent, xml.etree.ElementTree.Element): - raise TypeError('The parent must a xml.etree.ElementTree.Element') + raise TypeError("The parent must a xml.etree.ElementTree.Element") # Checking that the name is of type string if not isinstance(name, str): - raise TypeError('The element name must be of type string') + raise TypeError("The element name must be of type string") # Checking that the attributes are of type dict if not isinstance(attrib, dict): - raise TypeError('The attributes must be provided as dict') + raise TypeError("The attributes must be provided as dict") # Adding the element - ET.SubElement(parent, - name, - attrib) + ET.SubElement(parent, name, attrib) -def create_symbol(parent: xml.etree.ElementTree.Element, - color: str, - symbol_text: str, - outline_width: str = '0.26', - alpha: str = '1'): +def create_symbol( + parent: xml.etree.ElementTree.Element, + color: str, + symbol_text: str, + outline_width: str = "0.26", + alpha: str = "1", +): """Creating symbol entry Parameters @@ -677,167 +780,159 @@ def create_symbol(parent: xml.etree.ElementTree.Element, try: import xml.etree.cElementTree as ET except ModuleNotFoundError: - raise ModuleNotFoundError('xml package is not installed') + raise ModuleNotFoundError("xml package is not installed") # Checking that the parent is a XML element if not isinstance(parent, xml.etree.ElementTree.Element): - raise TypeError('The parent must a xml.etree.ElementTree.Element') + raise TypeError("The parent must a xml.etree.ElementTree.Element") # Checking that the color is of type string if not isinstance(color, str): - raise TypeError('The color values must be of type string') + raise TypeError("The color values must be of type string") # Checking that the symbol_text is of type string if not isinstance(symbol_text, str): - raise TypeError('The symbol_text must be of type string') + raise TypeError("The symbol_text must be of type string") # Checking that the outline_width is of type string if not isinstance(outline_width, str): - raise TypeError('The outline_width must be of type string') + raise TypeError("The outline_width must be of type string") # Checking that the opacity value is of type string if not isinstance(alpha, str): - raise TypeError('The opacity value alpha must be of type string') + raise TypeError("The opacity value alpha must be of type string") # Creating symbol element - symbol = ET.SubElement(parent, - 'symbol', - attrib={"force_rhr": "0", - "alpha": alpha, - "is_animated": "0", - "type": "fill", - "frame_rate": "10", - "name": symbol_text, - "clip_to_extent": "1"}) - - data_defined_properties1 = ET.SubElement(symbol, - 'data_defined_properties') - - option1 = ET.SubElement(data_defined_properties1, - 'Option', - attrib={"type": 'Map'}) - - option1_1 = ET.SubElement(option1, - 'Option', - attrib={"value": '', - "type": 'QString', - "name": 'name'}) - - option1_2 = ET.SubElement(option1, - 'Option', - attrib={"name": 'properties'}) - - option1_3 = ET.SubElement(option1, - 'Option', - attrib={"value": 'collection', - "type": 'QString', - "name": 'type'}) - - layer = ET.SubElement(symbol, - 'layer', - attrib={"locked": '0', - "pass": '0', - "class": 'SimpleFill', - "enabled": '1'}) - - option2 = ET.SubElement(layer, - 'Option', - attrib={"type": 'Map'}) - - option2_1 = ET.SubElement(option2, - 'Option', - attrib={"value": '3x:0,0,0,0,0,0', - "type": 'QString', - "name": 'border_width_map_unit_scale'}) - - option2_2 = ET.SubElement(option2, - 'Option', - attrib={"value": color, - "type": 'QString', - "name": 'color'}) - - option2_3 = ET.SubElement(option2, - 'Option', - attrib={"value": 'bevel', - "type": 'QString', - "name": 'joinstyle'}) - - option2_4 = ET.SubElement(option2, - 'Option', - attrib={"value": '0,0', - "type": 'QString', - "name": 'offset'}) - - option2_5 = ET.SubElement(option2, - 'Option', - attrib={"value": '3x:0,0,0,0,0,0', - "type": 'QString', - "name": 'offset_map_unit_scale'}) - - option2_6 = ET.SubElement(option2, - 'Option', - attrib={"value": 'MM', - "type": 'QString', - "name": 'offset_unit'}) - - option2_7 = ET.SubElement(option2, - 'Option', - attrib={"value": '35,35,35,255', - "type": 'QString', - "name": 'outline_color'}) - - option2_8 = ET.SubElement(option2, - 'Option', - attrib={"value": 'solid', - "type": 'QString', - "name": 'outline_style'}) - - option2_9 = ET.SubElement(option2, - 'Option', - attrib={"value": outline_width, - "type": 'QString', - "name": 'outline_width'}) - - option2_10 = ET.SubElement(option2, - 'Option', - attrib={"value": 'MM', - "type": 'QString', - "name": 'outline_width_unit'}) - - option2_11 = ET.SubElement(option2, - 'Option', - attrib={"value": 'solid', - "type": 'QString', - "name": 'style'}) - - data_defined_properties2 = ET.SubElement(layer, - 'data_defined_properties') - - option3 = ET.SubElement(data_defined_properties2, - 'Option', - attrib={"type": 'Map'}) - - option3_1 = ET.SubElement(option3, - 'Option', attrib={"value": '', - "type": 'QString', - "name": 'name'}) - option3_2 = ET.SubElement(option3, - 'Option', - attrib={"name": 'properties'}) - - option3_3 = ET.SubElement(option3, - 'Option', - attrib={"value": 'collection', - "type": 'QString', - "name": 'type'}) - - -def save_qgis_qml_file(gdf: gpd.geodataframe.GeoDataFrame, - value: str = 'formation', - color: str = 'color', - outline_width: Union[int, float] = 0.26, - alpha: Union[int, float] = 1, - path: str = ''): + symbol = ET.SubElement( + parent, + "symbol", + attrib={ + "force_rhr": "0", + "alpha": alpha, + "is_animated": "0", + "type": "fill", + "frame_rate": "10", + "name": symbol_text, + "clip_to_extent": "1", + }, + ) + + data_defined_properties1 = ET.SubElement(symbol, "data_defined_properties") + + option1 = ET.SubElement(data_defined_properties1, "Option", attrib={"type": "Map"}) + + option1_1 = ET.SubElement( + option1, "Option", attrib={"value": "", "type": "QString", "name": "name"} + ) + + option1_2 = ET.SubElement(option1, "Option", attrib={"name": "properties"}) + + option1_3 = ET.SubElement( + option1, + "Option", + attrib={"value": "collection", "type": "QString", "name": "type"}, + ) + + layer = ET.SubElement( + symbol, + "layer", + attrib={"locked": "0", "pass": "0", "class": "SimpleFill", "enabled": "1"}, + ) + + option2 = ET.SubElement(layer, "Option", attrib={"type": "Map"}) + + option2_1 = ET.SubElement( + option2, + "Option", + attrib={ + "value": "3x:0,0,0,0,0,0", + "type": "QString", + "name": "border_width_map_unit_scale", + }, + ) + + option2_2 = ET.SubElement( + option2, "Option", attrib={"value": color, "type": "QString", "name": "color"} + ) + + option2_3 = ET.SubElement( + option2, + "Option", + attrib={"value": "bevel", "type": "QString", "name": "joinstyle"}, + ) + + option2_4 = ET.SubElement( + option2, "Option", attrib={"value": "0,0", "type": "QString", "name": "offset"} + ) + + option2_5 = ET.SubElement( + option2, + "Option", + attrib={ + "value": "3x:0,0,0,0,0,0", + "type": "QString", + "name": "offset_map_unit_scale", + }, + ) + + option2_6 = ET.SubElement( + option2, + "Option", + attrib={"value": "MM", "type": "QString", "name": "offset_unit"}, + ) + + option2_7 = ET.SubElement( + option2, + "Option", + attrib={"value": "35,35,35,255", "type": "QString", "name": "outline_color"}, + ) + + option2_8 = ET.SubElement( + option2, + "Option", + attrib={"value": "solid", "type": "QString", "name": "outline_style"}, + ) + + option2_9 = ET.SubElement( + option2, + "Option", + attrib={"value": outline_width, "type": "QString", "name": "outline_width"}, + ) + + option2_10 = ET.SubElement( + option2, + "Option", + attrib={"value": "MM", "type": "QString", "name": "outline_width_unit"}, + ) + + option2_11 = ET.SubElement( + option2, "Option", attrib={"value": "solid", "type": "QString", "name": "style"} + ) + + data_defined_properties2 = ET.SubElement(layer, "data_defined_properties") + + option3 = ET.SubElement(data_defined_properties2, "Option", attrib={"type": "Map"}) + + option3_1 = ET.SubElement( + option3, "Option", attrib={"value": "", "type": "QString", "name": "name"} + ) + option3_2 = ET.SubElement(option3, "Option", attrib={"name": "properties"}) + + option3_3 = ET.SubElement( + option3, + "Option", + attrib={"value": "collection", "type": "QString", "name": "type"}, + ) + + +def save_qgis_qml_file( + gdf: gpd.geodataframe.GeoDataFrame, + value: str = "formation", + color: str = "color", + outline_width: Union[int, float] = 0.26, + alpha: Union[int, float] = 1, + path: str = "", +): """Creating and saving a QGIS Style File/QML File based on a GeoDataFrame Parameters @@ -867,29 +962,30 @@ def save_qgis_qml_file(gdf: gpd.geodataframe.GeoDataFrame, from PIL import ImageColor except ModuleNotFoundError: raise ModuleNotFoundError( - 'Pillow package is not installed. Use "pip install Pillow" to install the latest version') + 'Pillow package is not installed. Use "pip install Pillow" to install the latest version' + ) # Trying to import xml but returning an error if xml is not installed try: import xml.etree.cElementTree as ET except ModuleNotFoundError: - raise ModuleNotFoundError('xml package is not installed') + raise ModuleNotFoundError("xml package is not installed") # Checking that the gdf is of type GeoDataFrame if not isinstance(gdf, gpd.geodataframe.GeoDataFrame): - raise TypeError('gdf must be a GeoDataFrame') + raise TypeError("gdf must be a GeoDataFrame") # Checking that the geometry column is present in the gdf - if 'geometry' not in gdf: - raise ValueError('geometry column not present in GeoDataFrame') + if "geometry" not in gdf: + raise ValueError("geometry column not present in GeoDataFrame") # Checking that all geometries are Polygons - if not all(gdf.geom_type == 'Polygon'): - raise ValueError('All geometries of the GeoDataFrame must be polygons') + if not all(gdf.geom_type == "Polygon"): + raise ValueError("All geometries of the GeoDataFrame must be polygons") # Checking that the value used for the categorization in QGIS is of type string if not isinstance(value, str): - raise TypeError('value column name must be of type string') + raise TypeError("value column name must be of type string") # Checking that the value column is present in the gdf if value not in gdf: @@ -897,26 +993,36 @@ def save_qgis_qml_file(gdf: gpd.geodataframe.GeoDataFrame, # Checking that the color column is of type string if not isinstance(color, str): - raise TypeError('color column name must be of type string') + raise TypeError("color column name must be of type string") # Checking that the value column is present in the gdf if color not in gdf: raise ValueError('"%s" not in gdf. Please provide a valid column name.' % color) # Creating RGBA column from hex colors - gdf['RGBA'] = [str(ImageColor.getcolor(color, "RGBA")).lstrip('(').rstrip(')').replace(' ', '') for color in - gdf[color]] + gdf["RGBA"] = [ + str(ImageColor.getcolor(color, "RGBA")).lstrip("(").rstrip(")").replace(" ", "") + for color in gdf[color] + ] # Defining category subelement values - render_text = ['true'] * len(gdf['formation'].unique()) - value_text = gdf['formation'].unique().tolist() - type_text = ['string'] * len(gdf['formation'].unique()) - label_text = gdf['formation'].unique().tolist() - symbol_text = [str(value) for value in np.arange(0, len(gdf['formation'].unique())).tolist()] - outline_width = [str(outline_width)] * len(gdf['formation'].unique()) - alpha = [str(alpha)] * len(gdf['formation'].unique()) - - list_values_categories = [render_text, value_text, type_text, label_text, symbol_text] + render_text = ["true"] * len(gdf["formation"].unique()) + value_text = gdf["formation"].unique().tolist() + type_text = ["string"] * len(gdf["formation"].unique()) + label_text = gdf["formation"].unique().tolist() + symbol_text = [ + str(value) for value in np.arange(0, len(gdf["formation"].unique())).tolist() + ] + outline_width = [str(outline_width)] * len(gdf["formation"].unique()) + alpha = [str(alpha)] * len(gdf["formation"].unique()) + + list_values_categories = [ + render_text, + value_text, + type_text, + label_text, + symbol_text, + ] # Defining category subelement keys list_keys_categories = ["render", "value", "type", "label", "symbol"] @@ -925,92 +1031,92 @@ def save_qgis_qml_file(gdf: gpd.geodataframe.GeoDataFrame, category_name = "category" # Creating Root Element - root = ET.Element("qgis", attrib={"version": '3.28.1-Firenze', - "styleCategories": 'Symbology'}) + root = ET.Element( + "qgis", attrib={"version": "3.28.1-Firenze", "styleCategories": "Symbology"} + ) # Inserting Comment comment = ET.Comment("DOCTYPE qgis PUBLIC 'http://mrcc.com/qgis.dtd' 'SYSTEM''") root.insert(0, comment) # Creating renderer element - renderer = ET.SubElement(root, "renderer-v2", attrib={"attr": value, - "symbollevels": '0', - "type": 'categorizedSymbol', - "forecaster": '0', - "referencescale": '-1', - "enableorderby": '0'}) + renderer = ET.SubElement( + root, + "renderer-v2", + attrib={ + "attr": value, + "symbollevels": "0", + "type": "categorizedSymbol", + "forecaster": "0", + "referencescale": "-1", + "enableorderby": "0", + }, + ) # Creating categories element - categories = ET.SubElement(renderer, 'categories') + categories = ET.SubElement(renderer, "categories") # Creating elements and attributes - list_attributes = create_attributes(list_keys_categories, - list_values_categories) - [create_subelement(categories, - category_name, - attrib) for attrib in list_attributes] + list_attributes = create_attributes(list_keys_categories, list_values_categories) + [create_subelement(categories, category_name, attrib) for attrib in list_attributes] # Creating Symbols - symbols = ET.SubElement(renderer, - 'symbols') - - [create_symbol(symbols, - color, - symbol, - outline_w, - opacity) for color, symbol, outline_w, opacity in zip(gdf['RGBA'].unique(), - symbol_text, - outline_width, alpha)] - - source_symbol = ET.SubElement(renderer, - 'source_symbol') - - create_symbol(source_symbol, - color='152,125,183,255', - symbol_text='0', - outline_width=outline_width[0], - alpha=alpha[0]) - - roation = ET.SubElement(renderer, - 'rotation', - attrib={}) - - sizescale = ET.SubElement(renderer, - 'sizescale', - attrib={}) - - blendMode = ET.SubElement(root, - "blendMode", ) + symbols = ET.SubElement(renderer, "symbols") + + [ + create_symbol(symbols, color, symbol, outline_w, opacity) + for color, symbol, outline_w, opacity in zip( + gdf["RGBA"].unique(), symbol_text, outline_width, alpha + ) + ] + + source_symbol = ET.SubElement(renderer, "source_symbol") + + create_symbol( + source_symbol, + color="152,125,183,255", + symbol_text="0", + outline_width=outline_width[0], + alpha=alpha[0], + ) + + roation = ET.SubElement(renderer, "rotation", attrib={}) + + sizescale = ET.SubElement(renderer, "sizescale", attrib={}) + + blendMode = ET.SubElement( + root, + "blendMode", + ) blendMode.text = "0" - featureblendMode = ET.SubElement(root, - "featureBlendMode") + featureblendMode = ET.SubElement(root, "featureBlendMode") featureblendMode.text = "0" - layerGeometryType = ET.SubElement(root, - "layerGeometryType") + layerGeometryType = ET.SubElement(root, "layerGeometryType") layerGeometryType.text = "2" # Creating tree tree = ET.ElementTree(root) # Insert line breaks - ET.indent(tree, ' ') + ET.indent(tree, " ") # Saving file tree.write(path, encoding="utf-8", xml_declaration=False) - print('QML file successfully saved as %s' % path) + print("QML file successfully saved as %s" % path) -def clip_fault_of_gempy_model(geo_model, - fault: str, - which: str = 'first', - buffer_first: Union[int, float] = None, - buffer_last: Union[int, float] = None, - i_size: Union[int, float] = 1000, - j_size: Union[int, float] = 1000, - invert_first: bool = True, - invert_last: bool = False) -> Union[ - pv.core.pointset.PolyData, List[pv.core.pointset.PolyData]]: +def clip_fault_of_gempy_model( + geo_model, + fault: str, + which: str = "first", + buffer_first: Union[int, float] = None, + buffer_last: Union[int, float] = None, + i_size: Union[int, float] = 1000, + j_size: Union[int, float] = 1000, + invert_first: bool = True, + invert_last: bool = False, +) -> Union[pv.core.pointset.PolyData, List[pv.core.pointset.PolyData]]: """ Clip fault of a GemPy model. @@ -1058,143 +1164,170 @@ def clip_fault_of_gempy_model(geo_model, """ # Trying to import gempy but returning error if gempy is not installed - #try: + # try: # import gempy as gp - #except ModuleNotFoundError: + # except ModuleNotFoundError: # raise ModuleNotFoundError( # 'GemPy package is not installed. Use pip install gempy to install the latest version') # Checking that the fault is provided as string if not isinstance(fault, str): - raise TypeError('Faults must be provided as one string for one fault ') + raise TypeError("Faults must be provided as one string for one fault ") # Checking that the fault is a fault of the geo_model if isinstance(fault, str): - if fault not in geo_model.surfaces.df['surface'][geo_model.surfaces.df['isFault'] == True].tolist(): - raise ValueError('Fault is not part of the GemPy geo_model') + if ( + fault + not in geo_model.surfaces.df["surface"][ + geo_model.surfaces.df["isFault"] == True + ].tolist() + ): + raise ValueError("Fault is not part of the GemPy geo_model") # Getting the fault DataFrames fault_df_interfaces = geo_model.surface_points.df[ - geo_model.surface_points.df['surface'] == fault].reset_index(drop=True) + geo_model.surface_points.df["surface"] == fault + ].reset_index(drop=True) fault_df_orientations = geo_model.orientations.df[ - geo_model.orientations.df['surface'] == fault].reset_index(drop=True) + geo_model.orientations.df["surface"] == fault + ].reset_index(drop=True) # Checking that the parameter which is of type string or list of strings if not isinstance(which, str): raise TypeError( - 'The parameter "which" must be provided as string. Options for each fault include "first", "last", or "both"') + 'The parameter "which" must be provided as string. Options for each fault include "first", "last", or "both"' + ) # Checking that the correct values are provided for the parameter which if isinstance(which, str): - if which not in ['first', 'last', 'both']: - raise ValueError('The options for the parameter "which" include "first", "last", or "both"') + if which not in ["first", "last", "both"]: + raise ValueError( + 'The options for the parameter "which" include "first", "last", or "both"' + ) # Checking that the i size is of type int or float if not isinstance(i_size, (int, float)): - raise TypeError('i_size must be provided as int or float') + raise TypeError("i_size must be provided as int or float") # Checking that the j size is of type int or float if not isinstance(j_size, (int, float)): - raise TypeError('j_size must be provided as int or float') + raise TypeError("j_size must be provided as int or float") # Extracting depth map - mesh = visualization.create_depth_maps_from_gempy(geo_model, - surfaces=fault) + mesh = visualization.create_depth_maps_from_gempy(geo_model, surfaces=fault) # Getting the first interface points - if which == 'first': + if which == "first": - fault_df_interfaces_selected = fault_df_interfaces.iloc[0:1].reset_index(drop=True) + fault_df_interfaces_selected = fault_df_interfaces.iloc[0:1].reset_index( + drop=True + ) # Creating plane from DataFrames - plane, azimuth = create_plane_from_interface_and_orientation_dfs(df_interface=fault_df_interfaces_selected, - df_orientations=fault_df_orientations, - i_size=i_size, - j_size=j_size) + plane, azimuth = create_plane_from_interface_and_orientation_dfs( + df_interface=fault_df_interfaces_selected, + df_orientations=fault_df_orientations, + i_size=i_size, + j_size=j_size, + ) # Translating Clipping Plane if buffer_first: # Checking that buffer_first is of type int or float if not isinstance(buffer_first, (int, float)): - raise TypeError('buffer_first must be provided as int or float') + raise TypeError("buffer_first must be provided as int or float") - plane = translate_clipping_plane(plane=plane, - azimuth=azimuth, - buffer=buffer_first) + plane = translate_clipping_plane( + plane=plane, azimuth=azimuth, buffer=buffer_first + ) # Clipping mesh - mesh[fault][0] = mesh[fault][0].clip_surface(plane, - invert=invert_first) + mesh[fault][0] = mesh[fault][0].clip_surface(plane, invert=invert_first) # Getting the last interface points - elif which == 'last': + elif which == "last": - fault_df_interfaces_selected = fault_df_interfaces.iloc[-1:].reset_index(drop=True) + fault_df_interfaces_selected = fault_df_interfaces.iloc[-1:].reset_index( + drop=True + ) # Creating plane from DataFrames - plane, azimuth = create_plane_from_interface_and_orientation_dfs(df_interface=fault_df_interfaces_selected, - df_orientations=fault_df_orientations, - i_size=i_size, - j_size=j_size) + plane, azimuth = create_plane_from_interface_and_orientation_dfs( + df_interface=fault_df_interfaces_selected, + df_orientations=fault_df_orientations, + i_size=i_size, + j_size=j_size, + ) # Translating Clipping Plane if buffer_last: # Checking that buffer_last is of type int or float if not isinstance(buffer_last, (int, float)): - raise TypeError('buffer_last must be provided as int or float') + raise TypeError("buffer_last must be provided as int or float") - plane = translate_clipping_plane(plane=plane, - azimuth=azimuth, - buffer=buffer_last) + plane = translate_clipping_plane( + plane=plane, azimuth=azimuth, buffer=buffer_last + ) # Clipping mesh - mesh[fault][0] = mesh[fault][0].clip_surface(plane, - invert_last) + mesh[fault][0] = mesh[fault][0].clip_surface(plane, invert_last) - if which == 'both': + if which == "both": # First point - fault_df_interfaces_selected = fault_df_interfaces.iloc[0:1].reset_index(drop=True) + fault_df_interfaces_selected = fault_df_interfaces.iloc[0:1].reset_index( + drop=True + ) # Creating plane from DataFrames - plane1, azimuth1 = create_plane_from_interface_and_orientation_dfs(df_interface=fault_df_interfaces_selected, - df_orientations=fault_df_orientations, - i_size=i_size, - j_size=j_size) + plane1, azimuth1 = create_plane_from_interface_and_orientation_dfs( + df_interface=fault_df_interfaces_selected, + df_orientations=fault_df_orientations, + i_size=i_size, + j_size=j_size, + ) # Translating Clipping Plane if buffer_first: - plane1 = translate_clipping_plane(plane=plane1, - azimuth=azimuth1, - buffer=buffer_first) + plane1 = translate_clipping_plane( + plane=plane1, azimuth=azimuth1, buffer=buffer_first + ) # Last Point - fault_df_interfaces_selected = fault_df_interfaces.iloc[-1:].reset_index(drop=True) + fault_df_interfaces_selected = fault_df_interfaces.iloc[-1:].reset_index( + drop=True + ) # Creating plane from DataFrames - plane2, azimuth2 = create_plane_from_interface_and_orientation_dfs(df_interface=fault_df_interfaces_selected, - df_orientations=fault_df_orientations, - i_size=i_size, - j_size=j_size) + plane2, azimuth2 = create_plane_from_interface_and_orientation_dfs( + df_interface=fault_df_interfaces_selected, + df_orientations=fault_df_orientations, + i_size=i_size, + j_size=j_size, + ) # Translating Clipping Plane if buffer_last: - plane2 = translate_clipping_plane(plane=plane2, - azimuth=azimuth2, - buffer=-buffer_last) + plane2 = translate_clipping_plane( + plane=plane2, azimuth=azimuth2, buffer=-buffer_last + ) # Clipping mesh - mesh[fault][0] = mesh[fault][0].clip_surface(plane1, - invert=invert_first).clip_surface(plane2, - invert=invert_last) + mesh[fault][0] = ( + mesh[fault][0] + .clip_surface(plane1, invert=invert_first) + .clip_surface(plane2, invert=invert_last) + ) return mesh -def create_plane_from_interface_and_orientation_dfs(df_interface: pd.DataFrame, - df_orientations: pd.DataFrame, - i_size: Union[int, float] = 1000, - j_size: Union[int, float] = 1000) -> pv.core.pointset.PolyData: +def create_plane_from_interface_and_orientation_dfs( + df_interface: pd.DataFrame, + df_orientations: pd.DataFrame, + i_size: Union[int, float] = 1000, + j_size: Union[int, float] = 1000, +) -> pv.core.pointset.PolyData: """ Create PyVista plane from GemPy interface and orientations DataFrames. @@ -1229,54 +1362,57 @@ def create_plane_from_interface_and_orientation_dfs(df_interface: pd.DataFrame, """ # Checking that the interface DataFrame is a DataFrame if not isinstance(df_interface, pd.DataFrame): - raise TypeError('Interface must be provided as Pandas DataFrame') + raise TypeError("Interface must be provided as Pandas DataFrame") # Checking that the orientations DataFrame is a DataFrame if not isinstance(df_orientations, pd.DataFrame): - raise TypeError('Orientations must be provided as Pandas DataFrame') + raise TypeError("Orientations must be provided as Pandas DataFrame") # Checking that the i size is of type int or float if not isinstance(i_size, (int, float)): - raise TypeError('i_size must be provided as int or float') + raise TypeError("i_size must be provided as int or float") # Checking that the j size is of type int or float if not isinstance(j_size, (int, float)): - raise TypeError('j_size must be provided as int or float') + raise TypeError("j_size must be provided as int or float") # Creating GeoDataFrame from interface - gdf_interface = gpd.GeoDataFrame(geometry=gpd.points_from_xy(x=df_interface['X'], - y=df_interface['Y']), - data=df_interface) + gdf_interface = gpd.GeoDataFrame( + geometry=gpd.points_from_xy(x=df_interface["X"], y=df_interface["Y"]), + data=df_interface, + ) # Creating GeoDataFrame from orientations - gdf_orientations = gpd.GeoDataFrame(geometry=gpd.points_from_xy(x=df_orientations['X'], - y=df_orientations['Y']), - data=df_orientations) + gdf_orientations = gpd.GeoDataFrame( + geometry=gpd.points_from_xy(x=df_orientations["X"], y=df_orientations["Y"]), + data=df_orientations, + ) # Finding nearest orientation to the respective interface to set the orientation of the plane - gdf_orientations_nearest = gpd.sjoin_nearest(gdf_interface, - gdf_orientations) + gdf_orientations_nearest = gpd.sjoin_nearest(gdf_interface, gdf_orientations) # Extracting azimuth for clipping plane - azimuth = gdf_orientations_nearest['azimuth'][0] + azimuth = gdf_orientations_nearest["azimuth"][0] # Extracting center of clipping plane - center = df_interface[['X', 'Y', 'Z']].values[0] + center = df_interface[["X", "Y", "Z"]].values[0] # Creating clipping plane, direction is created from the orientation of the fault. - plane = pv.Plane(center=center, - direction=(np.cos(np.radians(azimuth)), - np.sin(np.radians(azimuth)), - 0.0), - i_size=i_size, - j_size=j_size) + plane = pv.Plane( + center=center, + direction=(np.cos(np.radians(azimuth)), np.sin(np.radians(azimuth)), 0.0), + i_size=i_size, + j_size=j_size, + ) return plane, azimuth -def translate_clipping_plane(plane: pv.core.pointset.PolyData, - azimuth: Union[int, float, np.int64], - buffer: Union[int, float]) -> pv.core.pointset.PolyData: +def translate_clipping_plane( + plane: pv.core.pointset.PolyData, + azimuth: Union[int, float, np.int64], + buffer: Union[int, float], +) -> pv.core.pointset.PolyData: """ Translate clipping plane. @@ -1308,23 +1444,27 @@ def translate_clipping_plane(plane: pv.core.pointset.PolyData, """ # Checking that the plane is of type PyVista PolyData if not isinstance(plane, pv.core.pointset.PolyData): - raise TypeError('The clipping plane must be provided as PyVista PolyData') + raise TypeError("The clipping plane must be provided as PyVista PolyData") # Checking that the azimuth is of type int or float if not isinstance(azimuth, (int, float, np.int64)): - raise TypeError('The azimuth must be provided as int or float') + raise TypeError("The azimuth must be provided as int or float") # Checking that the buffer is of type int or float if not isinstance(buffer, (int, float, type(None))): - raise TypeError('The buffer must be provided as int or float') + raise TypeError("The buffer must be provided as int or float") # Calculating translation factor in X and Y Directio x_translation = -np.cos(np.radians(azimuth)) * buffer y_translation = -np.sin(np.radians(azimuth)) * buffer # Translating plane - plane = plane.translate((x_translation * np.cos(np.radians(azimuth)), - y_translation * np.sin(np.radians(azimuth)), - 0.0)) + plane = plane.translate( + ( + x_translation * np.cos(np.radians(azimuth)), + y_translation * np.sin(np.radians(azimuth)), + 0.0, + ) + ) return plane From 762c6fa1915a3568a678117b404a4238754c6bae Mon Sep 17 00:00:00 2001 From: AlexanderJuestel Date: Mon, 22 Jul 2024 20:49:27 +0200 Subject: [PATCH 20/63] Format misc.py --- gemgis/misc.py | 874 ++++++++++++++++++++++++++++++------------------- 1 file changed, 537 insertions(+), 337 deletions(-) diff --git a/gemgis/misc.py b/gemgis/misc.py index 16f895bb..bdb7a2cb 100644 --- a/gemgis/misc.py +++ b/gemgis/misc.py @@ -31,8 +31,8 @@ # Borehole logs can be requested at no charge from the Geological Survey from the database DABO: # https://www.gd.nrw.de/gd_archive_dabo.htm -def load_pdf(path: str, - save_as_txt: bool = True) -> str: + +def load_pdf(path: str, save_as_txt: bool = True) -> str: """ Load PDF file containing borehole data. @@ -73,17 +73,21 @@ def load_pdf(path: str, try: import pypdf except ModuleNotFoundError: - raise ModuleNotFoundError('PyPDF package is not installed. Use pip install pypdf to install the latest version') + raise ModuleNotFoundError( + "PyPDF package is not installed. Use pip install pypdf to install the latest version" + ) # Trying to import tqdm but returning error if tqdm is not installed try: from tqdm import tqdm except ModuleNotFoundError: - raise ModuleNotFoundError('tqdm package is not installed. Use pip install tqdm to install the latest version') + raise ModuleNotFoundError( + "tqdm package is not installed. Use pip install tqdm to install the latest version" + ) # Checking that the file path is of type string if not isinstance(path, str): - raise TypeError('Path/Name must be of type string') + raise TypeError("Path/Name must be of type string") # Getting the absolute path path = os.path.abspath(path=path) @@ -94,14 +98,14 @@ def load_pdf(path: str, # Checking that the file exists if not os.path.exists(path): - raise FileNotFoundError('File not found') + raise FileNotFoundError("File not found") # Checking that save_as_bool is of type bool if not isinstance(save_as_txt, bool): - raise TypeError('Save_as_txt variable must be of type bool') + raise TypeError("Save_as_txt variable must be of type bool") # Open the file as binary object - data = open(path, 'rb') + data = open(path, "rb") # Create new PdfFileReader object filereader = pypdf.PdfReader(data) @@ -110,7 +114,7 @@ def load_pdf(path: str, number_of_pages = len(filereader.pages) # Create empty string to store page content - page_content = '' + page_content = "" # Retrieve page content for each page for i in tqdm(range(number_of_pages)): @@ -121,14 +125,14 @@ def load_pdf(path: str, # Saving a txt-file of the retrieved page content for further usage if save_as_txt: # Split path to get original file name - name = path.split('.pdf')[0] + name = path.split(".pdf")[0] # Open new text file - with open(name + '.txt', "w") as text_file: + with open(name + ".txt", "w") as text_file: text_file.write(page_content) # Print out message if saving was successful - print('%s.txt successfully saved' % name) + print("%s.txt successfully saved" % name) return page_content @@ -181,7 +185,7 @@ def load_symbols(path: str) -> list: # Checking that the path is of type string if not isinstance(path, str): - raise TypeError('Path must be of type string') + raise TypeError("Path must be of type string") # Getting the absolute path path = os.path.abspath(path=path) @@ -192,11 +196,11 @@ def load_symbols(path: str) -> list: # Checking that the file exists if not os.path.exists(path): - raise FileNotFoundError('File not found') + raise FileNotFoundError("File not found") # Opening file with open(path, "r") as text_file: - symbols = [(i, '') for i in text_file.read().splitlines()] + symbols = [(i, "") for i in text_file.read().splitlines()] return symbols @@ -237,7 +241,7 @@ def load_formations(path: str) -> list: # Checking that the path is of type string if not isinstance(path, str): - raise TypeError('Path must be of type string') + raise TypeError("Path must be of type string") # Getting the absolute path path = os.path.abspath(path=path) @@ -248,13 +252,15 @@ def load_formations(path: str) -> list: # Checking that the file exists if not os.path.exists(path): - raise FileNotFoundError('File not found') + raise FileNotFoundError("File not found") # Opening file with open(path, "rb") as text_file: formations = text_file.read().decode("UTF-8").split() - formations = [(formations[i], formations[i + 1]) for i in range(0, len(formations) - 1, 2)] + formations = [ + (formations[i], formations[i + 1]) for i in range(0, len(formations) - 1, 2) + ] return formations @@ -319,155 +325,181 @@ def get_meta_data(page: List[str]) -> list: # Checking that the data is of type list if not isinstance(page, list): - raise TypeError('Page must be of type list') + raise TypeError("Page must be of type list") # Checking that all elements are of type str if not all(isinstance(n, str) for n in page): - raise TypeError('All elements of the list must be of type str') + raise TypeError("All elements of the list must be of type str") # Obtaining DABO Number - well_dabo = page[page.index('Bnum:') + 1:page.index('Bnum:') + 2] - well_dabo = ''.join(well_dabo) - well_dabo = well_dabo.split('Object')[0] - well_dabo = 'DABO_' + well_dabo + well_dabo = page[page.index("Bnum:") + 1 : page.index("Bnum:") + 2] + well_dabo = "".join(well_dabo) + well_dabo = well_dabo.split("Object")[0] + well_dabo = "DABO_" + well_dabo # Obtaining Name of Well - well_name = page[page.index('Name') + 1:page.index('Bohrungs-')] - well_name = ''.join(well_name).replace(':', '') + well_name = page[page.index("Name") + 1 : page.index("Bohrungs-")] + well_name = "".join(well_name).replace(":", "") # Obtaining Number of Well - well_number = page[page.index('Aufschluß-Nr.') + 1:page.index('Aufschluß-Nr.') + 4] - well_number = ''.join(well_number).replace(':', '') - well_number = well_number.split('Archiv-Nr.')[0] + well_number = page[ + page.index("Aufschluß-Nr.") + 1 : page.index("Aufschluß-Nr.") + 4 + ] + well_number = "".join(well_number).replace(":", "") + well_number = well_number.split("Archiv-Nr.")[0] # Obtaining Depth of well - well_depth = page[page.index('Endteufe') + 3:page.index('Endteufe') + 4] - well_depth = float(''.join(well_depth).replace(':', '')) + well_depth = page[page.index("Endteufe") + 3 : page.index("Endteufe") + 4] + well_depth = float("".join(well_depth).replace(":", "")) # Obtaining Stratigraphie der Endteufe - well_strat = page[page.index('Stratigraphie') + 3:page.index('Stratigraphie') + 4] - well_strat = ''.join(well_strat).replace(':', '') + well_strat = page[page.index("Stratigraphie") + 3 : page.index("Stratigraphie") + 4] + well_strat = "".join(well_strat).replace(":", "") # Obtaining Topographic Map Sheet Number - well_tk = page[page.index('TK') + 2:page.index('TK') + 5] - well_tk = ''.join(well_tk).replace(':', '') - well_tk = ''.join(well_tk).replace('[TK', ' [TK ') + well_tk = page[page.index("TK") + 2 : page.index("TK") + 5] + well_tk = "".join(well_tk).replace(":", "") + well_tk = "".join(well_tk).replace("[TK", " [TK ") # Obtaining Commune - well_gemarkung = page[page.index('Gemarkung') + 1:page.index('Gemarkung') + 2] - well_gemarkung = ''.join(well_gemarkung).replace(':', '') + well_gemarkung = page[page.index("Gemarkung") + 1 : page.index("Gemarkung") + 2] + well_gemarkung = "".join(well_gemarkung).replace(":", "") # Obtaining GK Coordinates of wells - well_coord_x_gk = page[page.index('Rechtswert/Hochwert') + 3:page.index('Rechtswert/Hochwert') + 4] - well_coord_x_gk = ''.join(well_coord_x_gk).replace(':', '') + well_coord_x_gk = page[ + page.index("Rechtswert/Hochwert") + 3 : page.index("Rechtswert/Hochwert") + 4 + ] + well_coord_x_gk = "".join(well_coord_x_gk).replace(":", "") - well_coord_y_gk = page[page.index('Rechtswert/Hochwert') + 5:page.index('Rechtswert/Hochwert') + 6] - well_coord_y_gk = ''.join(well_coord_y_gk).replace(':', '') + well_coord_y_gk = page[ + page.index("Rechtswert/Hochwert") + 5 : page.index("Rechtswert/Hochwert") + 6 + ] + well_coord_y_gk = "".join(well_coord_y_gk).replace(":", "") # Obtaining UTM Coordinates of wells - well_coord_x = page[page.index('East/North') + 3:page.index('East/North') + 4] - well_coord_x = ''.join(well_coord_x).replace(':', '') + well_coord_x = page[page.index("East/North") + 3 : page.index("East/North") + 4] + well_coord_x = "".join(well_coord_x).replace(":", "") - well_coord_y = page[page.index('East/North') + 5:page.index('East/North') + 6] - well_coord_y = ''.join(well_coord_y).replace(':', '') + well_coord_y = page[page.index("East/North") + 5 : page.index("East/North") + 6] + well_coord_y = "".join(well_coord_y).replace(":", "") - well_coord_z = page[page.index('Ansatzpunktes') + 3:page.index('Ansatzpunktes') + 4] - well_coord_z = ''.join(well_coord_z).replace(':', '') + well_coord_z = page[ + page.index("Ansatzpunktes") + 3 : page.index("Ansatzpunktes") + 4 + ] + well_coord_z = "".join(well_coord_z).replace(":", "") # Obtaining Coordinates Precision - well_coords = page[page.index('Koordinatenbestimmung') + 1:page.index('Koordinatenbestimmung') + 7] - well_coords = ' '.join(well_coords).replace(':', '') - well_coords = well_coords.split(' Hoehenbestimmung')[0] + well_coords = page[ + page.index("Koordinatenbestimmung") + + 1 : page.index("Koordinatenbestimmung") + + 7 + ] + well_coords = " ".join(well_coords).replace(":", "") + well_coords = well_coords.split(" Hoehenbestimmung")[0] # Obtaining height precision - well_height = page[page.index('Hoehenbestimmung') + 1:page.index('Hoehenbestimmung') + 8] - well_height = ' '.join(well_height).replace(':', '') - well_height = ''.join(well_height).replace(' .', '') - well_height = well_height.split(' Hauptzweck')[0] + well_height = page[ + page.index("Hoehenbestimmung") + 1 : page.index("Hoehenbestimmung") + 8 + ] + well_height = " ".join(well_height).replace(":", "") + well_height = "".join(well_height).replace(" .", "") + well_height = well_height.split(" Hauptzweck")[0] # Obtaining Purpose - well_zweck = page[page.index('Aufschlusses') + 1:page.index('Aufschlusses') + 4] - well_zweck = ' '.join(well_zweck).replace(':', '') - well_zweck = well_zweck.split(' Aufschlussart')[0] + well_zweck = page[page.index("Aufschlusses") + 1 : page.index("Aufschlusses") + 4] + well_zweck = " ".join(well_zweck).replace(":", "") + well_zweck = well_zweck.split(" Aufschlussart")[0] # Obtaining Kind - well_aufschlussart = page[page.index('Aufschlussart') + 1:page.index('Aufschlussart') + 3] - well_aufschlussart = ' '.join(well_aufschlussart).replace(':', '') - well_aufschlussart = well_aufschlussart.split(' Aufschlussverfahren')[0] + well_aufschlussart = page[ + page.index("Aufschlussart") + 1 : page.index("Aufschlussart") + 3 + ] + well_aufschlussart = " ".join(well_aufschlussart).replace(":", "") + well_aufschlussart = well_aufschlussart.split(" Aufschlussverfahren")[0] # Obtaining Procedure - well_aufschlussverfahren = page[page.index('Aufschlussverfahren') + 1:page.index('Aufschlussverfahren') + 4] - well_aufschlussverfahren = ' '.join(well_aufschlussverfahren).replace(':', '') - well_aufschlussverfahren = well_aufschlussverfahren.split(' Vertraulichkeit')[0] + well_aufschlussverfahren = page[ + page.index("Aufschlussverfahren") + 1 : page.index("Aufschlussverfahren") + 4 + ] + well_aufschlussverfahren = " ".join(well_aufschlussverfahren).replace(":", "") + well_aufschlussverfahren = well_aufschlussverfahren.split(" Vertraulichkeit")[0] # Obtaining Confidentiality - well_vertraulichkeit = page[page.index('Vertraulichkeit') + 1:page.index('Vertraulichkeit') + 14] - well_vertraulichkeit = ' '.join(well_vertraulichkeit).replace(':', '') - well_vertraulichkeit = well_vertraulichkeit.split(' Art')[0] + well_vertraulichkeit = page[ + page.index("Vertraulichkeit") + 1 : page.index("Vertraulichkeit") + 14 + ] + well_vertraulichkeit = " ".join(well_vertraulichkeit).replace(":", "") + well_vertraulichkeit = well_vertraulichkeit.split(" Art")[0] # Obtaining Type of Record - well_aufnahme = page[page.index('Aufnahme') + 1:page.index('Aufnahme') + 10] - well_aufnahme = ' '.join(well_aufnahme).replace(':', '') - well_aufnahme = well_aufnahme.split(' . Schichtenverzeichnis')[0] + well_aufnahme = page[page.index("Aufnahme") + 1 : page.index("Aufnahme") + 10] + well_aufnahme = " ".join(well_aufnahme).replace(":", "") + well_aufnahme = well_aufnahme.split(" . Schichtenverzeichnis")[0] # Obtaining Lithlog Version - well_version = page[page.index('Version') + 1:page.index('Version') + 3] - well_version = ' '.join(well_version).replace(':', '') - well_version = well_version.split(' Qualität')[0] + well_version = page[page.index("Version") + 1 : page.index("Version") + 3] + well_version = " ".join(well_version).replace(":", "") + well_version = well_version.split(" Qualität")[0] # Obtaining Quality - well_quality = page[page.index('Qualität') + 1:page.index('Qualität') + 9] - well_quality = ' '.join(well_quality).replace(':', '') - well_quality = well_quality.split(' erster')[0] + well_quality = page[page.index("Qualität") + 1 : page.index("Qualität") + 9] + well_quality = " ".join(well_quality).replace(":", "") + well_quality = well_quality.split(" erster")[0] # Obtaining Drilling Period - well_date = page[page.index('Bohrtag') + 1:page.index('Bohrtag') + 6] - well_date = ' '.join(well_date).replace(':', '') - well_date = well_date.split(' . Grundwasserstand')[0] + well_date = page[page.index("Bohrtag") + 1 : page.index("Bohrtag") + 6] + well_date = " ".join(well_date).replace(":", "") + well_date = well_date.split(" . Grundwasserstand")[0] # Obtaining Remarks - well_remarks = page[page.index('Bemerkung') + 1:page.index('Bemerkung') + 14] - well_remarks = ' '.join(well_remarks).replace(':', '') - well_remarks = well_remarks.split(' . Originalschichtenverzeichnis')[0] + well_remarks = page[page.index("Bemerkung") + 1 : page.index("Bemerkung") + 14] + well_remarks = " ".join(well_remarks).replace(":", "") + well_remarks = well_remarks.split(" . Originalschichtenverzeichnis")[0] # Obtaining Availability of Lithlog - well_lithlog = page[page.index('Originalschichtenverzeichnis') + 1:page.index('Originalschichtenverzeichnis') + 7] - well_lithlog = ' '.join(well_lithlog).replace(':', '') - well_lithlog = well_lithlog.split(' .Schichtdaten')[0] - well_lithlog = well_lithlog.split(' .Geologischer Dienst NRW')[0] + well_lithlog = page[ + page.index("Originalschichtenverzeichnis") + + 1 : page.index("Originalschichtenverzeichnis") + + 7 + ] + well_lithlog = " ".join(well_lithlog).replace(":", "") + well_lithlog = well_lithlog.split(" .Schichtdaten")[0] + well_lithlog = well_lithlog.split(" .Geologischer Dienst NRW")[0] # Create list with data - data = [well_dabo, - well_name, - well_number, - float(well_depth), - float(well_coord_x), - float(well_coord_y), - float(well_coord_z), - float(well_coord_x_gk), - float(well_coord_y_gk), - well_strat, - well_tk, - well_gemarkung, - well_coords, - well_height, - well_zweck, - well_aufschlussart, - well_aufschlussverfahren, - well_vertraulichkeit, - well_aufnahme, - well_version, - well_quality, - well_date, - well_remarks, - well_lithlog] + data = [ + well_dabo, + well_name, + well_number, + float(well_depth), + float(well_coord_x), + float(well_coord_y), + float(well_coord_z), + float(well_coord_x_gk), + float(well_coord_y_gk), + well_strat, + well_tk, + well_gemarkung, + well_coords, + well_height, + well_zweck, + well_aufschlussart, + well_aufschlussverfahren, + well_vertraulichkeit, + well_aufnahme, + well_version, + well_quality, + well_date, + well_remarks, + well_lithlog, + ] return data -def get_meta_data_df(data: str, - name: str = 'GD', - return_gdf: bool = True) -> Union[pd.DataFrame, gpd.geodataframe.GeoDataFrame]: +def get_meta_data_df( + data: str, name: str = "GD", return_gdf: bool = True +) -> Union[pd.DataFrame, gpd.geodataframe.GeoDataFrame]: """Function to create a dataframe with coordinates and meta data of the different boreholes Parameters @@ -524,110 +556,124 @@ def get_meta_data_df(data: str, # Checking that the data is of type list if not isinstance(data, str): - raise TypeError('Data must be provided as list of strings') + raise TypeError("Data must be provided as list of strings") # Checking that the name is of type string if not isinstance(name, str): - raise TypeError('Path/Name must be of type string') + raise TypeError("Path/Name must be of type string") # Checking that the return_gdf variable is of type bool if not isinstance(return_gdf, bool): - raise TypeError('Return_gdf variable must be of type bool') + raise TypeError("Return_gdf variable must be of type bool") # Split Data data = data.split() - data = '#'.join(data) - data = data.split('-#Stammdaten') - data = [item.split('|')[0] for item in data] - data = [item.split('#') for item in data] + data = "#".join(data) + data = data.split("-#Stammdaten") + data = [item.split("|")[0] for item in data] + data = [item.split("#") for item in data] # Filter out wells without Stratigraphic Column - data = [item for item in data if 'Beschreibung' in item] + data = [item for item in data if "Beschreibung" in item] # Get Coordinates of data coordinates = [get_meta_data(page=item) for item in data] # Create dataframe from coordinates - coordinates_dataframe = pd.DataFrame(data=coordinates, columns=['DABO No.', - 'Name', - 'Number', - 'Depth', - 'X', - 'Y', - 'Z', - 'X_GK', - 'Y_GK', - 'Last Stratigraphic Unit', - 'Map Sheet', - 'Commune', - 'Coordinates Precision', - 'Height Precision', - 'Purpose', - 'Kind', - 'Procedure', - 'Confidentiality', - 'Record Type', - 'Lithlog Version', - 'Quality', - 'Drilling Period', - 'Remarks', - 'Availability Lithlog']) + coordinates_dataframe = pd.DataFrame( + data=coordinates, + columns=[ + "DABO No.", + "Name", + "Number", + "Depth", + "X", + "Y", + "Z", + "X_GK", + "Y_GK", + "Last Stratigraphic Unit", + "Map Sheet", + "Commune", + "Coordinates Precision", + "Height Precision", + "Purpose", + "Kind", + "Procedure", + "Confidentiality", + "Record Type", + "Lithlog Version", + "Quality", + "Drilling Period", + "Remarks", + "Availability Lithlog", + ], + ) # Creating an empty list for indices index = [] # Filling index list with indices for i in range(len(coordinates_dataframe)): - index = np.append(index, [name + '{0:04}'.format(i + 1)]) - index = pd.DataFrame(data=index, columns=['Index']) + index = np.append(index, [name + "{0:04}".format(i + 1)]) + index = pd.DataFrame(data=index, columns=["Index"]) # Creating DataFrame coordinates_dataframe = pd.concat([coordinates_dataframe, index], axis=1) # Selecting columns - coordinates_dataframe = coordinates_dataframe[['Index', - 'DABO No.', - 'Name', - 'Number', - 'Depth', - 'X', - 'Y', - 'Z', - 'X_GK', - 'Y_GK', - 'Last Stratigraphic Unit', - 'Map Sheet', - 'Commune', - 'Coordinates Precision', - 'Height Precision', - 'Purpose', - 'Kind', - 'Procedure', - 'Confidentiality', - 'Record Type', - 'Lithlog Version', - 'Quality', - 'Drilling Period', - 'Remarks', - 'Availability Lithlog' - ]] + coordinates_dataframe = coordinates_dataframe[ + [ + "Index", + "DABO No.", + "Name", + "Number", + "Depth", + "X", + "Y", + "Z", + "X_GK", + "Y_GK", + "Last Stratigraphic Unit", + "Map Sheet", + "Commune", + "Coordinates Precision", + "Height Precision", + "Purpose", + "Kind", + "Procedure", + "Confidentiality", + "Record Type", + "Lithlog Version", + "Quality", + "Drilling Period", + "Remarks", + "Availability Lithlog", + ] + ] # Remove duplicates containing identical X, Y and Z coordinates - coordinates_dataframe = coordinates_dataframe[~coordinates_dataframe.duplicated(subset=['X', 'Y', 'Z'])] + coordinates_dataframe = coordinates_dataframe[ + ~coordinates_dataframe.duplicated(subset=["X", "Y", "Z"]) + ] # Convert df to gdf if return_gdf: - coordinates_dataframe = gpd.GeoDataFrame(data=coordinates_dataframe, - geometry=gpd.points_from_xy(x=coordinates_dataframe.X, - y=coordinates_dataframe.Y, - crs='EPSG:4647')) + coordinates_dataframe = gpd.GeoDataFrame( + data=coordinates_dataframe, + geometry=gpd.points_from_xy( + x=coordinates_dataframe.X, y=coordinates_dataframe.Y, crs="EPSG:4647" + ), + ) return coordinates_dataframe -def get_stratigraphic_data(text: list, - symbols: List[Tuple[str, str]], - formations: List[Tuple[str, str]], ) -> list: +def get_stratigraphic_data( + text: list, + symbols: List[Tuple[str, str]], + formations: List[Tuple[str, str]], +) -> list: """Function to retrieve the stratigraphic data from borehole logs Parameters @@ -672,15 +718,15 @@ def get_stratigraphic_data(text: list, # Checking if the provided text is of type list if not isinstance(text, list): - raise TypeError('The provided data must be of type list') + raise TypeError("The provided data must be of type list") # Checking if the provided symbols are of type list if not isinstance(symbols, list): - raise TypeError('The provided symbols must be of type list') + raise TypeError("The provided symbols must be of type list") # Checking if the provided formations are of type list if not isinstance(formations, list): - raise TypeError('The provided formations must be of type list') + raise TypeError("The provided formations must be of type list") # Creating empty lists depth = [] @@ -691,52 +737,120 @@ def get_stratigraphic_data(text: list, txt = text # Join elements of list - txt = ''.join(txt) + txt = "".join(txt) # Obtaining Name of Well - well_name = text[text.index('Name') + 1:text.index('Bohrungs-')] - well_name = ''.join(well_name).replace(':', '') + well_name = text[text.index("Name") + 1 : text.index("Bohrungs-")] + well_name = "".join(well_name).replace(":", "") # Obtaining Depth of well - well_depth = text[text.index('Endteufe') + 3:text.index('Endteufe') + 4] - well_depth = float(''.join(well_depth).replace(':', '')) + well_depth = text[text.index("Endteufe") + 3 : text.index("Endteufe") + 4] + well_depth = float("".join(well_depth).replace(":", "")) # Obtaining UTM Coordinates of wells - well_coord_x = text[text.index('East/North') + 3:text.index('East/North') + 4] - well_coord_x = ''.join(well_coord_x).replace(':', '') + well_coord_x = text[text.index("East/North") + 3 : text.index("East/North") + 4] + well_coord_x = "".join(well_coord_x).replace(":", "") - well_coord_y = text[text.index('East/North') + 5:text.index('East/North') + 6] - well_coord_y = ''.join(well_coord_y).replace(':', '') + well_coord_y = text[text.index("East/North") + 5 : text.index("East/North") + 6] + well_coord_y = "".join(well_coord_y).replace(":", "") - well_coord_z = text[text.index('Ansatzpunktes') + 3:text.index('Ansatzpunktes') + 4] - well_coord_z = ''.join(well_coord_z).replace(':', '') + well_coord_z = text[ + text.index("Ansatzpunktes") + 3 : text.index("Ansatzpunktes") + 4 + ] + well_coord_z = "".join(well_coord_z).replace(":", "") # Defining Phrases - phrases = ['Fachaufsicht:GeologischerDienstNRW', 'Auftraggeber:GeologischerDienstNRW', - 'Bohrunternehmer:GeologischerDienstNRW', 'aufgestelltvon:GeologischerDienstNRW', - 'geol./stratgr.bearbeitetvon:GeologischerDienstNRW', 'NachRh.W.B.-G.', 'Vol.-', 'Mst.-Bänke', 'Cen.-', - 'Tst.-Stücke', 'mit Mst. - Stücken', 'Flaserstruktur(O.-', 'FlaserstrukturO.-', 'Kalkst.-', - 'gca.-Mächtigkeit', 'ca.-', 'Karbonsst.-Gerölle', - 'Mst.-Stücken', 'Mst.-Bank17,1-17,2m', 'Tst.-Stücke', 'Mst.-Bank', 'Mst. - Stücken', 'hum.-torfig', - 'rötl.-ocker', 'Pfl.-Reste', 'Utbk.-Flözg', 'Glauk.-', 'Toneisensteinlagenu.-', 'Ostrac.-', 'Stromat.-', - 'u.-knötchen', 'U.-Camp.', 'Kalkmergelst.-Gerölle', 'Pfl.-Laden', 'Pfl.-Häcksel', 'ca.-Angabe,', 'Z.-', - 'Hgd.-Schiefer', 'Sdst.-Fame', 'Orig.-Schi', 'Mergels.-', 'Kst.-', 'Steink.-G', 'Steink.-', 'Sst.-', - 'bzw.-anfang', 'nd.-er', 'u.-knäuel', 'u.-konk', 'u.-knoten', 'ng.-Bür', 'Ton.-', 'org.-', 'FS.-', - 'dkl.-', 'Schluff.-', 'Erw.-', 'Abl.-', 'abl.-', 'Sch.-', 'alsU.-', 'Plänerkst.-', 'Süßw.-', 'KV.-', - 'duchläss.-', 'Verwitt.-', 'durchlass.-', 'San.-', 'Unterkr.-', 'grünl.-', 'Stringocephal.-', 'Zinkbl.-', - 'Amphip.-', 'Tonst.-', 'Öffn.-', 'Trennflä.-', 'Randkalku.-dolomit', - 'keineAngaben,Bemerkung:nachOrig.-SV:"Lehm",'] + phrases = [ + "Fachaufsicht:GeologischerDienstNRW", + "Auftraggeber:GeologischerDienstNRW", + "Bohrunternehmer:GeologischerDienstNRW", + "aufgestelltvon:GeologischerDienstNRW", + "geol./stratgr.bearbeitetvon:GeologischerDienstNRW", + "NachRh.W.B.-G.", + "Vol.-", + "Mst.-Bänke", + "Cen.-", + "Tst.-Stücke", + "mit Mst. - Stücken", + "Flaserstruktur(O.-", + "FlaserstrukturO.-", + "Kalkst.-", + "gca.-Mächtigkeit", + "ca.-", + "Karbonsst.-Gerölle", + "Mst.-Stücken", + "Mst.-Bank17,1-17,2m", + "Tst.-Stücke", + "Mst.-Bank", + "Mst. - Stücken", + "hum.-torfig", + "rötl.-ocker", + "Pfl.-Reste", + "Utbk.-Flözg", + "Glauk.-", + "Toneisensteinlagenu.-", + "Ostrac.-", + "Stromat.-", + "u.-knötchen", + "U.-Camp.", + "Kalkmergelst.-Gerölle", + "Pfl.-Laden", + "Pfl.-Häcksel", + "ca.-Angabe,", + "Z.-", + "Hgd.-Schiefer", + "Sdst.-Fame", + "Orig.-Schi", + "Mergels.-", + "Kst.-", + "Steink.-G", + "Steink.-", + "Sst.-", + "bzw.-anfang", + "nd.-er", + "u.-knäuel", + "u.-konk", + "u.-knoten", + "ng.-Bür", + "Ton.-", + "org.-", + "FS.-", + "dkl.-", + "Schluff.-", + "Erw.-", + "Abl.-", + "abl.-", + "Sch.-", + "alsU.-", + "Plänerkst.-", + "Süßw.-", + "KV.-", + "duchläss.-", + "Verwitt.-", + "durchlass.-", + "San.-", + "Unterkr.-", + "grünl.-", + "Stringocephal.-", + "Zinkbl.-", + "Amphip.-", + "Tonst.-", + "Öffn.-", + "Trennflä.-", + "Randkalku.-dolomit", + 'keineAngaben,Bemerkung:nachOrig.-SV:"Lehm",', + ] # Replace phrases for i in phrases: - txt = txt.replace(i, '') + txt = txt.replace(i, "") # Replace Symbols for a, b in symbols: if a in txt: txt = txt.replace(a, b) - if 'TiefeBeschreibungStratigraphie' in txt: + if "TiefeBeschreibungStratigraphie" in txt: # Every line ends with a '.' and every new line starts with '-', # the string will be separated there, the result is that every line of stratigraphy will be one string now @@ -752,27 +866,29 @@ def get_stratigraphic_data(text: list, # else: # txt = txt.split('TiefeBeschreibungStratigraphie..-')[1] - txt = txt.split('TiefeBeschreibungStratigraphie..-')[1] + txt = txt.split("TiefeBeschreibungStratigraphie..-")[1] except IndexError: # Create data - data = [well_name, - float(well_depth), - float(well_coord_x), - float(well_coord_y), - float(well_coord_z), - depth, - strings, - subs, - form] + data = [ + well_name, + float(well_depth), + float(well_coord_x), + float(well_coord_y), + float(well_coord_z), + depth, + strings, + subs, + form, + ] return data # Join txt - txt = ''.join(txt) + txt = "".join(txt) # Split text at .- - txt = txt.split('.-') + txt = txt.split(".-") # For loop over every string that contains layer information for a in range(len(txt)): @@ -781,35 +897,35 @@ def get_stratigraphic_data(text: list, break else: # Every string is combined to a sequence of characters - string = ''.join(txt[a]) - if string not in (None, ''): + string = "".join(txt[a]) + if string not in (None, ""): try: # The depth information is extracted from the string - depth.append(float(string.split('m', 1)[0])) + depth.append(float(string.split("m", 1)[0])) # The depth information is cut off from the string and # only the lithologies and stratigraphy is kept - string = string.split('m', 1)[1] + string = string.split("m", 1)[1] # Remove all numbers from string (e.g. von 10m bis 20m) - string = ''.join(f for f in string if not f.isdigit()) + string = "".join(f for f in string if not f.isdigit()) except ValueError: pass else: pass # Removing symbols from string - string = string.replace(':', '') - string = string.replace('-', '') - string = string.replace('.', '') - string = string.replace(',', '') - string = string.replace('?', '') - string = string.replace('/', '') + string = string.replace(":", "") + string = string.replace("-", "") + string = string.replace(".", "") + string = string.replace(",", "") + string = string.replace("?", "") + string = string.replace("/", "") # Replace PDF-formation with formation name forms = string for q, r in formations: if "..---.m" not in forms: - if 'keineAngaben' in forms: - formation = 'NichtEingestuft' + if "keineAngaben" in forms: + formation = "NichtEingestuft" elif q in forms: new_string = forms.split(q, 1) forma = forms.split(new_string[0], 1)[1] @@ -822,25 +938,29 @@ def get_stratigraphic_data(text: list, form.append(formation) # Create Data - data = [well_name, - float(well_depth), - float(well_coord_x), - float(well_coord_y), - float(well_coord_z), - depth, - strings, - subs, - form] + data = [ + well_name, + float(well_depth), + float(well_coord_x), + float(well_coord_y), + float(well_coord_z), + depth, + strings, + subs, + form, + ] return data -def get_stratigraphic_data_df(data: str, - name: str, - symbols: List[Tuple[str, str]], - formations: List[Tuple[str, str]], - remove_last: bool = False, - return_gdf: bool = True) -> Union[pd.DataFrame, gpd.geodataframe.GeoDataFrame]: +def get_stratigraphic_data_df( + data: str, + name: str, + symbols: List[Tuple[str, str]], + formations: List[Tuple[str, str]], + remove_last: bool = False, + return_gdf: bool = True, +) -> Union[pd.DataFrame, gpd.geodataframe.GeoDataFrame]: """Function to create a dataframe with coordinates and the stratigraphy of the different boreholes Parameters @@ -848,7 +968,7 @@ def get_stratigraphic_data_df(data: str, data : list List containing the strings of the borehole log - + name : str Name for index reference, e.g. ``name='GD'`` @@ -909,36 +1029,36 @@ def get_stratigraphic_data_df(data: str, # Checking that the data is provided as string if not isinstance(data, str): - raise TypeError('Data must be provided as string') + raise TypeError("Data must be provided as string") # Checking that the name of the index is provided as string if not isinstance(name, str): - raise TypeError('Index name must be provided as string') + raise TypeError("Index name must be provided as string") # Checking that the symbols are provided as list if not isinstance(symbols, list): - raise TypeError('Symbols must be provided as list of tuples of strings') + raise TypeError("Symbols must be provided as list of tuples of strings") # Checking that the formations are provided as list if not isinstance(formations, list): - raise TypeError('Formations must be provided as list of tuples of strings') + raise TypeError("Formations must be provided as list of tuples of strings") # Checking that the remove_last variable is of type bool if not isinstance(remove_last, bool): - raise TypeError('Remove_last variable must be of type bool') + raise TypeError("Remove_last variable must be of type bool") # Checking that the return_gdf variable is of type bool if not isinstance(return_gdf, bool): - raise TypeError('Return_gdf variable must be of type bool') + raise TypeError("Return_gdf variable must be of type bool") # Splitting the entire string into a list data = data.split() # Join all elements of list/all pages of the borehole logs and separate with # - data = '#'.join(data) + data = "#".join(data) # Split entire string at each new page into separate elements of a list - data = data.split('-#Stammdaten') + data = data.split("-#Stammdaten") # Cut off the last part of each element, this is not done for each page # Segment to filter out stratigraphic tables that have multiple versions and are on multiple pages @@ -951,137 +1071,217 @@ def get_stratigraphic_data_df(data: str, # else: # data = [item.split('|Geologischer#Dienst#NRW#')[0] for item in data] - data = [item.split('|Geologischer#Dienst#NRW#')[0] for item in data] + data = [item.split("|Geologischer#Dienst#NRW#")[0] for item in data] # Remove last part of each page if log stretches over multiple pages - data = [re.sub(r'Geologischer#Dienst#NRW#\d\d.\d\d.\d\d\d\d-#\d+#-#', '#', item) for item in data] - data = [re.sub(r'Geologischer#Dienst#NRW#\d\d.\d\d.\d\d\d\d-#\d+#-', '#', item) for item in data] + data = [ + re.sub(r"Geologischer#Dienst#NRW#\d\d.\d\d.\d\d\d\d-#\d+#-#", "#", item) + for item in data + ] + data = [ + re.sub(r"Geologischer#Dienst#NRW#\d\d.\d\d.\d\d\d\d-#\d+#-", "#", item) + for item in data + ] # Connect different parts of each element - data = [''.join(item) for item in data] + data = ["".join(item) for item in data] # Split each element at # - data = [item.split('#') for item in data] + data = [item.split("#") for item in data] # Filter out wells without Stratigraphic Column - data = [item for item in data if 'Beschreibung' in item] + data = [item for item in data if "Beschreibung" in item] # Create empty list for indices index = [] # Get stratigraphic data for each well - stratigraphy = [get_stratigraphic_data(text=item, - symbols=symbols, - formations=formations) for item in data] + stratigraphy = [ + get_stratigraphic_data(text=item, symbols=symbols, formations=formations) + for item in data + ] # Create DataFrame from list of stratigraphic data stratigraphy = pd.DataFrame(data=stratigraphy) # Create DataFrame for index for i in range(len(stratigraphy)): - index = np.append(index, [str(name + '{0:04}'.format(i + 1))]) + index = np.append(index, [str(name + "{0:04}".format(i + 1))]) index = pd.DataFrame(index) # Concatenate DataFrames stratigraphy_dataframe_new = pd.concat([stratigraphy, index], axis=1) # Label DataFrame Columns - stratigraphy_dataframe_new.columns = ['Name', 'Depth', 'X', 'Y', 'Altitude', 'Z', 'PDF-Formation', 'Subformation', - 'formation', 'Index'] + stratigraphy_dataframe_new.columns = [ + "Name", + "Depth", + "X", + "Y", + "Altitude", + "Z", + "PDF-Formation", + "Subformation", + "formation", + "Index", + ] # Select Columns stratigraphy_dataframe_new = stratigraphy_dataframe_new[ - ['Index', 'Name', 'X', 'Y', 'Z', 'Depth', 'Altitude', 'PDF-Formation', 'Subformation', 'formation']] + [ + "Index", + "Name", + "X", + "Y", + "Z", + "Depth", + "Altitude", + "PDF-Formation", + "Subformation", + "formation", + ] + ] # Adjust data - strati_depth = stratigraphy_dataframe_new[['Index', 'Z']] - lst_col1 = 'Z' - depth = pd.DataFrame({ - col: np.repeat(strati_depth['Index'].values, strati_depth[lst_col1].str.len()) - for col in strati_depth.columns.drop(lst_col1)} - ).assign(**{lst_col1: np.concatenate(strati_depth[lst_col1].values)})[strati_depth.columns] - - strati_depth = stratigraphy_dataframe_new[['Name', 'Z']] - lst_col1 = 'Z' - names = pd.DataFrame({ - col: np.repeat(strati_depth['Name'].values, strati_depth[lst_col1].str.len()) - for col in strati_depth.columns.drop(lst_col1)} - ).assign(**{lst_col1: np.concatenate(strati_depth[lst_col1].values)})[strati_depth.columns] - - strati_depth = stratigraphy_dataframe_new[['X', 'Z']] - lst_col1 = 'Z' - x_coord = pd.DataFrame({ - col: np.repeat(strati_depth['X'].values, strati_depth[lst_col1].str.len()) - for col in strati_depth.columns.drop(lst_col1)} - ).assign(**{lst_col1: np.concatenate(strati_depth[lst_col1].values)})[strati_depth.columns] - - strati_depth = stratigraphy_dataframe_new[['Y', 'Z']] - lst_col1 = 'Z' - y_coord = pd.DataFrame({ - col: np.repeat(strati_depth['Y'].values, strati_depth[lst_col1].str.len()) - for col in strati_depth.columns.drop(lst_col1)} - ).assign(**{lst_col1: np.concatenate(strati_depth[lst_col1].values)})[strati_depth.columns] - - strati_depth = stratigraphy_dataframe_new[['Altitude', 'Z']] - lst_col1 = 'Z' - altitude = pd.DataFrame({ - col: np.repeat(strati_depth['Altitude'].values, strati_depth[lst_col1].str.len()) - for col in strati_depth.columns.drop(lst_col1)} - ).assign(**{lst_col1: np.concatenate(strati_depth[lst_col1].values)})[strati_depth.columns] - - strati_depth = stratigraphy_dataframe_new[['Depth', 'Z']] - lst_col1 = 'Z' - welldepth = pd.DataFrame({ - col: np.repeat(strati_depth['Depth'].values, strati_depth[lst_col1].str.len()) - for col in strati_depth.columns.drop(lst_col1)} - ).assign(**{lst_col1: np.concatenate(strati_depth[lst_col1].values)})[strati_depth.columns] - - strati_formation = stratigraphy_dataframe_new[['Index', 'formation']] - lst_col4 = 'formation' - formation = pd.DataFrame({ - col: np.repeat(strati_formation['Index'].values, strati_formation[lst_col4].str.len()) - for col in strati_formation.columns.drop(lst_col4)} - ).assign(**{lst_col4: np.concatenate(strati_formation[lst_col4].values)})[strati_formation.columns] + strati_depth = stratigraphy_dataframe_new[["Index", "Z"]] + lst_col1 = "Z" + depth = pd.DataFrame( + { + col: np.repeat( + strati_depth["Index"].values, strati_depth[lst_col1].str.len() + ) + for col in strati_depth.columns.drop(lst_col1) + } + ).assign(**{lst_col1: np.concatenate(strati_depth[lst_col1].values)})[ + strati_depth.columns + ] + + strati_depth = stratigraphy_dataframe_new[["Name", "Z"]] + lst_col1 = "Z" + names = pd.DataFrame( + { + col: np.repeat( + strati_depth["Name"].values, strati_depth[lst_col1].str.len() + ) + for col in strati_depth.columns.drop(lst_col1) + } + ).assign(**{lst_col1: np.concatenate(strati_depth[lst_col1].values)})[ + strati_depth.columns + ] + + strati_depth = stratigraphy_dataframe_new[["X", "Z"]] + lst_col1 = "Z" + x_coord = pd.DataFrame( + { + col: np.repeat(strati_depth["X"].values, strati_depth[lst_col1].str.len()) + for col in strati_depth.columns.drop(lst_col1) + } + ).assign(**{lst_col1: np.concatenate(strati_depth[lst_col1].values)})[ + strati_depth.columns + ] + + strati_depth = stratigraphy_dataframe_new[["Y", "Z"]] + lst_col1 = "Z" + y_coord = pd.DataFrame( + { + col: np.repeat(strati_depth["Y"].values, strati_depth[lst_col1].str.len()) + for col in strati_depth.columns.drop(lst_col1) + } + ).assign(**{lst_col1: np.concatenate(strati_depth[lst_col1].values)})[ + strati_depth.columns + ] + + strati_depth = stratigraphy_dataframe_new[["Altitude", "Z"]] + lst_col1 = "Z" + altitude = pd.DataFrame( + { + col: np.repeat( + strati_depth["Altitude"].values, strati_depth[lst_col1].str.len() + ) + for col in strati_depth.columns.drop(lst_col1) + } + ).assign(**{lst_col1: np.concatenate(strati_depth[lst_col1].values)})[ + strati_depth.columns + ] + + strati_depth = stratigraphy_dataframe_new[["Depth", "Z"]] + lst_col1 = "Z" + welldepth = pd.DataFrame( + { + col: np.repeat( + strati_depth["Depth"].values, strati_depth[lst_col1].str.len() + ) + for col in strati_depth.columns.drop(lst_col1) + } + ).assign(**{lst_col1: np.concatenate(strati_depth[lst_col1].values)})[ + strati_depth.columns + ] + + strati_formation = stratigraphy_dataframe_new[["Index", "formation"]] + lst_col4 = "formation" + formation = pd.DataFrame( + { + col: np.repeat( + strati_formation["Index"].values, strati_formation[lst_col4].str.len() + ) + for col in strati_formation.columns.drop(lst_col4) + } + ).assign(**{lst_col4: np.concatenate(strati_formation[lst_col4].values)})[ + strati_formation.columns + ] # Create DataFrame - strat = pd.concat([names, x_coord, y_coord, depth, altitude, welldepth, formation], - axis=1) + strat = pd.concat( + [names, x_coord, y_coord, depth, altitude, welldepth, formation], axis=1 + ) # Name Columns of DataFrame - strat = strat[['Index', 'Name', 'X', 'Y', 'Z', 'Altitude', 'Depth', 'formation']] + strat = strat[["Index", "Name", "X", "Y", "Z", "Altitude", "Depth", "formation"]] # Delete Duplicated columns (Index) strat = strat.loc[:, ~strat.columns.duplicated()] # Rename columns of Data Frame - strat.columns = ['Index', 'Name', 'X', 'Y', 'DepthLayer', 'Altitude', 'Depth', - 'formation'] + strat.columns = [ + "Index", + "Name", + "X", + "Y", + "DepthLayer", + "Altitude", + "Depth", + "formation", + ] # Create Depth Column Usable for GemPy - strat['Z'] = strat['Altitude'] - strat['DepthLayer'] + strat["Z"] = strat["Altitude"] - strat["DepthLayer"] # Reorder Columns of DataFrame - strat = strat[['Index', 'Name', 'X', 'Y', 'Z', 'Altitude', 'Depth', 'formation']] + strat = strat[["Index", "Name", "X", "Y", "Z", "Altitude", "Depth", "formation"]] # Delete Last - strat = strat.groupby(['Index', 'formation']).last().sort_values(by=['Index', 'Z'], - ascending=[True, False]).reset_index() + strat = ( + strat.groupby(["Index", "formation"]) + .last() + .sort_values(by=["Index", "Z"], ascending=[True, False]) + .reset_index() + ) # Selecting Data - strat = strat[['Index', 'Name', 'X', 'Y', 'Z', 'Altitude', 'Depth', 'formation']] + strat = strat[["Index", "Name", "X", "Y", "Z", "Altitude", "Depth", "formation"]] # Remove unusable entries - strat = strat[strat['formation'] != 'NichtEingestuft'] + strat = strat[strat["formation"] != "NichtEingestuft"] # Removing the last interfaces of each well since it does not represent a true interfaces if remove_last: - strat = strat[strat.groupby('Index').cumcount(ascending=False) > 0] + strat = strat[strat.groupby("Index").cumcount(ascending=False) > 0] # Convert df to gdf if return_gdf: - strat = gpd.GeoDataFrame(data=strat, - geometry=gpd.points_from_xy(x=strat.X, - y=strat.Y, - crs='EPSG:4647')) + strat = gpd.GeoDataFrame( + data=strat, + geometry=gpd.points_from_xy(x=strat.X, y=strat.Y, crs="EPSG:4647"), + ) return strat From 423eb17383e9197cecf54b28bfb359a28b97177f Mon Sep 17 00:00:00 2001 From: AlexanderJuestel Date: Mon, 22 Jul 2024 20:49:59 +0200 Subject: [PATCH 21/63] Format gemgis.py --- gemgis/gemgis.py | 359 +++++++++++++++++++++++++++++------------------ 1 file changed, 221 insertions(+), 138 deletions(-) diff --git a/gemgis/gemgis.py b/gemgis/gemgis.py index c5d544a6..0f72c38b 100644 --- a/gemgis/gemgis.py +++ b/gemgis/gemgis.py @@ -20,6 +20,7 @@ """ import numpy as np + # import scooby import pandas as pd import rasterio @@ -53,7 +54,7 @@ class GemPyData(object): """ This class creates an object with attributes containing i.e. the interfaces or orientations that can directly be passed to a GemPy Model - + The following attributes are available: - model_name: string - the name of the model - crs: string - the coordinate reference system of the model @@ -64,8 +65,8 @@ class GemPyData(object): - customsections: GeoDataFrame containing the Linestrings or Endpoints of custom sections - resolution: list - List containing the x,y and z resolution of the model - dem: Union[string, array] - String containing the path to the DEM or array containing DEM values - - stack: dict - Dictionary containing the layer stack associated with the model - - surface_colors: dict - Dictionary containing the surface colors for the model + - stack: dict - Dictionary containing the layer stack associated with the model + - surface_colors: dict - Dictionary containing the surface colors for the model - is_fault: list - list of surface that are classified as faults - geolmap: Union[GeoDataFrame,np.ndarray rasterio.io.Datasetreader] - GeoDataFrame or array containing the geological map either as vector or raster data set @@ -82,31 +83,33 @@ class GemPyData(object): - contours: GeoDataFrame containing the contour lines of the model area """ - def __init__(self, - model_name=None, - crs=None, - extent=None, - resolution=None, - interfaces=None, - orientations=None, - section_dict=None, - customsections=None, - dem=None, - stack=None, - surface_colors=None, - is_fault=None, - geolmap=None, - basemap=None, - faults=None, - tectonics=None, - raw_i=None, - raw_o=None, - raw_dem=None, - wms=None, - slope=None, - hillshades=None, - aspect=None, - contours=None): + def __init__( + self, + model_name=None, + crs=None, + extent=None, + resolution=None, + interfaces=None, + orientations=None, + section_dict=None, + customsections=None, + dem=None, + stack=None, + surface_colors=None, + is_fault=None, + geolmap=None, + basemap=None, + faults=None, + tectonics=None, + raw_i=None, + raw_o=None, + raw_dem=None, + wms=None, + slope=None, + hillshades=None, + aspect=None, + contours=None, + ): # Checking if data type are correct @@ -129,9 +132,13 @@ def __init__(self, if all(isinstance(n, (int, (int, float))) for n in extent): self.extent = extent else: - raise TypeError('Coordinates for extent must be provided as integers or floats') + raise TypeError( + "Coordinates for extent must be provided as integers or floats" + ) else: - raise ValueError('Length of extent must be 6 [minx,maxx,miny,maxy,minz,maxz]') + raise ValueError( + "Length of extent must be 6 [minx,maxx,miny,maxy,minz,maxz]" + ) self.extent = extent else: raise TypeError("Extent must be of type list") @@ -143,9 +150,11 @@ def __init__(self, if all(isinstance(n, int) for n in resolution): self.resolution = resolution else: - raise TypeError('Values for resolution must be provided as integers') + raise TypeError( + "Values for resolution must be provided as integers" + ) else: - raise ValueError('Length of resolution must be 3 [x,y,z]') + raise ValueError("Length of resolution must be 3 [x,y,z]") self.resolution = resolution else: raise TypeError("Resolution must be of type list") @@ -153,8 +162,11 @@ def __init__(self, # Checking if the interfaces object is a Pandas df containing all relevant columns if isinstance(interfaces, (type(None), pd.core.frame.DataFrame)): if isinstance(interfaces, pd.core.frame.DataFrame): - assert pd.Series(['X', 'Y', 'Z', 'formation']).isin( - interfaces.columns).all(), 'Interfaces DataFrame is missing columns' + assert ( + pd.Series(["X", "Y", "Z", "formation"]) + .isin(interfaces.columns) + .all() + ), "Interfaces DataFrame is missing columns" self.interfaces = interfaces else: raise TypeError("Interfaces df must be a Pandas DataFrame") @@ -162,8 +174,13 @@ def __init__(self, # Checking if the orientations object is Pandas df containing all relevant columns if isinstance(orientations, (type(None), pd.core.frame.DataFrame)): if isinstance(orientations, pd.core.frame.DataFrame): - assert pd.Series(['X', 'Y', 'Z', 'formation', 'dip', 'azimuth', 'polarity']).isin( - orientations.columns).all(), 'Orientations DataFrame is missing columns' + assert ( + pd.Series( + ["X", "Y", "Z", "formation", "dip", "azimuth", "polarity"] + ) + .isin(orientations.columns) + .all() + ), "Orientations DataFrame is missing columns" self.orientations = orientations else: raise TypeError("Orientations df must be a Pandas DataFrame") @@ -184,18 +201,32 @@ def __init__(self, if isinstance(dem, (type(None), np.ndarray, rasterio.io.DatasetReader, str)): self.dem = dem else: - raise TypeError("Digital Elevation Model must be a np Array, a raster loaded with rasterio or a string") + raise TypeError( + "Digital Elevation Model must be a np Array, a raster loaded with rasterio or a string" + ) # Checking if the provided surface colors object is of type dict if isinstance(surface_colors, (type(None), dict)): self.surface_colors = surface_colors elif isinstance(surface_colors, str): - self.surface_colors = create_surface_color_dict('../../gemgis/data/Test1/style1.qml') + self.surface_colors = create_surface_color_dict( + "../../gemgis/data/Test1/style1.qml" + ) else: - raise TypeError("Surface Colors Dict must be of type dict or a path directing to a qml file") + raise TypeError( + "Surface Colors Dict must be of type dict or a path directing to a qml file" + ) # Checking that the provided geological map is a gdf containing polygons - if isinstance(geolmap, (type(None), gpd.geodataframe.GeoDataFrame, rasterio.io.DatasetReader, np.ndarray)): + if isinstance( + geolmap, + ( + type(None), + gpd.geodataframe.GeoDataFrame, + rasterio.io.DatasetReader, + np.ndarray, + ), + ): if isinstance(geolmap, gpd.geodataframe.GeoDataFrame): if all(geolmap.geom_type == "Polygon"): self.geolmap = geolmap @@ -215,7 +246,9 @@ def __init__(self, else: self.basemap = basemap else: - raise TypeError('Base Map must be a Raster loaded with rasterio or a NumPy Array') + raise TypeError( + "Base Map must be a Raster loaded with rasterio or a NumPy Array" + ) # Checking the the provided faults are a gdf containing LineStrings if isinstance(faults, (type(None), gpd.geodataframe.GeoDataFrame)): @@ -234,10 +267,10 @@ def __init__(self, if all(isinstance(n, str) for n in is_fault): self.is_fault = is_fault else: - raise TypeError('Fault Names must be provided as strings') + raise TypeError("Fault Names must be provided as strings") self.is_fault = is_fault else: - TypeError('List of faults must be of type list') + TypeError("List of faults must be of type list") # Checking that the provided raw input data objects are of type gdf if isinstance(raw_i, (gpd.geodataframe.GeoDataFrame, type(None))): @@ -258,7 +291,9 @@ def __init__(self, # Setting the hillshades attribute if isinstance(hillshades, np.ndarray): self.hillshades = hillshades - elif isinstance(self.raw_dem, np.ndarray) and isinstance(hillshades, type(None)): + elif isinstance(self.raw_dem, np.ndarray) and isinstance( + hillshades, type(None) + ): self.hillshades = calculate_hillshades(self.raw_dem, self.extent) else: self.hillshades = hillshades @@ -273,18 +308,18 @@ def __init__(self, # Calculate model dimensions if not isinstance(self.extent, type(None)): - self.model_width = self.extent[1]-self.extent[0] - self.model_length = self.extent[3]-self.extent[2] - self.model_depth = self.extent[5]-self.extent[4] - self.model_area = self.model_width*self.model_length - self.model_volume = self.model_area*self.model_depth + self.model_width = self.extent[1] - self.extent[0] + self.model_length = self.extent[3] - self.extent[2] + self.model_depth = self.extent[5] - self.extent[4] + self.model_area = self.model_width * self.model_length + self.model_volume = self.model_area * self.model_depth # Calculate cell dimensions if not isinstance(self.resolution, type(None)): if not isinstance(self.extent, type(None)): - self.cell_width = self.model_width/self.resolution[0] - self.cell_length = self.model_length/self.resolution[1] - self.cell_depth = self.model_depth/self.resolution[2] + self.cell_width = self.model_width / self.resolution[0] + self.cell_length = self.model_length / self.resolution[1] + self.cell_depth = self.model_depth / self.resolution[2] # Setting the wms attribute if isinstance(wms, np.ndarray): @@ -302,7 +337,7 @@ def __init__(self, else: self.customsections = customsections else: - raise TypeError('Custom sections must be provided as GeoDataFrame') + raise TypeError("Custom sections must be provided as GeoDataFrame") # Setting the contours attribute if isinstance(contours, (type(None), gpd.geodataframe.GeoDataFrame)): @@ -311,13 +346,15 @@ def __init__(self, else: self.contours = contours else: - raise TypeError('Custom sections must be provided as GeoDataFrame') + raise TypeError("Custom sections must be provided as GeoDataFrame") # Function tested - def to_section_dict(self, - gdf: gpd.geodataframe.GeoDataFrame, - section_column: str = 'section_name', - resolution=None): + def to_section_dict( + self, + gdf: gpd.geodataframe.GeoDataFrame, + section_column: str = "section_name", + resolution=None, + ): """ Converting custom sections stored in shape files to GemPy section_dicts Args: @@ -333,50 +370,70 @@ def to_section_dict(self, # Checking if gdf is of type GeoDataFrame if not isinstance(gdf, gpd.geodataframe.GeoDataFrame): - raise TypeError('gdf must be of type GeoDataFrame') + raise TypeError("gdf must be of type GeoDataFrame") # Checking if the section_column is of type string if not isinstance(section_column, str): - raise TypeError('Name for section_column must be of type string') + raise TypeError("Name for section_column must be of type string") # Checking if resolution is of type list if not isinstance(resolution, list): - raise TypeError('resolution must be of type list') + raise TypeError("resolution must be of type list") # Checking if X and Y values are in column - if np.logical_not(pd.Series(['X', 'Y']).isin(gdf.columns).all()): + if np.logical_not(pd.Series(["X", "Y"]).isin(gdf.columns).all()): gdf = extract_xy(gdf) # Checking the length of the resolution list if len(resolution) != 2: - raise ValueError('resolution list must be of length two') + raise ValueError("resolution list must be of length two") # Checking that a valid section name is provided - if not {'section_name'}.issubset(gdf.columns): + if not {"section_name"}.issubset(gdf.columns): if not {section_column}.issubset(gdf.columns): - raise ValueError('Section_column name needed to create section_dict') + raise ValueError("Section_column name needed to create section_dict") # Extracting Section names section_names = gdf[section_column].unique() # Create section dicts for Point Shape Files if all(gdf.geom_type == "Point"): - section_dict = {i: ([gdf[gdf[section_column] == i].X.iloc[0], gdf[gdf[section_column] == i].Y.iloc[0]], - [gdf[gdf[section_column] == i].X.iloc[1], gdf[gdf[section_column] == i].Y.iloc[1]], - resolution) for i in section_names} + section_dict = { + i: ( + [ + gdf[gdf[section_column] == i].X.iloc[0], + gdf[gdf[section_column] == i].Y.iloc[0], + ], + [ + gdf[gdf[section_column] == i].X.iloc[1], + gdf[gdf[section_column] == i].Y.iloc[1], + ], + resolution, + ) + for i in section_names + } # Create section dicts for Line Shape Files else: - section_dict = {i: ([gdf[gdf[section_column] == i].X.iloc[0], gdf[gdf[section_column] == i].Y.iloc[0]], - [gdf[gdf[section_column] == i].X.iloc[1], gdf[gdf[section_column] == i].Y.iloc[1]], - resolution) for i in section_names} + section_dict = { + i: ( + [ + gdf[gdf[section_column] == i].X.iloc[0], + gdf[gdf[section_column] == i].Y.iloc[0], + ], + [ + gdf[gdf[section_column] == i].X.iloc[1], + gdf[gdf[section_column] == i].Y.iloc[1], + ], + resolution, + ) + for i in section_names + } self.section_dict = section_dict # Function tested - def to_gempy_df(self, - gdf: gpd.geodataframe.GeoDataFrame, - cat: str, **kwargs): + def to_gempy_df(self, gdf: gpd.geodataframe.GeoDataFrame, cat: str, **kwargs): """ Converting a GeoDataFrame into a Pandas DataFrame ready to be read in for GemPy Args: @@ -390,91 +447,111 @@ def to_gempy_df(self, # Checking if gdf is of type GeoDataFrame if not isinstance(gdf, gpd.geodataframe.GeoDataFrame): - raise TypeError('gdf must be of type GeoDataFrame') + raise TypeError("gdf must be of type GeoDataFrame") # Checking if type is of type string if not isinstance(cat, str): - raise TypeError('Type must be of type string') + raise TypeError("Type must be of type string") - if np.logical_not(pd.Series(['X', 'Y', 'Z']).isin(gdf.columns).all()): - dem = kwargs.get('dem', None) - extent = kwargs.get('extent', None) + if np.logical_not(pd.Series(["X", "Y", "Z"]).isin(gdf.columns).all()): + dem = kwargs.get("dem", None) + extent = kwargs.get("extent", None) if not isinstance(dem, type(None)): gdf = extract_xyz(gdf, dem, extent=extent) else: - raise FileNotFoundError('DEM not provided to obtain Z values for point data') - if np.logical_not(pd.Series(['formation']).isin(gdf.columns).all()): - raise ValueError('formation names not defined') + raise FileNotFoundError( + "DEM not provided to obtain Z values for point data" + ) + if np.logical_not(pd.Series(["formation"]).isin(gdf.columns).all()): + raise ValueError("formation names not defined") # Converting dip and azimuth columns to floats - if pd.Series(['dip']).isin(gdf.columns).all(): - gdf['dip'] = gdf['dip'].astype(float) + if pd.Series(["dip"]).isin(gdf.columns).all(): + gdf["dip"] = gdf["dip"].astype(float) - if pd.Series(['azimuth']).isin(gdf.columns).all(): - gdf['azimuth'] = gdf['azimuth'].astype(float) + if pd.Series(["azimuth"]).isin(gdf.columns).all(): + gdf["azimuth"] = gdf["azimuth"].astype(float) # Converting formation column to string - if pd.Series(['formation']).isin(gdf.columns).all(): - gdf['formation'] = gdf['formation'].astype(str) + if pd.Series(["formation"]).isin(gdf.columns).all(): + gdf["formation"] = gdf["formation"].astype(str) # Checking if dataframe is an orientation or interfaces df - if pd.Series(['dip']).isin(gdf.columns).all(): - if cat == 'orientations': - if (gdf['dip'] > 90).any(): - raise ValueError('dip values exceed 90 degrees') - if np.logical_not(pd.Series(['azimuth']).isin(gdf.columns).all()): - raise ValueError('azimuth values not defined') - if (gdf['azimuth'] > 360).any(): - raise ValueError('azimuth values exceed 360 degrees') + if pd.Series(["dip"]).isin(gdf.columns).all(): + if cat == "orientations": + if (gdf["dip"] > 90).any(): + raise ValueError("dip values exceed 90 degrees") + if np.logical_not(pd.Series(["azimuth"]).isin(gdf.columns).all()): + raise ValueError("azimuth values not defined") + if (gdf["azimuth"] > 360).any(): + raise ValueError("azimuth values exceed 360 degrees") # Create orientations dataframe - if np.logical_not(pd.Series(['polarity']).isin(gdf.columns).all()): - df = pd.DataFrame(gdf[['X', 'Y', 'Z', 'formation', 'dip', 'azimuth']].reset_index()) - df['polarity'] = 1 + if np.logical_not(pd.Series(["polarity"]).isin(gdf.columns).all()): + df = pd.DataFrame( + gdf[ + ["X", "Y", "Z", "formation", "dip", "azimuth"] + ].reset_index() + ) + df["polarity"] = 1 self.orientations = df else: - self.orientations = \ - pd.DataFrame(gdf[['X', 'Y', 'Z', 'formation', 'dip', 'azimuth', 'polarity']].reset_index()) + self.orientations = pd.DataFrame( + gdf[ + ["X", "Y", "Z", "formation", "dip", "azimuth", "polarity"] + ].reset_index() + ) else: - raise ValueError('GeoDataFrame contains orientations but type is interfaces') + raise ValueError( + "GeoDataFrame contains orientations but type is interfaces" + ) else: - if cat == 'interfaces': + if cat == "interfaces": # Create interfaces dataframe - self.interfaces = pd.DataFrame(gdf[['X', 'Y', 'Z', 'formation']].reset_index()) + self.interfaces = pd.DataFrame( + gdf[["X", "Y", "Z", "formation"]].reset_index() + ) else: - raise ValueError('GeoDataFrame contains interfaces but type is orientations') + raise ValueError( + "GeoDataFrame contains interfaces but type is orientations" + ) # Function tested - def set_extent(self, minx: Union[int, float] = 0, - maxx: Union[int, float] = 0, - miny: Union[int, float] = 0, - maxy: Union[int, float] = 0, - minz: Union[int, float] = 0, - maxz: Union[int, float] = 0, - **kwargs): + def set_extent( + self, + minx: Union[int, float] = 0, + maxx: Union[int, float] = 0, + miny: Union[int, float] = 0, + maxy: Union[int, float] = 0, + minz: Union[int, float] = 0, + maxz: Union[int, float] = 0, + **kwargs, + ): + """ + Setting the extent for a model + Args: + minx - float defining the left border of the model + maxx - float defining the right border of the model + miny - float defining the upper border of the model + maxy - float defining the lower border of the model + minz - float defining the top border of the model + maxz - float defining the bottom border of the model + Kwargs: + gdf - GeoDataFrame from which bounds the extent will be set + Return: + extent - list with resolution values """ - Setting the extent for a model - Args: - minx - float defining the left border of the model - maxx - float defining the right border of the model - miny - float defining the upper border of the model - maxy - float defining the lower border of the model - minz - float defining the top border of the model - maxz - float defining the bottom border of the model - Kwargs: - gdf - GeoDataFrame from which bounds the extent will be set - Return: - extent - list with resolution values - """ - gdf = kwargs.get('gdf', None) + gdf = kwargs.get("gdf", None) if not isinstance(gdf, (type(None), gpd.geodataframe.GeoDataFrame)): - raise TypeError('gdf mus be of type GeoDataFrame') + raise TypeError("gdf mus be of type GeoDataFrame") # Checking if bounds are of type int or float - if not all(isinstance(i, (int, float)) for i in [minx, maxx, miny, maxy, minz, maxz]): - raise TypeError('bounds must be of type int or float') + if not all( + isinstance(i, (int, float)) for i in [minx, maxx, miny, maxy, minz, maxz] + ): + raise TypeError("bounds must be of type int or float") # Checking if the gdf is of type None if isinstance(gdf, type(None)): @@ -490,8 +567,14 @@ def set_extent(self, minx: Union[int, float] = 0, # Create extent from gdf of geom_type point or linestring else: bounds = gdf.bounds - extent = [round(bounds.minx.min(), 2), round(bounds.maxx.max(), 2), round(bounds.miny.min(), 2), - round(bounds.maxy.max(), 2), minz, maxz] + extent = [ + round(bounds.minx.min(), 2), + round(bounds.maxx.max(), 2), + round(bounds.miny.min(), 2), + round(bounds.maxy.max(), 2), + minz, + maxz, + ] self.extent = extent self.model_width = self.extent[1] - self.extent[0] @@ -517,15 +600,15 @@ def set_resolution(self, x: int, y: int, z: int): # Checking if x is of type int if not isinstance(x, int): - raise TypeError('X must be of type int') + raise TypeError("X must be of type int") # Checking if y is of type int if not isinstance(y, int): - raise TypeError('Y must be of type int') + raise TypeError("Y must be of type int") # Checking if y is of type int if not isinstance(z, int): - raise TypeError('Z must be of type int') + raise TypeError("Z must be of type int") self.resolution = [x, y, z] @@ -546,7 +629,7 @@ def to_surface_color_dict(self, path: str, **kwargs): # Checking that the path is of type str if not isinstance(path, str): - raise TypeError('path must be provided as string') + raise TypeError("path must be provided as string") # Parse qml columns, classes = parse_categorized_qml(path) @@ -557,14 +640,14 @@ def to_surface_color_dict(self, path: str, **kwargs): # Create surface_colors_dict surface_colors_dict = {k: v["color"] for k, v in styles.items() if k} - basement = kwargs.get('basement', None) + basement = kwargs.get("basement", None) # Checking if discarded formation is of type string if not isinstance(basement, (str, type(None))): - raise TypeError('Basement formation name must be of type string') + raise TypeError("Basement formation name must be of type string") # Pop oldest lithology from dict as it does not need a color in GemPy if isinstance(basement, str): - surface_colors_dict['basement'] = surface_colors_dict.pop(basement) + surface_colors_dict["basement"] = surface_colors_dict.pop(basement) self.surface_colors = surface_colors_dict From e1b0ab889e74855596c8cf9f973923ea6657e8f3 Mon Sep 17 00:00:00 2001 From: AlexanderJuestel Date: Mon, 22 Jul 2024 20:50:50 +0200 Subject: [PATCH 22/63] Format download_gemgis_data.py --- gemgis/download_gemgis_data.py | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/gemgis/download_gemgis_data.py b/gemgis/download_gemgis_data.py index a999445b..b83f19e2 100644 --- a/gemgis/download_gemgis_data.py +++ b/gemgis/download_gemgis_data.py @@ -23,9 +23,7 @@ import zipfile -def create_pooch(storage_url: str, - files: List[str], - target: str): +def create_pooch(storage_url: str, files: List[str], target: str): """ Create pooch class to fetch files from a website. @@ -51,19 +49,22 @@ def create_pooch(storage_url: str, import pooch except ModuleNotFoundError: raise ModuleNotFoundError( - 'Pooch package is not installed. Use pip install pooch to install the latest version') + "Pooch package is not installed. Use pip install pooch to install the latest version" + ) # Create new pooch - pc = pooch.create(base_url=storage_url, - path=target, - registry={i: None for i in files}) + pc = pooch.create( + base_url=storage_url, path=target, registry={i: None for i in files} + ) return pc -def download_tutorial_data(filename: str, - dirpath: str = '', - storage_url: str = 'https://rwth-aachen.sciebo.de/s/AfXRsZywYDbUF34/download?path=%2F'): +def download_tutorial_data( + filename: str, + dirpath: str = "", + storage_url: str = "https://rwth-aachen.sciebo.de/s/AfXRsZywYDbUF34/download?path=%2F", +): """ Download the GemGIS data for each tutorial. @@ -83,20 +84,19 @@ def download_tutorial_data(filename: str, """ try: from pooch import HTTPDownloader + download = HTTPDownloader(progressbar=False) except ModuleNotFoundError: raise ModuleNotFoundError( - 'Pooch package is not installed. Use pip install pooch to install the latest version') + "Pooch package is not installed. Use pip install pooch to install the latest version" + ) # Creating pooch object - pooch_data = create_pooch(storage_url=storage_url, - files=[filename], - target=dirpath) + pooch_data = create_pooch(storage_url=storage_url, files=[filename], target=dirpath) # Downloading data to the defined folder - pooch_data.fetch(fname=filename, - downloader=download) + pooch_data.fetch(fname=filename, downloader=download) # Opening zip file and unzip in specified directory - with zipfile.ZipFile(dirpath + filename, 'r') as zip_ref: + with zipfile.ZipFile(dirpath + filename, "r") as zip_ref: zip_ref.extractall(dirpath) From 84d01f7f3cc3ff08b4828a237f5f88a7130e4410 Mon Sep 17 00:00:00 2001 From: AlexanderJuestel Date: Mon, 22 Jul 2024 23:02:27 +0200 Subject: [PATCH 23/63] Format docstrings extract_xy_points --- gemgis/vector.py | 120 ++++++++++++++++++++++++++++++++--------------- 1 file changed, 83 insertions(+), 37 deletions(-) diff --git a/gemgis/vector.py b/gemgis/vector.py index 2e1fc6be..6dd5bf0a 100644 --- a/gemgis/vector.py +++ b/gemgis/vector.py @@ -53,45 +53,75 @@ def extract_xy_points( target_crs: Union[str, pyproj.crs.crs.CRS] = None, bbox: Optional[Sequence[float]] = None, ) -> gpd.geodataframe.GeoDataFrame: - """Extracting X and Y coordinates from a GeoDataFrame (Points) and returning a GeoDataFrame with X and Y - coordinates as additional columns + """Extract X and Y coordinates from a GeoDataFrame (Points) and returning a GeoDataFrame with X and Y + coordinates as additional columns. Parameters ---------- gdf : gpd.geodataframe.GeoDataFrame - GeoDataFrame created from vector data containing elements of geom_type Point - - reset_index : bool - Variable to reset the index of the resulting GeoDataFrame. - Options include: ``True`` or ``False``, default set to ``True`` + GeoDataFrame created from vector data containing exclusively elements of `geom_type` `'Point'`. + + +----+------+-----------+------------------------+ + | ID | id | formation | geometry | + +----+------+-----------+------------------------+ + | 0 | None | Ton | POINT (19.150 293.313) | + +----+------+-----------+------------------------+ + | 1 | None | Ton | POINT (61.934 381.459) | + +----+------+-----------+------------------------+ + | 2 | None | Ton | POINT (109.358 480.946)| + +----+------+-----------+------------------------+ + | 3 | None | Ton | POINT (157.812 615.999)| + +----+------+-----------+------------------------+ + | 4 | None | Ton | POINT (191.318 719.094)| + +----+------+-----------+------------------------+ + + reset_index : bool, default: ``True`` + Variable to reset the index of the resulting GeoDataFrame. + Options include: ``True`` or ``False``, default set to ``True``. - drop_id : bool - Variable to drop the id column. - Options include: ``True`` or ``False``, default set to ``True`` + drop_id : bool, default: ``True`` + Variable to drop the id column. + Options include: ``True`` or ``False``, default set to ``True``. - drop_index : bool - Variable to drop the index column. - Options include: ``True`` or ``False``, default set to ``True`` + drop_index : bool, default: ``True`` + Variable to drop the index column. + Options include: ``True`` or ``False``, default set to ``True``. - overwrite_xy : bool - Variable to overwrite existing X and Y values. - Options include: ``True`` or ``False``, default set to ``False`` + overwrite_xy : bool, default: ``False`` + Variable to overwrite existing X and Y values. + Options include: ``True`` or ``False``, default set to ``False``. target_crs : Union[str, pyproj.crs.crs.CRS] - Name of the CRS provided to reproject coordinates of the GeoDataFrame, e.g. ``target_crs='EPSG:4647'`` + Name of the CRS provided to reproject coordinates of the GeoDataFrame, e.g. ``target_crs='EPSG:4647'``. bbox : list - Values (minx, maxx, miny, maxy) to limit the extent of the data, e.g. ``bbox=[0, 972, 0, 1069]`` + Values (minx, maxx, miny, maxy) to limit the extent of the data, e.g. ``bbox=[0, 972, 0, 1069]``. Returns ------- gdf : gpd.geodataframe.GeoDataFrame - GeoDataFrame with appended X and Y coordinates as new columns and optional columns + GeoDataFrame with appended X and Y coordinates as new columns and optional columns. + + +----+-----------+-------------------------+-----------+-----------+ + | ID | formation | geometry | X | Y | + +====+===========+=========================+===========+===========+ + | 0 | Ton | POINT (19.150 293.313) | 19.150 | 293.313 | + +----+-----------+-------------------------+-----------+-----------+ + | 1 | Ton | POINT (61.934 381.459) | 61.934 | 381.459 | + +----+-----------+-------------------------+-----------+-----------+ + | 2 | Ton | POINT (109.358 480.946) | 109.358 | 480.946 | + +----+-----------+-------------------------+-----------+-----------+ + | 3 | Ton | POINT (157.812 615.999) | 157.812 | 615.999 | + +----+-----------+-------------------------+-----------+-----------+ + | 4 | Ton | POINT (191.318 719.094) | 191.318 | 719.094 | + +----+-----------+-------------------------+-----------+-----------+ .. versionadded:: 1.0.x + .. versionchanged:: 1.2 + Example _______ @@ -100,33 +130,49 @@ def extract_xy_points( >>> import geopandas as gpd >>> gdf = gpd.read_file(filename='file.shp') >>> gdf - id formation geometry - 0 None Ton POINT (19.150 293.313) - 1 None Ton POINT (61.934 381.459) - 2 None Ton POINT (109.358 480.946) - 3 None Ton POINT (157.812 615.999) - 4 None Ton POINT (191.318 719.094) - - >>> # Extracting X and Y Coordinates from Point Objects + + +----+------+-----------+------------------------+ + | ID | id | formation | geometry | + +----+------+-----------+------------------------+ + | 0 | None | Ton | POINT (19.150 293.313) | + +----+------+-----------+------------------------+ + | 1 | None | Ton | POINT (61.934 381.459) | + +----+------+-----------+------------------------+ + | 2 | None | Ton | POINT (109.358 480.946)| + +----+------+-----------+------------------------+ + | 3 | None | Ton | POINT (157.812 615.999)| + +----+------+-----------+------------------------+ + | 4 | None | Ton | POINT (191.318 719.094)| + +----+------+-----------+------------------------+ + + + >>> # Extracting X and Y Coordinates from Point GeoDataFrame >>> gdf_xy = gg.vector.extract_xy_points(gdf=gdf, reset_index=False) >>> gdf_xy - formation geometry X Y - 0 Ton POINT (19.150 293.313) 19.15 293.31 - 1 Ton POINT (61.934 381.459) 61.93 381.46 - 2 Ton POINT (109.358 480.946) 109.36 480.95 - 3 Ton POINT (157.812 615.999) 157.81 616.00 - 4 Ton POINT (191.318 719.094) 191.32 719.09 + + +----+-----------+-------------------------+-----------+-----------+ + | ID | formation | geometry | X | Y | + +====+===========+=========================+===========+===========+ + | 0 | Ton | POINT (19.150 293.313) | 19.150 | 293.313 | + +----+-----------+-------------------------+-----------+-----------+ + | 1 | Ton | POINT (61.934 381.459) | 61.934 | 381.459 | + +----+-----------+-------------------------+-----------+-----------+ + | 2 | Ton | POINT (109.358 480.946) | 109.358 | 480.946 | + +----+-----------+-------------------------+-----------+-----------+ + | 3 | Ton | POINT (157.812 615.999) | 157.812 | 615.999 | + +----+-----------+-------------------------+-----------+-----------+ + | 4 | Ton | POINT (191.318 719.094) | 191.318 | 719.094 | + +----+-----------+-------------------------+-----------+-----------+ See Also ________ - extract_xy_linestring : Extracting X and Y coordinates from a GeoDataFrame containing Shapely LineStrings and + extract_xy : Extract X and Y coordinates from Vector Data + extract_xy_linestring : Extract X and Y coordinates from a GeoDataFrame containing Shapely LineStrings and saving the X and Y coordinates as lists for each LineString - extract_xy_linestrings : Extracting X and Y coordinates from a GeoDataFrame containing Shapely LineStrings - extract_xy : Extracting X and Y coordinates from Vector Data + extract_xy_linestrings : Extract X and Y coordinates from a GeoDataFrame containing Shapely LineStrings """ - # Checking that gdf is of type GepDataFrame if not isinstance(gdf, gpd.geodataframe.GeoDataFrame): raise TypeError("Loaded object is not a GeoDataFrame") From 60ae6b14cc3e7904afc5f8371f8538186cf627bc Mon Sep 17 00:00:00 2001 From: AlexanderJuestel Date: Mon, 22 Jul 2024 23:02:57 +0200 Subject: [PATCH 24/63] Fix API Reference and other edits --- docs/getting_started/api.rst | 6 +++--- gemgis/gemgis.py | 3 +-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/docs/getting_started/api.rst b/docs/getting_started/api.rst index 1daf72d8..d5e60a31 100644 --- a/docs/getting_started/api.rst +++ b/docs/getting_started/api.rst @@ -42,9 +42,9 @@ or from single Shapely Polygons or multiple Shapely Polygons. :toctree: reference/vector_api gemgis.vector.extract_xy_from_polygon_intersections - gemgis.vector.intersection_polygon_polygon - gemgis.vector.intersections_polygon_polygons - gemgis.vector.intersections_polygons_polygons + gemgis.vector.intersect_polygon_polygon + gemgis.vector.intersect_polygon_polygons + gemgis.vector.intersect_polygons_polygons Calculating and extracting coordinates from cross sections diff --git a/gemgis/gemgis.py b/gemgis/gemgis.py index 0f72c38b..e2e10c62 100644 --- a/gemgis/gemgis.py +++ b/gemgis/gemgis.py @@ -528,8 +528,7 @@ def set_extent( maxz: Union[int, float] = 0, **kwargs, ): - """ - Setting the extent for a model + """Setting the extent for a model. Args: minx - float defining the left border of the model maxx - float defining the right border of the model From 8ee959a9b149ddbf67cf9ba6292539eea1b65fdc Mon Sep 17 00:00:00 2001 From: AlexanderJuestel Date: Tue, 23 Jul 2024 15:54:24 +0200 Subject: [PATCH 25/63] Fix #335 --- gemgis/vector.py | 87 ++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 85 insertions(+), 2 deletions(-) diff --git a/gemgis/vector.py b/gemgis/vector.py index 6dd5bf0a..cd0e06b7 100644 --- a/gemgis/vector.py +++ b/gemgis/vector.py @@ -7805,7 +7805,6 @@ def load_gpx_as_geometry( path : str Path to the GPX file, e.g. ``path='file.gpx'`` - layer : Union[int, str] The integer index or name of a layer in a multi-layer dataset, e.g. ``layer='tracks'``, default is ``tracks`` @@ -7884,7 +7883,91 @@ def load_gpx_as_geometry( return shape -# def load_gpx_as_gdf(): +def load_gpx_as_gdf(path: str, layer: Union[int, str] = "tracks") -> gpd.geodataframe.GeoDataFrame: + """Load GPX File as GeoPandas GeoDataFrame. + + Parameters + __________ + path : str + Path to the GPX file, e.g. ``path='file.gpx'``. + layer : Union[int, str], default: `'tracks'` + The integer index or name of a layer in a multi-layer dataset, e.g. ``layer='tracks'``, default is + ``tracks``. + + Returns + _______ + gdf : gpd.geodataframe.GeoDataFrame + GeoPandas GeoDataFrame containing the GPX Data + + +----+-----------------------------+--------+-------------------------------+ + | ID | geometry | ele | time | + +----+-----------------------------+--------+-------------------------------+ + | 0 | POINT (-71.11928 42.43888) | 44.59 | 2001-11-28 21:05:28+00:00 | + +----+-----------------------------+--------+-------------------------------+ + | 1 | POINT (-71.11969 42.43923) | 57.61 | 2001-06-02 03:26:55+00:00 | + +----+-----------------------------+--------+-------------------------------+ + | 2 | POINT (-71.11615 42.43892) | 44.83 | 2001-11-16 23:03:38+00:00 | + +----+-----------------------------+--------+-------------------------------+ + + + .. versionadded:: 1.2 + + Example + _______ + + >>> # Loading Libraries and File + >>> import gemgis as gg + >>> gpx = gg.vector.load_gpx_as_gdf(path='file.gpx', layer='tracks') + >>> gpx + +----+-----------------------------+--------+-------------------------------+ + | ID | geometry | ele | time | + +----+-----------------------------+--------+-------------------------------+ + | 0 | POINT (-71.11928 42.43888) | 44.59 | 2001-11-28 21:05:28+00:00 | + +----+-----------------------------+--------+-------------------------------+ + | 1 | POINT (-71.11969 42.43923) | 57.61 | 2001-06-02 03:26:55+00:00 | + +----+-----------------------------+--------+-------------------------------+ + | 2 | POINT (-71.11615 42.43892) | 44.83 | 2001-11-16 23:03:38+00:00 | + +----+-----------------------------+--------+-------------------------------+ + + See Also + ________ + + load_gpx : Load a GPX file as Collection + load_gpx_as_dict : Load a GPX file as dict + load_gpx_as_geometry : Load a GPX file as geometry + + """ + # Trying to import pyogrio but returning error if pyogrio is not installed + try: + import pyogrio + except ModuleNotFoundError: + raise ModuleNotFoundError( + "pyogrio package is not installed. Use pip install pyogrio to install the latest version" + ) + + # Checking that the path is of type string + if not isinstance(path, str): + raise TypeError("The path must be provided as string") + + # Checking that the layer is of type int or string + if not isinstance(layer, (int, str)): + raise TypeError("Layer must be provided as integer index or as string") + + # Getting the absolute path + path = os.path.abspath(path=path) + + if not os.path.exists(path): + raise LookupError("Invalid path provided") + + # Checking that the file has the correct file ending + if not path.endswith(".gpx"): + raise TypeError("The data must be provided as gpx file") + + # Opening GPX File + gdf = pyogrio.read_datamframe(path_or_buffer=path, + layer=layer) + + return gdf # Miscellaneous Functions From 34ddc0f2148653d66f257550213754a5984f2825 Mon Sep 17 00:00:00 2001 From: AlexanderJuestel Date: Tue, 23 Jul 2024 16:11:58 +0200 Subject: [PATCH 26/63] Format extract_xy_linstring --- gemgis/vector.py | 71 +++++++++++++++++++++++++++++++++++------------- 1 file changed, 52 insertions(+), 19 deletions(-) diff --git a/gemgis/vector.py b/gemgis/vector.py index cd0e06b7..7bd73b8f 100644 --- a/gemgis/vector.py +++ b/gemgis/vector.py @@ -274,29 +274,51 @@ def extract_xy_linestring( target_crs: Union[str, pyproj.crs.crs.CRS] = None, bbox: Optional[Sequence[float]] = None, ) -> gpd.geodataframe.GeoDataFrame: - """Extracting the coordinates of Shapely LineStrings within a GeoDataFrame - and storing the X and Y coordinates in lists per LineString + """Extract the coordinates of Shapely LineStrings within a GeoDataFrame + and storing the X and Y coordinates in lists per LineString. Parameters __________ gdf : gpd.geodataframe.GeoDataFrame - GeoDataFrame created from vector data containing elements of geom_type LineString + GeoDataFrame created from vector data containing elements of geom_type LineString. + + +----+-----------+-----------+----------------------------------------------------+ + | | id | formation | geometry | + +----+-----------+-----------+----------------------------------------------------+ + | 0 | None | Sand1 | LINESTRING (0.256 264.862, 10.593 276.734, 17.... | + +----+-----------+----------------------------------------------------------------+ + | 1 | None | Ton | LINESTRING (0.188 495.787, 8.841 504.142, 41.0... | + +----+-----------+----------------------------------------------------------------+ + | 2 | None | Ton | LINESTRING (970.677 833.053, 959.372 800.023, ... | + +----+-----------+----------------------------------------------------------------+ target_crs : Union[str, pyproj.crs.crs.CRS] - Name of the CRS provided to reproject coordinates of the GeoDataFrame, e.g. ``target_crs='EPSG:4647'`` - + Name of the CRS provided to reproject coordinates of the GeoDataFrame, e.g. ``target_crs='EPSG:4647'``. bbox : Optional[Sequence[float]] - Values (minx, maxx, miny, maxy) to limit the extent of the data, e.g. ``bbox=[0, 972, 0, 1069]`` + Values (minx, maxx, miny, maxy) to limit the extent of the data, e.g. ``bbox=[0, 972, 0, 1069]``. Returns _______ gdf : gpd.geodataframe.GeoDataFrame - GeoDataFrame containing the additional X and Y columns with lists of X and Y coordinates + GeoDataFrame containing the additional X and Y columns with lists of X and Y coordinates. + + +----+-----------+-----------+----------------------------------------------------+-----------------------------------------------------+-------------------------------------------------------------+ + | | id | formation | geometry | X | Y | + +----+-----------+-----------+----------------------------------------------------+-----------------------------------------------------+-------------------------------------------------------------+ + | 0 | None | Sand1 | LINESTRING (0.256 264.862, 10.593 276.734, 17.... | [0.256327195431048, 10.59346813871597, 17.1349...] | [264.86214748436396, 276.73370778641777, 289.0...] | + +----+-----------+-----------+-------------------------------------------+--------+-----------------------------------------------------+-------------------------------------------------------------+ + | 1 | None | Ton | LINESTRING (0.188 495.787, 8.841 504.142, 41.0... | [0.1881868620686138, 8.840672956663411, 41.092...] | [495.787213546976, 504.1418419288791, 546.4230...] | + +----+-----------+-----------+-------------------------------------------+--------+-----------------------------------------------------+-------------------------------------------------------------+ + | 2 | None | Ton | LINESTRING (970.677 833.053, 959.372 800.023, ... | [970.6766251230017, 959.3724321757514, 941.291...] | [833.052616499831, 800.0232029873156, 754.8012...] | + +----+-----------+-----------+-------------------------------------------+--------+-----------------------------------------------------+-------------------------------------------------------------+ + .. versionadded:: 1.0.x + .. versionchanged:: 1.2 + Example _______ @@ -305,28 +327,39 @@ def extract_xy_linestring( >>> import geopandas as gpd >>> gdf = gpd.read_file(filename='file.shp') >>> gdf - id formation geometry - 0 None Sand1 LINESTRING (0.256 264.862, 10.593 276.734, 17.... - 1 None Ton LINESTRING (0.188 495.787, 8.841 504.142, 41.0... - 2 None Ton LINESTRING (970.677 833.053, 959.372 800.023, ... + + +----+-----------+-----------+----------------------------------------------------+ + | | id | formation | geometry | + +----+-----------+-----------+----------------------------------------------------+ + | 0 | None | Sand1 | LINESTRING (0.256 264.862, 10.593 276.734, 17.... | + +----+-----------+----------------------------------------------------------------+ + | 1 | None | Ton | LINESTRING (0.188 495.787, 8.841 504.142, 41.0... | + +----+-----------+----------------------------------------------------------------+ + | 2 | None | Ton | LINESTRING (970.677 833.053, 959.372 800.023, ... | + +----+-----------+----------------------------------------------------------------+ >>> # Extracting X and Y Coordinates from LineString Objects >>> gdf_xy = gg.vector.extract_xy_linestring(gdf=gdf) >>> gdf_xy - id formation geometry X Y - 0 None Sand1 LINESTRING (0.256 264.862, 10.593 276.734, 17.... [0.256327195431048, 10.59346813871597, 17.1349... [264.86214748436396, 276.73370778641777, 289.0... - 1 None Ton LINESTRING (0.188 495.787, 8.841 504.142, 41.0... [0.1881868620686138, 8.840672956663411, 41.092... [495.787213546976, 504.1418419288791, 546.4230... - 2 None Ton LINESTRING (970.677 833.053, 959.372 800.023, ... [970.6766251230017, 959.3724321757514, 941.291... [833.052616499831, 800.0232029873156, 754.8012... + + +----+-----------+-----------+----------------------------------------------------+-----------------------------------------------------+-------------------------------------------------------------+ + | | id | formation | geometry | X | Y | + +----+-----------+-----------+----------------------------------------------------+-----------------------------------------------------+-------------------------------------------------------------+ + | 0 | None | Sand1 | LINESTRING (0.256 264.862, 10.593 276.734, 17.... | [0.256327195431048, 10.59346813871597, 17.1349...] | [264.86214748436396, 276.73370778641777, 289.0...] | + +----+-----------+-----------+-------------------------------------------+--------+-----------------------------------------------------+-------------------------------------------------------------+ + | 1 | None | Ton | LINESTRING (0.188 495.787, 8.841 504.142, 41.0... | [0.1881868620686138, 8.840672956663411, 41.092...] | [495.787213546976, 504.1418419288791, 546.4230...] | + +----+-----------+-----------+-------------------------------------------+--------+-----------------------------------------------------+-------------------------------------------------------------+ + | 2 | None | Ton | LINESTRING (970.677 833.053, 959.372 800.023, ... | [970.6766251230017, 959.3724321757514, 941.291...] | [833.052616499831, 800.0232029873156, 754.8012...] | + +----+-----------+-----------+-------------------------------------------+--------+-----------------------------------------------------+-------------------------------------------------------------+ See Also ________ - extract_xy_linestrings : Extracting X and Y coordinates from a GeoDataFrame containing Shapely LineStrings - extract_xy_points : Extracting X and Y coordinates from a GeoDataFrame containing Shapely Points - extract_xy : Extracting X and Y coordinates from Vector Data + extract_xy : Extract X and Y coordinates from Vector Data + extract_xy_points : Extract X and Y coordinates from a GeoDataFrame containing Shapely Points + extract_xy_linestrings : Extract X and Y coordinates from a GeoDataFrame containing Shapely LineStrings """ - # Checking that gdf is of type GepDataFrame if not isinstance(gdf, gpd.geodataframe.GeoDataFrame): raise TypeError("Loaded object is not a GeoDataFrame") From ff634489548a9db4f4a055b384d5289a4bf43970 Mon Sep 17 00:00:00 2001 From: AlexanderJuestel Date: Tue, 23 Jul 2024 20:36:21 +0200 Subject: [PATCH 27/63] Format extract_xy_linestrings --- gemgis/vector.py | 109 +++++++++++++++++++++++++++++++++-------------- 1 file changed, 76 insertions(+), 33 deletions(-) diff --git a/gemgis/vector.py b/gemgis/vector.py index 7bd73b8f..39eab56b 100644 --- a/gemgis/vector.py +++ b/gemgis/vector.py @@ -435,46 +435,73 @@ def extract_xy_linestrings( target_crs: Union[str, pyproj.crs.crs.CRS] = None, bbox: Optional[Sequence[float]] = None, ) -> gpd.geodataframe.GeoDataFrame: - """Extracting X and Y coordinates from a GeoDataFrame (LineStrings) and returning a GeoDataFrame with X and Y - coordinates as additional columns + """Extract X and Y coordinates from a GeoDataFrame (LineStrings) and returning a GeoDataFrame with X and Y + coordinates as additional columns. Parameters __________ gdf : gpd.geodataframe.GeoDataFrame - GeoDataFrame created from vector data containing elements of geom_type LineString - reset_index : bool + GeoDataFrame created from vector data containing elements of geom_type LineString. + + +----+-----------+-----------+----------------------------------------------------+ + | | id | formation | geometry | + +----+-----------+-----------+----------------------------------------------------+ + | 0 | None | Sand1 | LINESTRING (0.256 264.862, 10.593 276.734, 17.... | + +----+-----------+----------------------------------------------------------------+ + | 1 | None | Ton | LINESTRING (0.188 495.787, 8.841 504.142, 41.0... | + +----+-----------+----------------------------------------------------------------+ + | 2 | None | Ton | LINESTRING (970.677 833.053, 959.372 800.023, ... | + +----+-----------+----------------------------------------------------------------+ + + reset_index : bool, default: ``True`` Variable to reset the index of the resulting GeoDataFrame. - Options include: ``True`` or ``False``, default set to ``True`` - drop_id : bool + Options include: ``True`` or ``False``, default set to ``True``. + drop_id : bool, default: ``True`` Variable to drop the id column. - Options include: ``True`` or ``False``, default set to ``True`` - drop_index : bool + Options include: ``True`` or ``False``, default set to ``True``. + drop_index : bool, default: ``True`` Variable to drop the index column. - Options include: ``True`` or ``False``, default set to ``True`` - drop_points : bool + Options include: ``True`` or ``False``, default set to ``True``. + drop_points : bool, default: ``True`` Variable to drop the points column. - Options include: ``True`` or ``False``, default set to ``True`` - drop_level0 : bool + Options include: ``True`` or ``False``, default set to ``True``. + drop_level0 : bool, default: ``True`` Variable to drop the level_0 column. - Options include: ``True`` or ``False``, default set to ``True`` - drop_level1 : bool + Options include: ``True`` or ``False``, default set to ``True``. + drop_level1 : bool, default: ``True`` Variable to drop the level_1 column. - Options include: ``True`` or ``False``, default set to ``True`` - overwrite_xy : bool + Options include: ``True`` or ``False``, default set to ``True``. + overwrite_xy : bool, default: ``False`` Variable to overwrite existing X and Y values. - Options include: ``True`` or ``False``, default set to ``False`` + Options include: ``True`` or ``False``, default set to ``False``. target_crs : Union[str, pyproj.crs.crs.CRS] - Name of the CRS provided to reproject coordinates of the GeoDataFrame, e.g. ``target_crs='EPSG:4647'`` + Name of the CRS provided to reproject coordinates of the GeoDataFrame, e.g. ``target_crs='EPSG:4647'``. bbox : Optional[Sequence[float]] - Values (minx, maxx, miny, maxy) to limit the extent of the data, e.g. ``bbox=[0, 972, 0, 1069]`` + Values (minx, maxx, miny, maxy) to limit the extent of the data, e.g. ``bbox=[0, 972, 0, 1069]``. Returns ------- gdf : gpd.geodataframe.GeoDataFrame - GeoDataFrame with appended X and Y coordinates as additional columns and optional columns + GeoDataFrame with appended X and Y coordinates as additional columns and optional columns. + + +----+-----------+------------------------+-------+--------+ + | | formation | geometry | X | Y | + +----+-----------+------------------------+-------+--------+ + | 0 | Sand1 | POINT (0.256 264.862) | 0.26 | 264.86 | + +----+-----------+------------------------+-------+--------+ + | 1 | Sand1 | POINT (10.593 276.734) | 10.59 | 276.73 | + +----+-----------+------------------------+-------+--------+ + | 2 | Sand1 | POINT (17.135 289.090) | 17.13 | 289.09 | + +----+-----------+------------------------+-------+--------+ + | 3 | Sand1 | POINT (19.150 293.313) | 19.15 | 293.31 | + +----+-----------+------------------------+-------+--------+ + | 4 | Sand1 | POINT (27.795 310.572) | 27.80 | 310.57 | + +----+-----------+------------------------+-------+--------+ .. versionadded:: 1.0.x + .. versionchanged:: 1.2 + Example _______ >>> # Loading Libraries and File @@ -482,27 +509,43 @@ def extract_xy_linestrings( >>> import geopandas as gpd >>> gdf = gpd.read_file(filename='file.shp') >>> gdf - id formation geometry - 0 None Sand1 LINESTRING (0.256 264.862, 10.593 276.734, 17.... - 1 None Ton LINESTRING (0.188 495.787, 8.841 504.142, 41.0... - 2 None Ton LINESTRING (970.677 833.053, 959.372 800.023, ... + + +----+-----------+-----------+----------------------------------------------------+ + | | id | formation | geometry | + +----+-----------+-----------+----------------------------------------------------+ + | 0 | None | Sand1 | LINESTRING (0.256 264.862, 10.593 276.734, 17.... | + +----+-----------+----------------------------------------------------------------+ + | 1 | None | Ton | LINESTRING (0.188 495.787, 8.841 504.142, 41.0... | + +----+-----------+----------------------------------------------------------------+ + | 2 | None | Ton | LINESTRING (970.677 833.053, 959.372 800.023, ... | + +----+-----------+----------------------------------------------------------------+ >>> # Extracting X and Y Coordinates from LineString Objects >>> gdf_xy = gg.vector.extract_xy_linestrings(gdf=gdf, reset_index=False) >>> gdf_xy - formation geometry X Y - 0 Sand1 POINT (0.256 264.862) 0.26 264.86 - 1 Sand1 POINT (10.593 276.734) 10.59 276.73 - 2 Sand1 POINT (17.135 289.090) 17.13 289.09 - 3 Sand1 POINT (19.150 293.313) 19.15 293.31 - 4 Sand1 POINT (27.795 310.572) 27.80 310.57 + + +----+-----------+------------------------+-------+--------+ + | | formation | geometry | X | Y | + +----+-----------+------------------------+-------+--------+ + | 0 | Sand1 | POINT (0.256 264.862) | 0.26 | 264.86 | + +----+-----------+------------------------+-------+--------+ + | 1 | Sand1 | POINT (10.593 276.734) | 10.59 | 276.73 | + +----+-----------+------------------------+-------+--------+ + | 2 | Sand1 | POINT (17.135 289.090) | 17.13 | 289.09 | + +----+-----------+------------------------+-------+--------+ + | 3 | Sand1 | POINT (19.150 293.313) | 19.15 | 293.31 | + +----+-----------+------------------------+-------+--------+ + | 4 | Sand1 | POINT (27.795 310.572) | 27.80 | 310.57 | + +----+-----------+------------------------+-------+--------+ See Also ________ - extract_xy_points : Extracting X and Y coordinates from a GeoDataFrame containing Shapely Points - extract_xy_linestring : Extracting X and Y coordinates from a GeoDataFrame containing Shapely LineStrings and + + extract_xy : Extract X and Y coordinates from Vector Data + extract_xy_points : Extract X and Y coordinates from a GeoDataFrame containing Shapely Points + extract_xy_linestring : Extract X and Y coordinates from a GeoDataFrame containing Shapely LineStrings and saving the X and Y coordinates as lists for each LineString - extract_xy : Extracting X and Y coordinates from Vector Data + Note ____ From 07ce0f4e487358d257ab2542584eb5ba3b65a792 Mon Sep 17 00:00:00 2001 From: AlexanderJuestel Date: Wed, 24 Jul 2024 18:22:17 +0200 Subject: [PATCH 28/63] Format extract_xy --- gemgis/vector.py | 131 +++++++++++++++++++++++++++++++---------------- 1 file changed, 87 insertions(+), 44 deletions(-) diff --git a/gemgis/vector.py b/gemgis/vector.py index 39eab56b..8d2809ee 100644 --- a/gemgis/vector.py +++ b/gemgis/vector.py @@ -706,7 +706,7 @@ def extract_xy( remove_total_bounds: bool = False, threshold_bounds: Union[float, int] = 0.1, ) -> gpd.geodataframe.GeoDataFrame: - """Extracting X and Y coordinates from a GeoDataFrame (Points, LineStrings, MultiLineStrings, Polygons, Geometry + """Extract X and Y coordinates from a GeoDataFrame (Points, LineStrings, MultiLineStrings, Polygons, Geometry Collections) and returning a GeoDataFrame with X and Y coordinates as additional columns Parameters @@ -714,62 +714,89 @@ def extract_xy( gdf : gpd.geodataframe.GeoDataFrame GeoDataFrame created from vector data such as Shapely Points, LineStrings, MultiLineStrings or Polygons or - data loaded from disc with GeoPandas (i.e. Shape File) + data loaded from disc with GeoPandas (i.e. Shape File). + + +----+-----------+------------------------+ + | id | formation | geometry | + +----+-----------+------------------------+ + | 0 | None | POINT (19.150 293.313) | + +----+-----------+------------------------+ + | 1 | None | POINT (61.934 381.459) | + +----+-----------+------------------------+ + | 2 | None | POINT (109.358 480.946)| + +----+-----------+------------------------+ + | 3 | None | POINT (157.812 615.999)| + +----+-----------+------------------------+ + | 4 | None | POINT (191.318 719.094)| + +----+-----------+------------------------+ - reset_index : bool + + reset_index : bool, default: ``True`` Variable to reset the index of the resulting GeoDataFrame. - Options include: ``True`` or ``False``, default set to ``True`` + Options include: ``True`` or ``False``, default set to ``True``. - drop_level0 : bool + drop_level0 : bool, default: ``True`` Variable to drop the level_0 column. - Options include: ``True`` or ``False``, default set to ``True`` + Options include: ``True`` or ``False``, default set to ``True``. - drop_level1 : bool + drop_level1 : bool, default: ``True`` Variable to drop the level_1 column. - Options include: ``True`` or ``False``, default set to ``True`` + Options include: ``True`` or ``False``, default set to ``True``. - drop_index : bool + drop_index : bool, default: ``True`` Variable to drop the index column. - Options include: ``True`` or ``False``, default set to ``True`` + Options include: ``True`` or ``False``, default set to ``True``. - drop_id : bool + drop_id : bool, default: ``True`` Variable to drop the id column. - Options include: ``True`` or ``False``, default set to ``True`` + Options include: ``True`` or ``False``, default set to ``True``. - drop_points : bool + drop_points : bool, default: ``True`` Variable to drop the points column. - Options include: ``True`` or ``False``, default set to ``True`` + Options include: ``True`` or ``False``, default set to ``True``. - overwrite_xy : bool + overwrite_xy : bool, default: ``False`` Variable to overwrite existing X and Y values. - Options include: ``True`` or ``False``, default set to ``False`` + Options include: ``True`` or ``False``, default set to ``False``. target_crs : Union[str, pyproj.crs.crs.CRS] - Name of the CRS provided to reproject coordinates of the GeoDataFrame, e.g. ``target_crs='EPSG:4647'`` + Name of the CRS provided to reproject coordinates of the GeoDataFrame, e.g. ``target_crs='EPSG:4647'``. bbox : list - Values (minx, maxx, miny, maxy) to limit the extent of the data, e.g. ``bbox=[0, 972, 0, 1069]`` + Values (minx, maxx, miny, maxy) to limit the extent of the data, e.g. ``bbox=[0, 972, 0, 1069]``. - remove_total_bounds: bool + remove_total_bounds: bool, default: ``False`` Variable to remove the vertices representing the total bounds of a GeoDataFrame consisting of Polygons - Options include: ``True`` or ``False``, default set to ``False`` + Options include: ``True`` or ``False``, default set to ``False``. - threshold_bounds : Union[float, int] + threshold_bounds : Union[float, int], default: ``0.1`` Variable to set the distance to the total bound from where vertices are being removed, - e.g. ``threshold_bounds=10``, default set to ``0.1`` + e.g. ``threshold_bounds=10``, default set to ``0.1``. Returns _______ gdf : gpd.geodataframe.GeoDataFrame - GeoDataFrame with appended x, y columns and Point geometry features + GeoDataFrame with appended x, y columns and Point geometry features. + + +----+-----------+------------------------+--------+--------+ + | ID | formation | geometry | X | Y | + +----+-----------+------------------------+--------+--------+ + | 0 | Ton | POINT (19.150 293.313) | 19.15 | 293.31 | + +----+-----------+------------------------+--------+--------+ + | 1 | Ton | POINT (61.934 381.459) | 61.93 | 381.46 | + +----+-----------+------------------------+--------+--------+ + | 2 | Ton | POINT (109.358 480.946)| 109.36 | 480.95 | + +----+-----------+------------------------+--------+--------+ + | 3 | Ton | POINT (157.812 615.999)| 157.81 | 616.00 | + +----+-----------+------------------------+--------+--------+ + | 4 | Ton | POINT (191.318 719.094)| 191.32 | 719.09 | + +----+-----------+------------------------+--------+--------+ .. versionadded:: 1.0.x - .. versionchanged:: 1.1 - If a GeoDataFrame contains LineStrings and MultiLineStrings, the index of the exploded GeoDataFrame will now - be reset. Not resetting the index will cause index errors later on. + .. versionchanged:: 1.2 Example _______ @@ -779,40 +806,56 @@ def extract_xy( >>> import geopandas as gpd >>> gdf = gpd.read_file(filename='file.shp') >>> gdf - id formation geometry - 0 None Ton POINT (19.150 293.313) - 1 None Ton POINT (61.934 381.459) - 2 None Ton POINT (109.358 480.946) - 3 None Ton POINT (157.812 615.999) - 4 None Ton POINT (191.318 719.094) + + +----+-----------+------------------------+ + | id | formation | geometry | + +----+-----------+------------------------+ + | 0 | None | POINT (19.150 293.313) | + +----+-----------+------------------------+ + | 1 | None | POINT (61.934 381.459) | + +----+-----------+------------------------+ + | 2 | None | POINT (109.358 480.946)| + +----+-----------+------------------------+ + | 3 | None | POINT (157.812 615.999)| + +----+-----------+------------------------+ + | 4 | None | POINT (191.318 719.094)| + +----+-----------+------------------------+ + >>> # Extracting X and Y Coordinates from Shapely Base Geometries >>> gdf_xy = gg.vector.extract_xy(gdf=gdf, reset_index=False) >>> gdf_xy - formation geometry X Y - 0 Ton POINT (19.150 293.313) 19.15 293.31 - 1 Ton POINT (61.934 381.459) 61.93 381.46 - 2 Ton POINT (109.358 480.946) 109.36 480.95 - 3 Ton POINT (157.812 615.999) 157.81 616.00 - 4 Ton POINT (191.318 719.094) 191.32 719.09 + + +----+-----------+------------------------+--------+--------+ + | ID | formation | geometry | X | Y | + +----+-----------+------------------------+--------+--------+ + | 0 | Ton | POINT (19.150 293.313) | 19.15 | 293.31 | + +----+-----------+------------------------+--------+--------+ + | 1 | Ton | POINT (61.934 381.459) | 61.93 | 381.46 | + +----+-----------+------------------------+--------+--------+ + | 2 | Ton | POINT (109.358 480.946)| 109.36 | 480.95 | + +----+-----------+------------------------+--------+--------+ + | 3 | Ton | POINT (157.812 615.999)| 157.81 | 616.00 | + +----+-----------+------------------------+--------+--------+ + | 4 | Ton | POINT (191.318 719.094)| 191.32 | 719.09 | + +----+-----------+------------------------+--------+--------+ + See Also ________ - extract_xy_points : Extracting X and Y coordinates from a GeoDataFrame containing Shapely Points - extract_xy_linestring : Extracting X and Y coordinates from a GeoDataFrame containing Shapely LineStrings and + extract_xy_points : Extract X and Y coordinates from a GeoDataFrame containing Shapely Points + extract_xy_linestring : Extract X and Y coordinates from a GeoDataFrame containing Shapely LineStrings and saving the X and Y coordinates as lists for each LineString - extract_xy_linestrings : Extracting X and Y coordinates from a GeoDataFrame containing Shapely LineStrings - + extract_xy_linestrings : Extract X and Y coordinates from a GeoDataFrame containing Shapely LineStrings Note ____ GeoDataFrames that contain multiple types of geometries are currently not supported. Please use - ``gdf = gdf.explode().reset_index(drop=True)`` to create a GeoDataFrame with only one type of geometries + ``gdf = gdf.explode().reset_index(drop=True)`` to create a GeoDataFrame with only one type of geometries. """ - # Input object must be a GeoDataFrame if not isinstance(gdf, gpd.geodataframe.GeoDataFrame): raise TypeError("Loaded object is not a GeoDataFrame") From 2e8b18058f3322cd7810b1332ec22ca8e4bdc109 Mon Sep 17 00:00:00 2001 From: AlexanderJuestel Date: Sat, 27 Jul 2024 14:44:59 +0200 Subject: [PATCH 29/63] Format extract_xyz_points --- gemgis/vector.py | 57 +++++++++++++++++++++++++++++++++++------------- 1 file changed, 42 insertions(+), 15 deletions(-) diff --git a/gemgis/vector.py b/gemgis/vector.py index 8d2809ee..883e3a21 100644 --- a/gemgis/vector.py +++ b/gemgis/vector.py @@ -1043,22 +1043,40 @@ def extract_xy( def extract_xyz_points( gdf: gpd.geodataframe.GeoDataFrame, ) -> gpd.geodataframe.GeoDataFrame: - """Extracting X, Y, and Z coordinates from a GeoDataFrame containing Shapely Points with Z components + """Extract X, Y, and Z coordinates from a GeoDataFrame containing Shapely Points with Z components. Parameters __________ gdf : gpd.geodataframe.GeoDataFrame - GeoDataFrame containing Shapely Points with X, Y, and Z components + GeoDataFrame containing Shapely Points with X, Y, and Z components. + + +----+-----------------------------------+ + | | geometry | + +----+-----------------------------------+ + | 0 | POINT Z (1.00000 2.00000 4.00000) | + +----+-----------------------------------+ + | 1 | POINT Z (1.00000 2.00000 4.00000) | + +----+-----------------------------------+ Returns _______ gdf : gpd.geodataframe.GeoDataFrame - GeoDataFrame containing Shapely Points with appended X, Y, and Z columns + GeoDataFrame containing Shapely Points with appended X, Y, and Z columns. + + +----+-----------------------------------+-------+-------+-------+ + | ID | geometry | X | Y | Z | + +----+-----------------------------------+-------+-------+-------+ + | 0 | POINT Z (1.00000 2.00000 4.00000) | 1.00 | 2.00 | 4.00 | + +----+-----------------------------------+-------+-------+-------+ + | 1 | POINT Z (1.00000 2.00000 4.00000) | 1.00 | 2.00 | 4.00 | + +----+-----------------------------------+-------+-------+-------+ .. versionadded:: 1.0.x + .. versionchanged:: 1.2 + Example _______ @@ -1068,33 +1086,42 @@ def extract_xyz_points( >>> import geopandas as gpd >>> point = Point(1,2,4) >>> point.wkt - 'POINT Z (0 0 0)' + 'POINT Z (1 2 4)' >>> # Creating GeoDataFrame from Point >>> gdf = gpd.GeoDataFrame(geometry=[point, point]) >>> gdf - geometry - 0 POINT Z (0.00000 0.00000 0.00000) - 1 POINT Z (0.00000 0.00000 0.00000) + + +----+-----------------------------------+ + | | geometry | + +----+-----------------------------------+ + | 0 | POINT Z (1.00000 2.00000 4.00000) | + +----+-----------------------------------+ + | 1 | POINT Z (1.00000 2.00000 4.00000) | + +----+-----------------------------------+ >>> # Extracting X, Y, and Z Coordinates from Point Objects >>> gdf = gg.vector.extract_xyz_points(gdf=gdf) >>> gdf - geometry X Y Z - 0 POINT Z (1.00000 2.00000 3.00000) 1.00 2.00 3.00 - 1 POINT Z (1.00000 2.00000 3.00000) 1.00 2.00 3.00 + + +----+-----------------------------------+-------+-------+-------+ + | ID | geometry | X | Y | Z | + +----+-----------------------------------+-------+-------+-------+ + | 0 | POINT Z (1.00000 2.00000 4.00000) | 1.00 | 2.00 | 4.00 | + +----+-----------------------------------+-------+-------+-------+ + | 1 | POINT Z (1.00000 2.00000 4.00000) | 1.00 | 2.00 | 4.00 | + +----+-----------------------------------+-------+-------+-------+ See Also ________ - extract_xyz_linestrings: Extracting X and Y coordinates from a GeoDataFrame containing Shapely LineStrings with - Z components - extract_xyz_polygons: Extracting X and Y coordinates from a GeoDataFrame containing Shapely Polygons with Z - component + extract_xyz_linestrings: Extract X and Y coordinates from a GeoDataFrame containing Shapely LineStrings with + Z components. + extract_xyz_polygons: Extract X and Y coordinates from a GeoDataFrame containing Shapely Polygons with Z + component. """ - # Checking that the input data is of type GeoDataFrame if not isinstance(gdf, gpd.geodataframe.GeoDataFrame): raise TypeError("Loaded object is not a GeoDataFrame") From 3f3b96f210d65bf7911d10a81eea25c8d262a2ca Mon Sep 17 00:00:00 2001 From: AlexanderJuestel Date: Sat, 27 Jul 2024 14:56:17 +0200 Subject: [PATCH 30/63] Format extract_xyz_linestrings --- gemgis/vector.py | 70 ++++++++++++++++++++++++++++++++++++------------ 1 file changed, 53 insertions(+), 17 deletions(-) diff --git a/gemgis/vector.py b/gemgis/vector.py index 883e3a21..d8cc62e5 100644 --- a/gemgis/vector.py +++ b/gemgis/vector.py @@ -1155,30 +1155,52 @@ def extract_xyz_linestrings( reset_index: bool = True, drop_index: bool = True, ) -> gpd.geodataframe.GeoDataFrame: - """Extracting X, Y, and Z coordinates from a GeoDataFrame containing Shapely LineStrings with Z components + """Extract X, Y, and Z coordinates from a GeoDataFrame containing Shapely LineStrings with Z components. Parameters __________ gdf : gpd.geodataframe.GeoDataFrame - GeoDataFrame containing Shapely LineStrings with X, Y, and Z components + GeoDataFrame containing Shapely LineStrings with X, Y, and Z components. - reset_index : bool + +----+-----------------------------------------------------+ + | | geometry | + +----+-----------------------------------------------------+ + | 0 | LINESTRING Z (1.00000 2.00000 3.00000, 4.00000 ...) | + +----+-----------------------------------------------------+ + | 1 | LINESTRING Z (1.00000 2.00000 3.00000, 4.00000 ...) | + +----+-----------------------------------------------------+ + + reset_index : bool, default: ``True`` Variable to reset the index of the resulting GeoDataFrame. - Options include: ``True`` or ``False``, default set to ``True`` + Options include: ``True`` or ``False``, default set to ``True``. - drop_index : bool + drop_index : bool, default: ``True`` Variable to drop the index column. - Options include: ``True`` or ``False``, default set to ``True`` + Options include: ``True`` or ``False``, default set to ``True``. Returns _______ gdf : gpd.geodataframe.GeoDataFrame - GeoDataFrame containing Shapely Points with appended X, Y, and Z columns + GeoDataFrame containing Shapely Points with appended X, Y, and Z columns. + + +----+------------------------+----------------+-------+-------+-------+ + | | geometry | points | X | Y | Z | + +----+------------------------+----------------+-------+-------+-------+ + | 0 | POINT (1.00000 2.00000)| (1.0, 2.0, 3.0)| 1.00 | 2.00 | 3.00 | + +----+------------------------+----------------+-------+-------+-------+ + | 1 | POINT (4.00000 5.00000)| (4.0, 5.0, 6.0)| 4.00 | 5.00 | 6.00 | + +----+------------------------+----------------+-------+-------+-------+ + | 2 | POINT (1.00000 2.00000)| (1.0, 2.0, 3.0)| 1.00 | 2.00 | 3.00 | + +----+------------------------+----------------+-------+-------+-------+ + | 3 | POINT (4.00000 5.00000)| (4.0, 5.0, 6.0)| 4.00 | 5.00 | 6.00 | + +----+------------------------+----------------+-------+-------+-------+ .. versionadded:: 1.0.x + .. versionchanged:: 1.2 + Example _______ @@ -1193,25 +1215,39 @@ def extract_xyz_linestrings( >>> # Creating GeoDataFrame from LineString >>> gdf = gpd.GeoDataFrame(geometry=[linestring, linestring]) >>> gdf - geometry - 0 LINESTRING Z (1.00000 2.00000 3.00000, 4.00000... - 1 LINESTRING Z (1.00000 2.00000 3.00000, 4.00000... + + +----+-----------------------------------------------------+ + | | geometry | + +----+-----------------------------------------------------+ + | 0 | LINESTRING Z (1.00000 2.00000 3.00000, 4.00000 ...) | + +----+-----------------------------------------------------+ + | 1 | LINESTRING Z (1.00000 2.00000 3.00000, 4.00000 ...) | + +----+-----------------------------------------------------+ + >>> # Extracting X, Y, and Z Coordinates from Point Objects >>> gdf = gg.vector.extract_xyz_linestrings(gdf=gdf) >>> gdf - geometry points X Y Z - 0 POINT (1.00000 2.00000) (1.0, 2.0, 3.0) 1.00 2.00 3.00 - 1 POINT (4.00000 5.00000) (4.0, 5.0, 6.0) 4.00 5.00 6.00 - 2 POINT (1.00000 2.00000) (1.0, 2.0, 3.0) 1.00 2.00 3.00 - 3 POINT (4.00000 5.00000) (4.0, 5.0, 6.0) 4.00 5.00 6.00 + + +----+------------------------+----------------+-------+-------+-------+ + | | geometry | points | X | Y | Z | + +----+------------------------+----------------+-------+-------+-------+ + | 0 | POINT (1.00000 2.00000)| (1.0, 2.0, 3.0)| 1.00 | 2.00 | 3.00 | + +----+------------------------+----------------+-------+-------+-------+ + | 1 | POINT (4.00000 5.00000)| (4.0, 5.0, 6.0)| 4.00 | 5.00 | 6.00 | + +----+------------------------+----------------+-------+-------+-------+ + | 2 | POINT (1.00000 2.00000)| (1.0, 2.0, 3.0)| 1.00 | 2.00 | 3.00 | + +----+------------------------+----------------+-------+-------+-------+ + | 3 | POINT (4.00000 5.00000)| (4.0, 5.0, 6.0)| 4.00 | 5.00 | 6.00 | + +----+------------------------+----------------+-------+-------+-------+ + See Also ________ - extract_xyz_points: Extracting X and Y coordinates from a GeoDataFrame containing Shapely Points with + extract_xyz_points: Extract X and Y coordinates from a GeoDataFrame containing Shapely Points with Z components - extract_xyz_polygons: Extracting X and Y coordinates from a GeoDataFrame containing Shapely Polygons with Z + extract_xyz_polygons: Extract X and Y coordinates from a GeoDataFrame containing Shapely Polygons with Z component """ From 58c82bfa5caa62eee01817e6f0f6c411e6ad9bbf Mon Sep 17 00:00:00 2001 From: AlexanderJuestel Date: Sat, 27 Jul 2024 15:06:04 +0200 Subject: [PATCH 31/63] Format extract_xyz_polygons --- gemgis/vector.py | 69 ++++++++++++++++++++++++++++++++++++------------ 1 file changed, 52 insertions(+), 17 deletions(-) diff --git a/gemgis/vector.py b/gemgis/vector.py index d8cc62e5..501f179b 100644 --- a/gemgis/vector.py +++ b/gemgis/vector.py @@ -1310,30 +1310,52 @@ def extract_xyz_polygons( reset_index: bool = True, drop_index: bool = True, ) -> gpd.geodataframe.GeoDataFrame: - """Extracting X, Y, and Z coordinates from a GeoDataFrame containing Shapely Polygons with Z components + """Extract X, Y, and Z coordinates from a GeoDataFrame containing Shapely Polygons with Z components. Parameters __________ gdf : gpd.geodataframe.GeoDataFrame - GeoDataFrame containing Shapely Polygons with X, Y, and Z components + GeoDataFrame containing Shapely Polygons with X, Y, and Z components. - reset_index : bool + +----+--------------------------------------------------+ + | | geometry | + +----+--------------------------------------------------+ + | 0 | POLYGON Z ((0.00000 0.00000 1.00000, 1.00000 ... | + +----+--------------------------------------------------+ + | 1 | POLYGON Z ((0.00000 0.00000 1.00000, 1.00000 ... | + +----+--------------------------------------------------+ + + reset_index : bool, default: ``True`` Variable to reset the index of the resulting GeoDataFrame. Options include: ``True`` or ``False``, default set to ``True`` - drop_index : bool + drop_index : bool, default: ``True`` Variable to drop the index column. - Options include: ``True`` or ``False``, default set to ``True`` + Options include: ``True`` or ``False``, default set to ``True``. Returns _______ gdf : gpd.geodataframe.GeoDataFrame - GeoDataFrame containing Shapely Points with appended X, Y, and Z columns + GeoDataFrame containing Shapely Points with appended X, Y, and Z columns. + + +----+------------------------+----------------+-------+-------+-------+ + | ID | geometry | points | X | Y | Z | + +----+------------------------+----------------+-------+-------+-------+ + | 0 | POINT (0.00000 0.00000)| [0.0, 0.0, 1.0]| 0.00 | 0.00 | 1.00 | + +----+------------------------+----------------+-------+-------+-------+ + | 1 | POINT (1.00000 0.00000)| [1.0, 0.0, 1.0]| 1.00 | 0.00 | 1.00 | + +----+------------------------+----------------+-------+-------+-------+ + | 2 | POINT (1.00000 1.00000)| [1.0, 1.0, 1.0]| 1.00 | 1.00 | 1.00 | + +----+------------------------+----------------+-------+-------+-------+ + | 3 | POINT (0.00000 1.00000)| [0.0, 1.0, 1.0]| 0.00 | 1.00 | 1.00 | + +----+------------------------+----------------+-------+-------+-------+ .. versionadded:: 1.0.x + .. versionchanged:: 1.2 + Example _______ @@ -1348,30 +1370,43 @@ def extract_xyz_polygons( >>> # Creating GeoDataFrame from LineString >>> gdf = gpd.GeoDataFrame(geometry=[polygon, polygon]) >>> gdf - geometry - 0 POLYGON Z ((0.00000 0.00000 1.00000, 1.00000 0... - 1 POLYGON Z ((0.00000 0.00000 1.00000, 1.00000 0... + + +----+--------------------------------------------------+ + | | geometry | + +----+--------------------------------------------------+ + | 0 | POLYGON Z ((0.00000 0.00000 1.00000, 1.00000 ... | + +----+--------------------------------------------------+ + | 1 | POLYGON Z ((0.00000 0.00000 1.00000, 1.00000 ... | + +----+--------------------------------------------------+ + >>> # Extracting X, Y, and Z Coordinates from Point Objects >>> gdf = gg.vector.extract_xyz_polygons(gdf=gdf) >>> gdf - geometry points X Y Z - 0 POINT (0.00000 0.00000) [0.0, 0.0, 1.0] 0.00 0.00 1.00 - 1 POINT (1.00000 0.00000) [1.0, 0.0, 1.0] 1.00 0.00 1.00 - 2 POINT (1.00000 1.00000) [1.0, 1.0, 1.0] 1.00 1.00 1.00 - 3 POINT (0.00000 1.00000) [0.0, 1.0, 1.0] 0.00 1.00 1.00 + + +----+------------------------+----------------+-------+-------+-------+ + | ID | geometry | points | X | Y | Z | + +----+------------------------+----------------+-------+-------+-------+ + | 0 | POINT (0.00000 0.00000)| [0.0, 0.0, 1.0]| 0.00 | 0.00 | 1.00 | + +----+------------------------+----------------+-------+-------+-------+ + | 1 | POINT (1.00000 0.00000)| [1.0, 0.0, 1.0]| 1.00 | 0.00 | 1.00 | + +----+------------------------+----------------+-------+-------+-------+ + | 2 | POINT (1.00000 1.00000)| [1.0, 1.0, 1.0]| 1.00 | 1.00 | 1.00 | + +----+------------------------+----------------+-------+-------+-------+ + | 3 | POINT (0.00000 1.00000)| [0.0, 1.0, 1.0]| 0.00 | 1.00 | 1.00 | + +----+------------------------+----------------+-------+-------+-------+ + See Also ________ - extract_xyz_points: Extracting X and Y coordinates from a GeoDataFrame containing Shapely Points with Z + extract_xyz_points: Extract X and Y coordinates from a GeoDataFrame containing Shapely Points with Z component - extract_xyz_linestrings: Extracting X and Y coordinates from a GeoDataFrame containing Shapely LineStrings with + extract_xyz_linestrings: Extract X and Y coordinates from a GeoDataFrame containing Shapely LineStrings with Z components """ - # Checking that the input data is of type GeoDataFrame if not isinstance(gdf, gpd.geodataframe.GeoDataFrame): raise TypeError("Loaded object is not a GeoDataFrame") From 2df1ec3b8661443d3e59d0591ac04ec1be4cf109 Mon Sep 17 00:00:00 2001 From: AlexanderJuestel Date: Sat, 27 Jul 2024 15:30:13 +0200 Subject: [PATCH 32/63] Format extract_xyz_rasterio --- gemgis/vector.py | 133 +++++++++++++++++++++++++++++------------------ 1 file changed, 83 insertions(+), 50 deletions(-) diff --git a/gemgis/vector.py b/gemgis/vector.py index 501f179b..6d316e58 100644 --- a/gemgis/vector.py +++ b/gemgis/vector.py @@ -1477,60 +1477,76 @@ def extract_xyz_rasterio( remove_total_bounds: bool = False, threshold_bounds: Union[float, int] = 0.1, ) -> gpd.geodataframe.GeoDataFrame: - """Extracting X and Y coordinates from a GeoDataFrame (Points, LineStrings, MultiLineStrings Polygons) and z values - from a rasterio object and returning a GeoDataFrame with X, Y, and Z coordinates as additional columns + """Extract X and Y coordinates from a GeoDataFrame (Points, LineStrings, MultiLineStrings Polygons) and z values + from a rasterio object and returning a GeoDataFrame with X, Y, and Z coordinates as additional columns. Parameters __________ gdf : gpd.geodataframe.GeoDataFrame - GeoDataFrame created from vector data containing Shapely Points, LineStrings, MultiLineStrings or Polygons + GeoDataFrame created from vector data containing Shapely Points, LineStrings, MultiLineStrings or Polygons. + + +----+-----------+------------------------+ + | | formation | geometry | + +----+-----------+------------------------+ + | 0 | Ton | POINT (19.150 293.313) | + +----+-----------+------------------------+ + | 1 | Ton | POINT (61.934 381.459) | + +----+-----------+------------------------+ + | 2 | Ton | POINT (109.358 480.946)| + +----+-----------+------------------------+ + | 3 | Ton | POINT (157.812 615.999)| + +----+-----------+------------------------+ + | 4 | Ton | POINT (191.318 719.094)| + +----+-----------+------------------------+ dem : rasterio.io.DatasetReader - Rasterio object containing the height values + Rasterio object containing the height values. - minz : float - Value defining the minimum elevation the data needs to be returned, e.g. ``minz=50``, default ``None`` + minz : float, default: ``None`` + Value defining the minimum elevation the data needs to be returned, e.g. ``minz=50``, default ``None``. - maxz : float - Value defining the maximum elevation the data needs to be returned, e.g. ``maxz=500``, default ``None`` + maxz : float, default: ``None`` + Value defining the maximum elevation the data needs to be returned, e.g. ``maxz=500``, default ``None```. - reset_index : bool - Variable to reset the index of the resulting GeoDataFrame, default ``True`` + reset_index : bool, default: ``True`` + Variable to reset the index of the resulting GeoDataFrame, e.g. ``reset_index=True``. + Options include: ``True`` or ``False``, default set to ``True``. - drop_level0 : bool - Variable to drop the level_0 column. - Options include: ``True`` or ``False``, default set to ``True`` + drop_level0 : bool, default: ``True`` + Variable to drop the level_0 column, e.g. ``drop_level0=True``. + Options include: ``True`` or ``False``, default set to ``True``. - drop_level1 : bool - Variable to drop the level_1 column. - Options include: ``True`` or ``False``, default set to ``True`` + drop_level1 : bool, default: ``True`` + Variable to drop the level_1 column, e.g. ``drop_level1=True``. + Options include: ``True`` or ``False``, default set to ``True``. - drop_index : bool - Variable to drop the index column. - Options include: ``True`` or ``False``, default set to ``True`` + drop_index : bool, default: ``True`` + Variable to drop the index column, e.g. ``drop_index=True``. + Options include: ``True`` or ``False``, default set to ``True``. - drop_id : bool + drop_id : bool, default: ``True`` Variable to drop the id column. - Options include: ``True`` or ``False``, default set to ``True`` + Options include: ``True`` or ``False``, default set to ``True``. - drop_points : bool - Variable to drop the points column. - Options include: ``True`` or ``False``, default set to ``True`` + drop_points : bool, default: ``True`` + Variable to drop the points column, e.g. ``drop_points=True``. + Options include: ``True`` or ``False``, default set to ``True``. - target_crs : Union[str, pyproj.crs.crs.CRS, rasterio.crs.CRS] - Name of the CRS provided to reproject coordinates of the GeoDataFrame, e.g. ``target_crs='EPSG:4647'`` + target_crs : Union[str, pyproj.crs.crs.CRS, rasterio.crs.CRS], default: ``None`` + Name of the CRS provided to reproject coordinates of the GeoDataFrame, e.g. ``target_crs='EPSG:4647'``. - bbox : list - Values (minx, maxx, miny, maxy) to limit the extent of the data, e.g. ``bbox=[0, 972, 0, 1069]`` + bbox : list, default: ``None`` + Values (minx, maxx, miny, maxy) to limit the extent of the data, e.g. ``bbox=[0, 972, 0, 1069]``. - remove_total_bounds: bool - Variable to remove the vertices representing the total bounds of a GeoDataFrame consisting of Polygons - Options include: ``True`` or ``False``, default set to ``False`` + remove_total_bounds: bool, default: ``False`` + Variable to remove the vertices representing the total bounds of a GeoDataFrame consisting of Polygons, + e.g. ``remove_total_bounds=False``. + Options include: ``True`` or ``False``, default set to ``False``. - threshold_bounds : Union[float, int] - Variable to set the distance to the total bound from where vertices are being removed, - e.g. ``threshold_bounds=10``, default set to ``0.1`` + threshold_bounds : Union[float, int], default: ``0.1`` + Variable to set the distance to the total bounds from where vertices are being removed, + e.g. ``threshold_bounds=10``, default set to ``0.1``. Returns _______ @@ -1549,12 +1565,21 @@ def extract_xyz_rasterio( >>> import rasterio >>> gdf = gpd.read_file(filename='file.shp') >>> gdf - id formation geometry - 0 None Ton POINT (19.150 293.313) - 1 None Ton POINT (61.934 381.459) - 2 None Ton POINT (109.358 480.946) - 3 None Ton POINT (157.812 615.999) - 4 None Ton POINT (191.318 719.094) + + +----+-----------+------------------------+ + | | formation | geometry | + +----+-----------+------------------------+ + | 0 | Ton | POINT (19.150 293.313) | + +----+-----------+------------------------+ + | 1 | Ton | POINT (61.934 381.459) | + +----+-----------+------------------------+ + | 2 | Ton | POINT (109.358 480.946)| + +----+-----------+------------------------+ + | 3 | Ton | POINT (157.812 615.999)| + +----+-----------+------------------------+ + | 4 | Ton | POINT (191.318 719.094)| + +----+-----------+------------------------+ + >>> # Loading raster file >>> dem = rasterio.open(fp='dem.tif') @@ -1564,21 +1589,29 @@ def extract_xyz_rasterio( >>> # Extracting X, Y, and Z Coordinates from Shapely Base Geometries and raster >>> gdf_xyz = gg.vector.extract_xyz_rasterio(gdf=gdf, dem=dem, reset_index=reset_index) >>> gdf_xyz - formation geometry X Y Z - 0 Ton POINT (19.150 293.313) 19.15 293.31 364.99 - 1 Ton POINT (61.934 381.459) 61.93 381.46 400.34 - 2 Ton POINT (109.358 480.946) 109.36 480.95 459.55 - 3 Ton POINT (157.812 615.999) 157.81 616.00 525.69 - 4 Ton POINT (191.318 719.094) 191.32 719.09 597.63 + + +----+-----------+------------------------+-------+-------+-------+ + | ID | formation | geometry | X | Y | Z | + +----+-----------+------------------------+-------+-------+-------+ + | 0 | Ton | POINT (19.150 293.313) | 19.15 | 293.31| 364.99| + +----+-----------+------------------------+-------+-------+-------+ + | 1 | Ton | POINT (61.934 381.459) | 61.93 | 381.46| 400.34| + +----+-----------+------------------------+-------+-------+-------+ + | 2 | Ton | POINT (109.358 480.946)| 109.36| 480.95| 459.55| + +----+-----------+------------------------+-------+-------+-------+ + | 3 | Ton | POINT (157.812 615.999)| 157.81| 616.00| 525.69| + +----+-----------+------------------------+-------+-------+-------+ + | 4 | Ton | POINT (191.318 719.094)| 191.32| 719.09| 597.63| + +----+-----------+------------------------+-------+-------+-------+ + See Also ________ - extract_xyz_array : Extracting X, Y, and Z coordinates from a GeoDataFrame and Digital Elevation Model as array - extract_xyz : Extracting X, Y, and Z coordinates from a GeoDataFrame and Digital Elevation Model + extract_xyz : Extract X, Y, and Z coordinates from a GeoDataFrame and Digital Elevation Model + extract_xyz_array : Extract X, Y, and Z coordinates from a GeoDataFrame and Digital Elevation Model as array """ - # Checking that the input data is of type GeoDataFrame if not isinstance(gdf, gpd.geodataframe.GeoDataFrame): raise TypeError("Loaded object is not a GeoDataFrame") @@ -1725,7 +1758,7 @@ def extract_xyz_rasterio( # Extracting the X and Y coordinates of the reprojected gdf gdf = extract_xy( - gdf, + gdf=gdf, reset_index=False, drop_index=False, drop_id=False, From c5def259d42e4515d522b9db9111c48d0765bc5d Mon Sep 17 00:00:00 2001 From: AlexanderJuestel Date: Sun, 28 Jul 2024 12:28:48 +0200 Subject: [PATCH 33/63] Format extract_xyz_array and extract_xyz --- gemgis/vector.py | 284 +++++++++++++++++++++++++++++++---------------- 1 file changed, 187 insertions(+), 97 deletions(-) diff --git a/gemgis/vector.py b/gemgis/vector.py index 6d316e58..cdb82bc4 100644 --- a/gemgis/vector.py +++ b/gemgis/vector.py @@ -1835,63 +1835,77 @@ def extract_xyz_array( remove_total_bounds: bool = False, threshold_bounds: Union[float, int] = 0.1, ) -> gpd.geodataframe.GeoDataFrame: - """Extracting X and Y coordinates from a GeoDataFrame (Points, LineStrings, MultiLineStrings Polygons) and Z values from - a NumPy nd.array and returning a GeoDataFrame with X, Y, and Z coordinates as additional columns + """Extract X and Y coordinates from a GeoDataFrame (Points, LineStrings, MultiLineStrings Polygons) and Z values from + a NumPy nd.array and returning a GeoDataFrame with X, Y, and Z coordinates as additional columns. Parameters __________ gdf : gpd.geodataframe.GeoDataFrame - GeoDataFrame created from vector data containing Shapely Points, LineStrings, MultiLineStrings or Polygons + GeoDataFrame created from vector data containing Shapely Points, LineStrings, MultiLineStrings or Polygons. + + +----+-----------+------------------------+ + | | formation | geometry | + +----+-----------+------------------------+ + | 0 | Ton | POINT (19.150 293.313) | + +----+-----------+------------------------+ + | 1 | Ton | POINT (61.934 381.459) | + +----+-----------+------------------------+ + | 2 | Ton | POINT (109.358 480.946)| + +----+-----------+------------------------+ + | 3 | Ton | POINT (157.812 615.999)| + +----+-----------+------------------------+ + | 4 | Ton | POINT (191.318 719.094)| + +----+-----------+------------------------+ dem : np.ndarray - NumPy ndarray containing the height values + NumPy ndarray containing the height values. extent : list List containing the extent of the np.ndarray, - must be provided in the same CRS as the gdf, e.g. ``extent=[0, 972, 0, 1069]`` + must be provided in the same CRS as the gdf, e.g. ``extent=[0, 972, 0, 1069]``. - minz : float - Value defining the minimum elevation the data needs to be returned, e.g. ``minz=50``, default ``None`` + minz : float, default: ``None`` + Value defining the minimum elevation the data needs to be returned, e.g. ``minz=50``, default ``None``. - maxz : float - Value defining the maximum elevation the data needs to be returned, e.g. ``maxz=500``, default ``None`` + maxz : float, default: ``None`` + Value defining the maximum elevation the data needs to be returned, e.g. ``maxz=500``, default ``None``. - reset_index : bool - Variable to reset the index of the resulting GeoDataFrame. - Options include: ``True`` or ``False``, default set to ``True`` + reset_index : bool, default: ``None`` + Variable to reset the index of the resulting GeoDataFrame, e.g. ``reset_index=True``. + Options include: ``True`` or ``False``, default set to ``True``. - drop_level0 : bool - Variable to drop the level_0 column. - Options include: ``True`` or ``False``, default set to ``True`` + drop_level0 : bool, default: ``True`` + Variable to drop the level_0 column, e.g. ``drop_level0=True``. + Options include: ``True`` or ``False``, default set to ``True``. - drop_level1 : bool - Variable to drop the level_1 column. - Options include: ``True`` or ``False``, default set to ``True`` + drop_level1 : bool, default: ``True`` + Variable to drop the level_1 column, e.g. ``drop_level1=True``. + Options include: ``True`` or ``False``, default set to ``True``. - drop_index : bool - Variable to drop the index column. - Options include: ``True`` or ``False``, default set to ``True`` + drop_index : bool, default: ``True`` + Variable to drop the index column, e.g. ``drop_index=True``. + Options include: ``True`` or ``False``, default set to ``True``. - drop_id : bool - Variable to drop the id column. - Options include: ``True`` or ``False``, default set to ``True`` + drop_id : bool, default: ``True`` + Variable to drop the id column, e.g. ``drop_id=True``. + Options include: ``True`` or ``False``, default set to ``True``. - drop_points : bool - Variable to drop the points column. - Options include: ``True`` or ``False``, default set to ``True`` + drop_points : bool, default: ``True`` + Variable to drop the points column, e.g. ``drop_points=True``. + Options include: ``True`` or ``False``, default set to ``True``. target_crs : Union[str, pyproj.crs.crs.CRS] - Name of the CRS provided to reproject coordinates of the GeoDataFrame, e.g. ``target_crs='EPSG:4647'`` + Name of the CRS provided to reproject coordinates of the GeoDataFrame, e.g. ``target_crs='EPSG:4647'``. bbox : list - Values (minx, maxx, miny, maxy) to limit the extent of the data, e.g. ``bbox=[0, 972, 0, 1069]`` + Values (minx, maxx, miny, maxy) to limit the extent of the data, e.g. ``bbox=[0, 972, 0, 1069]``. - remove_total_bounds: bool - Variable to remove the vertices representing the total bounds of a GeoDataFrame consisting of Polygons - Options include: ``True`` or ``False``, default set to ``False`` + remove_total_bounds: bool, default: ``False`` + Variable to remove the vertices representing the total bounds of a GeoDataFrame consisting of Polygons. + Options include: ``True`` or ``False``, default set to ``False``. - threshold_bounds : Union[float, int] + threshold_bounds : Union[float, int], default: ``0.1`` Variable to set the distance to the total bound from where vertices are being removed, e.g. ``threshold_bounds=10``, default set to ``0.1`` @@ -1901,8 +1915,24 @@ def extract_xyz_array( gdf : gpd.geodataframe.GeoDataFrame GeoDataFrame containing the X, Y, and Z coordinates + +----+-----------+------------------------+-------+-------+-------+ + | ID | formation | geometry | X | Y | Z | + +----+-----------+------------------------+-------+-------+-------+ + | 0 | Ton | POINT (19.150 293.313) | 19.15 | 293.31| 364.99| + +----+-----------+------------------------+-------+-------+-------+ + | 1 | Ton | POINT (61.934 381.459) | 61.93 | 381.46| 400.34| + +----+-----------+------------------------+-------+-------+-------+ + | 2 | Ton | POINT (109.358 480.946)| 109.36| 480.95| 459.55| + +----+-----------+------------------------+-------+-------+-------+ + | 3 | Ton | POINT (157.812 615.999)| 157.81| 616.00| 525.69| + +----+-----------+------------------------+-------+-------+-------+ + | 4 | Ton | POINT (191.318 719.094)| 191.32| 719.09| 597.63| + +----+-----------+------------------------+-------+-------+-------+ + .. versionadded:: 1.0.x + .. versionchanged:: 1.2 + Example _______ @@ -1912,12 +1942,20 @@ def extract_xyz_array( >>> import rasterio >>> gdf = gpd.read_file(filename='file.shp') >>> gdf - id formation geometry - 0 None Ton POINT (19.150 293.313) - 1 None Ton POINT (61.934 381.459) - 2 None Ton POINT (109.358 480.946) - 3 None Ton POINT (157.812 615.999) - 4 None Ton POINT (191.318 719.094) + + +----+-----------+------------------------+ + | | formation | geometry | + +----+-----------+------------------------+ + | 0 | Ton | POINT (19.150 293.313) | + +----+-----------+------------------------+ + | 1 | Ton | POINT (61.934 381.459) | + +----+-----------+------------------------+ + | 2 | Ton | POINT (109.358 480.946)| + +----+-----------+------------------------+ + | 3 | Ton | POINT (157.812 615.999)| + +----+-----------+------------------------+ + | 4 | Ton | POINT (191.318 719.094)| + +----+-----------+------------------------+ >>> # Loading raster file >>> dem = rasterio.open(fp='dem.tif') @@ -1930,22 +1968,29 @@ def extract_xyz_array( >>> # Extracting X, Y, and Z Coordinates from Shapely Base Geometries and array >>> gdf_xyz = gg.vector.extract_xyz_array(gdf=gdf, dem=dem.read(1), extent=extent, reset_index=reset_index) >>> gdf_xyz - formation geometry X Y Z - 0 Ton POINT (19.150 293.313) 19.15 293.31 364.99 - 1 Ton POINT (61.934 381.459) 61.93 381.46 400.34 - 2 Ton POINT (109.358 480.946) 109.36 480.95 459.55 - 3 Ton POINT (157.812 615.999) 157.81 616.00 525.69 - 4 Ton POINT (191.318 719.094) 191.32 719.09 597.63 + + +----+-----------+------------------------+-------+-------+-------+ + | ID | formation | geometry | X | Y | Z | + +----+-----------+------------------------+-------+-------+-------+ + | 0 | Ton | POINT (19.150 293.313) | 19.15 | 293.31| 364.99| + +----+-----------+------------------------+-------+-------+-------+ + | 1 | Ton | POINT (61.934 381.459) | 61.93 | 381.46| 400.34| + +----+-----------+------------------------+-------+-------+-------+ + | 2 | Ton | POINT (109.358 480.946)| 109.36| 480.95| 459.55| + +----+-----------+------------------------+-------+-------+-------+ + | 3 | Ton | POINT (157.812 615.999)| 157.81| 616.00| 525.69| + +----+-----------+------------------------+-------+-------+-------+ + | 4 | Ton | POINT (191.318 719.094)| 191.32| 719.09| 597.63| + +----+-----------+------------------------+-------+-------+-------+ See Also ________ - extract_xyz_rasterio : Extracting X, Y, and Z coordinates from a GeoDataFrame and Digital Elevation Model + extract_xyz : Extract X, Y, and Z coordinates from a GeoDataFrame and Digital Elevation Model + extract_xyz_rasterio : Extract X, Y, and Z coordinates from a GeoDataFrame and Digital Elevation Model as rasterio object - extract_xyz : Extracting X, Y, and Z coordinates from a GeoDataFrame and Digital Elevation Model """ - # Checking that the input data is of type GeoDataFrame if not isinstance(gdf, gpd.geodataframe.GeoDataFrame): raise TypeError("Loaded object is not a GeoDataFrame") @@ -1980,7 +2025,7 @@ def extract_xyz_array( if not isinstance(drop_points, bool): raise TypeError("Drop_points argument must be of type bool") - # Checking that drop_id is of type bool + # Checking that drop_index is of type bool if not isinstance(drop_index, bool): raise TypeError("Drop_index argument must be of type bool") @@ -2156,8 +2201,8 @@ def extract_xyz( remove_total_bounds: bool = False, threshold_bounds: Union[float, int] = 0.1, ) -> gpd.geodataframe.GeoDataFrame: - """Extracting X and Y coordinates from a GeoDataFrame (Points, LineStrings, MultiLineStrings Polygons) and Z values from - a NumPy nd.array or a Rasterio object and returning a GeoDataFrame with X, Y, and Z coordinates as additional columns + """Extract X and Y coordinates from a GeoDataFrame (Points, LineStrings, MultiLineStrings Polygons) and Z values from + a NumPy nd.array or a Rasterio object and returning a GeoDataFrame with X, Y, and Z coordinates as additional columns. Parameters __________ @@ -2167,55 +2212,69 @@ def extract_xyz( dem : Union[np.ndarray, rasterio.io.DatasetReader] NumPy ndarray or Rasterio object containing the height values, default value is None in case geometries - contain Z values + contain Z values. + + +----+-----------+------------------------+ + | | formation | geometry | + +----+-----------+------------------------+ + | 0 | Ton | POINT (19.150 293.313) | + +----+-----------+------------------------+ + | 1 | Ton | POINT (61.934 381.459) | + +----+-----------+------------------------+ + | 2 | Ton | POINT (109.358 480.946)| + +----+-----------+------------------------+ + | 3 | Ton | POINT (157.812 615.999)| + +----+-----------+------------------------+ + | 4 | Ton | POINT (191.318 719.094)| + +----+-----------+------------------------+ - minz : float - Value defining the minimum elevation of the data that needs to be returned, e.g. ``minz=50``, default ``None`` + minz : float, default: ``None`` + Value defining the minimum elevation of the data that needs to be returned, e.g. ``minz=50``, default ``None``. - maxz : float - Value defining the maximum elevation of the data that needs to be returned, e.g. ``maxz=500``, default ``None`` + maxz : float, default: ``None`` + Value defining the maximum elevation of the data that needs to be returned, e.g. ``maxz=500``, default ``None``. extent : List[Union[float,int]] List containing the extent of the np.ndarray, - must be provided in the same CRS as the gdf, e.g. ``extent=[0, 972, 0, 1069]`` + must be provided in the same CRS as the gdf, e.g. ``extent=[0, 972, 0, 1069]``. - reset_index : bool - Variable to reset the index of the resulting GeoDataFrame. - Options include: ``True`` or ``False``, default set to ``True`` + reset_index : bool, default: ``True`` + Variable to reset the index of the resulting GeoDataFrame, e.g. ``reset_index=True``. + Options include: ``True`` or ``False``, default set to ``True``. - drop_level0 : bool - Variable to drop the level_0 column. - Options include: ``True`` or ``False``, default set to ``True`` + drop_level0 : bool, default: ``True`` + Variable to drop the level_0 column, e.g. ``drop_level0=True``. + Options include: ``True`` or ``False``, default set to ``True``. - drop_level1 : bool - Variable to drop the level_1 column. - Options include: ``True`` or ``False``, default set to ``True`` + drop_level1 : bool, default: ``True`` + Variable to drop the level_1 column, e.g. ``drop_level1=True``. + Options include: ``True`` or ``False``, default set to ``True``. - drop_index : bool - Variable to drop the index column. - Options include: ``True`` or ``False``, default set to ``True`` + drop_index : bool, default: ``True`` + Variable to drop the index column, e.g. ``drop_index=True``. + Options include: ``True`` or ``False``, default set to ``True``. - drop_id : bool - Variable to drop the id column. - Options include: ``True`` or ``False``, default set to ``True`` + drop_id : bool, default: ``True`` + Variable to drop the id column, e.g. ``drop_id=True``. + Options include: ``True`` or ``False``, default set to ``True``. - drop_points : bool - Variable to drop the points column. - Options include: ``True`` or ``False``, default set to ``True`` + drop_points : bool, default: ``True`` + Variable to drop the points column, e.g. ``drop_points=True``. + Options include: ``True`` or ``False``, default set to ``True``. target_crs : Union[str, pyproj.crs.crs.CRS, rasterio.crs.CRS] - Name of the CRS provided to reproject coordinates of the GeoDataFrame, e.g. ``target_crs='EPSG:4647'`` + Name of the CRS provided to reproject coordinates of the GeoDataFrame, e.g. ``target_crs='EPSG:4647'``. bbox : list - Values (minx, maxx, miny, maxy) to limit the extent of the data, e.g. ``bbox=[0, 972, 0, 1069]`` + Values (minx, maxx, miny, maxy) to limit the extent of the data, e.g. ``bbox=[0, 972, 0, 1069]``. - remove_total_bounds: bool - Variable to remove the vertices representing the total bounds of a GeoDataFrame consisting of Polygons - Options include: ``True`` or ``False``, default set to ``False`` + remove_total_bounds: bool, default: ``False`` + Variable to remove the vertices representing the total bounds of a GeoDataFrame consisting of Polygons. + Options include: ``True`` or ``False``, default set to ``False``. - threshold_bounds : Union[float, int] + threshold_bounds : Union[float, int], default: ``0.1`` Variable to set the distance to the total bound from where vertices are being removed, - e.g. ``threshold_bounds=10``, default set to ``0.1`` + e.g. ``threshold_bounds=10``, default set to ``0.1``. Returns _______ @@ -2223,8 +2282,24 @@ def extract_xyz( gdf : gpd.geodataframe.GeoDataFrame GeoDataFrame containing the X, Y, and Z coordinates as additional columns + +----+-----------+------------------------+-------+-------+-------+ + | ID | formation | geometry | X | Y | Z | + +----+-----------+------------------------+-------+-------+-------+ + | 0 | Ton | POINT (19.150 293.313) | 19.15 | 293.31| 364.99| + +----+-----------+------------------------+-------+-------+-------+ + | 1 | Ton | POINT (61.934 381.459) | 61.93 | 381.46| 400.34| + +----+-----------+------------------------+-------+-------+-------+ + | 2 | Ton | POINT (109.358 480.946)| 109.36| 480.95| 459.55| + +----+-----------+------------------------+-------+-------+-------+ + | 3 | Ton | POINT (157.812 615.999)| 157.81| 616.00| 525.69| + +----+-----------+------------------------+-------+-------+-------+ + | 4 | Ton | POINT (191.318 719.094)| 191.32| 719.09| 597.63| + +----+-----------+------------------------+-------+-------+-------+ + .. versionadded:: 1.0.x + .. versionchanged.: 1.2 + Example _______ @@ -2234,12 +2309,20 @@ def extract_xyz( >>> import rasterio >>> gdf = gpd.read_file(filename='file.shp') >>> gdf - id formation geometry - 0 None Ton POINT (19.150 293.313) - 1 None Ton POINT (61.934 381.459) - 2 None Ton POINT (109.358 480.946) - 3 None Ton POINT (157.812 615.999) - 4 None Ton POINT (191.318 719.094) + + +----+-----------+------------------------+ + | | formation | geometry | + +----+-----------+------------------------+ + | 0 | Ton | POINT (19.150 293.313) | + +----+-----------+------------------------+ + | 1 | Ton | POINT (61.934 381.459) | + +----+-----------+------------------------+ + | 2 | Ton | POINT (109.358 480.946)| + +----+-----------+------------------------+ + | 3 | Ton | POINT (157.812 615.999)| + +----+-----------+------------------------+ + | 4 | Ton | POINT (191.318 719.094)| + +----+-----------+------------------------+ >>> # Loading raster file >>> dem = rasterio.open(fp='dem.tif') @@ -2249,22 +2332,29 @@ def extract_xyz( >>> # Extracting X, Y, and Z Coordinates from Shapely Base Geometries and DEM >>> gdf_xyz = gg.vector.extract_xyz(gdf=gdf, dem=dem, reset_index=reset_index) >>> gdf_xyz - formation geometry X Y Z - 0 Ton POINT (19.150 293.313) 19.15 293.31 364.99 - 1 Ton POINT (61.934 381.459) 61.93 381.46 400.34 - 2 Ton POINT (109.358 480.946) 109.36 480.95 459.55 - 3 Ton POINT (157.812 615.999) 157.81 616.00 525.69 - 4 Ton POINT (191.318 719.094) 191.32 719.09 597.63 + + +----+-----------+------------------------+-------+-------+-------+ + | ID | formation | geometry | X | Y | Z | + +----+-----------+------------------------+-------+-------+-------+ + | 0 | Ton | POINT (19.150 293.313) | 19.15 | 293.31| 364.99| + +----+-----------+------------------------+-------+-------+-------+ + | 1 | Ton | POINT (61.934 381.459) | 61.93 | 381.46| 400.34| + +----+-----------+------------------------+-------+-------+-------+ + | 2 | Ton | POINT (109.358 480.946)| 109.36| 480.95| 459.55| + +----+-----------+------------------------+-------+-------+-------+ + | 3 | Ton | POINT (157.812 615.999)| 157.81| 616.00| 525.69| + +----+-----------+------------------------+-------+-------+-------+ + | 4 | Ton | POINT (191.318 719.094)| 191.32| 719.09| 597.63| + +----+-----------+------------------------+-------+-------+-------+ See Also ________ - extract_xyz_array : Extracting X, Y, and Z coordinates from a GeoDataFrame and Digital Elevation Model as array - extract_xyz_rasterio : Extracting X, Y, and Z coordinates from a GeoDataFrame and Digital Elevation + extract_xyz_array : Extract X, Y, and Z coordinates from a GeoDataFrame and Digital Elevation Model as array + extract_xyz_rasterio : Extract X, Y, and Z coordinates from a GeoDataFrame and Digital Elevation as rasterio object """ - # Checking that the input data is of type GeoDataFrame if not isinstance(gdf, gpd.geodataframe.GeoDataFrame): raise TypeError("Loaded object is not a GeoDataFrame") From 29e819911710e8b7904a331f1c6f14efdd6cd85c Mon Sep 17 00:00:00 2001 From: AlexanderJuestel Date: Sun, 28 Jul 2024 12:30:43 +0200 Subject: [PATCH 34/63] Format explode_linestring --- gemgis/vector.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/gemgis/vector.py b/gemgis/vector.py index cdb82bc4..b7dcb612 100644 --- a/gemgis/vector.py +++ b/gemgis/vector.py @@ -2564,23 +2564,25 @@ def extract_xyz( def explode_linestring( linestring: shapely.geometry.linestring.LineString, ) -> List[shapely.geometry.point.Point]: - """Exploding a LineString to its vertices, also works for LineStrings with Z components + """Explode a LineString to its vertices, also works for LineStrings with Z components. Parameters __________ linestring : shapely.geometry.linestring.LineString Shapely LineString from which vertices are extracted, - e.g. ``linestring = LineString([(0, 0), (10, 10), (20, 20)])`` + e.g. ``linestring = LineString([(0, 0), (10, 10), (20, 20)])``. Returns _______ points_list : List[shapely.geometry.point.Point] - List of extracted Shapely Points + List of extracted Shapely Points. .. versionadded:: 1.0.x + .. versionchanged:: 1.2 + Example _______ @@ -2613,10 +2615,9 @@ def explode_linestring( See Also ________ - explode_linestring_to_elements : Exploding a LineString with more than two vertices into single LineStrings + explode_linestring_to_elements : Explode a LineString with more than two vertices into single LineStrings """ - # Checking that the input geometry is a Shapely LineString if not isinstance(linestring, shapely.geometry.linestring.LineString): raise TypeError("Input geometry must be a Shapely LineString") From 606a03108997031b57b8a7803cc95a8d5be22f38 Mon Sep 17 00:00:00 2001 From: AlexanderJuestel Date: Sun, 28 Jul 2024 12:32:52 +0200 Subject: [PATCH 35/63] Format explode_linestring_to_elements --- gemgis/vector.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/gemgis/vector.py b/gemgis/vector.py index b7dcb612..30ac1f27 100644 --- a/gemgis/vector.py +++ b/gemgis/vector.py @@ -2639,24 +2639,26 @@ def explode_linestring( def explode_linestring_to_elements( linestring: shapely.geometry.linestring.LineString, ) -> List[shapely.geometry.linestring.LineString]: - """Separating a LineString into its single elements and returning a list of LineStrings representing these elements, - also works for LineStrings with Z components + """Separate a LineString into its single elements and returning a list of LineStrings representing these elements, + also works for LineStrings with Z components. Parameters __________ - linestring : linestring: shapely.geometry.linestring.LineString + linestring : shapely.geometry.linestring.LineString Shapely LineString containing more than two vertices, - e.g. ``linestring = LineString([(0, 0), (10, 10), (20, 20)])`` + e.g. ``linestring = LineString([(0, 0), (10, 10), (20, 20)])``. Returns _______ splitted_linestrings : List[shapely.geometry.linestring.LineString] - List containing the separate elements of the original LineString stored as LineStrings + List containing the separate elements of the original LineString stored as LineStrings. .. versionadded:: 1.0.x + .. versionchanged:: 1.2 + Example _______ @@ -2684,10 +2686,9 @@ def explode_linestring_to_elements( See Also ________ - explode_linestring : Exploding a LineString into its single vertices + explode_linestring : Explode a LineString into its single vertices """ - # Checking that the LineString is a Shapely LineString if not isinstance(linestring, shapely.geometry.linestring.LineString): raise TypeError("Input geometry must be a Shapely LineString") From 085716b2c8219418b9331681d47f342ec8e05b60 Mon Sep 17 00:00:00 2001 From: AlexanderJuestel Date: Sun, 28 Jul 2024 12:34:26 +0200 Subject: [PATCH 36/63] Format explode_multilinestring --- gemgis/vector.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/gemgis/vector.py b/gemgis/vector.py index 30ac1f27..f6f80c76 100644 --- a/gemgis/vector.py +++ b/gemgis/vector.py @@ -2719,23 +2719,25 @@ def explode_linestring_to_elements( def explode_multilinestring( multilinestring: shapely.geometry.multilinestring.MultiLineString, ) -> List[shapely.geometry.linestring.LineString]: - """Exploding a MultiLineString into a list of LineStrings + """Explode a MultiLineString into a list of LineStrings. Parameters __________ multilinestring : shapely.geometry.multilinestring.MultiLineString Shapely MultiLineString consisting of multiple LineStrings, - e.g. ``multilinestring = MultiLineString([((0, 0), (1, 1)), ((-1, 0), (1, 0))])`` + e.g. ``multilinestring = MultiLineString([((0, 0), (1, 1)), ((-1, 0), (1, 0))])``. Returns _______ splitted_multilinestring : List[shapely.geometry.linestring.LineString] - List of Shapely LineStrings + List of Shapely LineStrings. .. versionadded:: 1.0.x + .. versionchanged:: 1.2 + Example _______ @@ -2763,11 +2765,10 @@ def explode_multilinestring( See Also ________ - explode_multilinestrings : Exploding a GeoDataFrame containing MultiLineStrings into a GeoDataFrame containing + explode_multilinestrings : Explode a GeoDataFrame containing MultiLineStrings into a GeoDataFrame containing LineStrings only """ - # Checking that the MultiLineString is a Shapely MultiLineString if not isinstance( multilinestring, shapely.geometry.multilinestring.MultiLineString From cb10bae975973a7dce13dd15b1a84be8e020dd71 Mon Sep 17 00:00:00 2001 From: AlexanderJuestel Date: Sun, 28 Jul 2024 12:42:10 +0200 Subject: [PATCH 37/63] Format explode_multilinestrings --- gemgis/vector.py | 73 +++++++++++++++++++++++++++++++++++------------- 1 file changed, 54 insertions(+), 19 deletions(-) diff --git a/gemgis/vector.py b/gemgis/vector.py index f6f80c76..7ab0d9c6 100644 --- a/gemgis/vector.py +++ b/gemgis/vector.py @@ -2799,34 +2799,56 @@ def explode_multilinestrings( drop_level0: bool = True, drop_level1: bool = True, ) -> gpd.geodataframe.GeoDataFrame: - """Exploding Shapely MultiLineStrings stored in a GeoDataFrame to Shapely LineStrings + """Explode Shapely MultiLineStrings stored in a GeoDataFrame to Shapely LineStrings. Parameters ---------- gdf : gpd.geodataframe.GeoDataFrame - GeoDataFrame created from vector data containing elements of geom_type MultiLineString + GeoDataFrame created from vector data containing elements of ``geom_type`` MultiLineString. - reset_index : bool + +----+----------------------------------------+ + | | geometry | + +----+----------------------------------------+ + | 0 | MULTILINESTRING ((0.0 0.0, 1.0 1.0)) | + +----+----------------------------------------+ + | 1 | MULTILINESTRING ((0.0 0.0, 1.0 1.0)) | + +----+----------------------------------------+ + + reset_index : bool, default: ``True`` Variable to reset the index of the resulting GeoDataFrame. - Options include: ``True`` or ``False``, default set to ``True`` + Options include: ``True`` or ``False``, default set to ``True``. - drop_level0 : bool + drop_level0 : bool, default: ``True`` Variable to drop the level_0 column. - Options include: ``True`` or ``False``, default set to ``True`` + Options include: ``True`` or ``False``, default set to ``True``. - drop_level1 : bool + drop_level1 : bool, default: ``True`` Variable to drop the level_1 column. - Options include: ``True`` or ``False``, default set to ``True`` + Options include: ``True`` or ``False``, default set to ``True``. Returns ------- gdf : gpd.geodataframe.GeoDataFrame - GeoDataFrame containing LineStrings + GeoDataFrame containing LineStrings. + + +----+------------------------------+ + | ID | geometry | + +----+------------------------------+ + | 0 | LINESTRING (0.0 0.0, 1.0 1.0)| + +----+------------------------------+ + | 1 | LINESTRING (-1.0 0.0, 1.0 0.0)| + +----+------------------------------+ + | 2 | LINESTRING (0.0 0.0, 1.0 1.0)| + +----+------------------------------+ + | 3 | LINESTRING (-1.0 0.0, 1.0 0.0)| + +----+------------------------------+ .. versionadded:: 1.0.x + .. versionchanged:: 1.2 + Example _______ @@ -2835,26 +2857,39 @@ def explode_multilinestrings( >>> import geopandas as gpd >>> gdf = gpd.read_file(filename='file.shp') >>> gdf - geometry - 0 MULTILINESTRING ((0.0 0.0, 1.0 1.0)) - 1 MULTILINESTRING ((0.0 0.0, 1.0 1.0)) + + +----+----------------------------------------+ + | | geometry | + +----+----------------------------------------+ + | 0 | MULTILINESTRING ((0.0 0.0, 1.0 1.0)) | + +----+----------------------------------------+ + | 1 | MULTILINESTRING ((0.0 0.0, 1.0 1.0)) | + +----+----------------------------------------+ + >>> # Exploding MultiLineStrings into single LineStrings >>> gdf_linestrings = gg.vector.explode_multilinestrings(gdf=gdf, reset_index=True) >>> gdf_linestrings - geometry - 0 LINESTRING (0.0 0.0, 1.0 1.0) - 1 LINESTRING (-1.0 0.0, 1.0 0.0) - 2 LINESTRING (0.0 0.0, 1.0 1.0) - 3 LINESTRING (-1.0 0.0, 1.0 0.0) + + +----+------------------------------+ + | ID | geometry | + +----+------------------------------+ + | 0 | LINESTRING (0.0 0.0, 1.0 1.0)| + +----+------------------------------+ + | 1 | LINESTRING (-1.0 0.0, 1.0 0.0)| + +----+------------------------------+ + | 2 | LINESTRING (0.0 0.0, 1.0 1.0)| + +----+------------------------------+ + | 3 | LINESTRING (-1.0 0.0, 1.0 0.0)| + +----+------------------------------+ + See Also ________ - explode_multilinestring : Exploding a MultiLineString into a list of single LineStrings + explode_multilinestring : Explode a MultiLineString into a list of single LineStrings """ - # Checking that gdf is of type GepDataFrame if not isinstance(gdf, gpd.geodataframe.GeoDataFrame): raise TypeError("Loaded object is not a GeoDataFrame") From 538e53844e1800f9819952373ea9fdb217bc9010 Mon Sep 17 00:00:00 2001 From: AlexanderJuestel Date: Sun, 28 Jul 2024 12:51:46 +0200 Subject: [PATCH 38/63] Format explode_polygon --- gemgis/vector.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/gemgis/vector.py b/gemgis/vector.py index 7ab0d9c6..9bf1b281 100644 --- a/gemgis/vector.py +++ b/gemgis/vector.py @@ -2941,22 +2941,24 @@ def explode_multilinestrings( def explode_polygon( polygon: shapely.geometry.polygon.Polygon, ) -> List[shapely.geometry.point.Point]: - """Exploding Shapely Polygon to list of Points + """Explode Shapely Polygon to list of Points. Parameters __________ polygon : shapely.geometry.polygon.Polygon - Shapely Polygon from which vertices are extracted, e.g. ``polygon = Polygon([(0, 0), (1, 1), (1, 0)])`` + Shapely Polygon from which vertices are extracted, e.g. ``polygon = Polygon([(0, 0), (1, 1), (1, 0)])``. Returns _______ point_list : List[shapely.geometry.point.Point] - List containing the vertices of a polygon as Shapely Points + List containing the vertices of a polygon as Shapely Points. .. versionadded:: 1.0.x + .. versionchanged:: 1.2 + Example _______ @@ -2986,10 +2988,9 @@ def explode_polygon( See Also ________ - explode_polygons : Exploding a GeoDataFrame containing Polygons into a GeoDataFrame containing LineStrings + explode_polygons : Explode a GeoDataFrame containing Polygons into a GeoDataFrame containing LineStrings """ - # Checking that the input polygon is a Shapely object if not isinstance(polygon, shapely.geometry.polygon.Polygon): raise TypeError("Polygon must be a Shapely Polygon") From e5713f3b2ff4791b0fb8c417c13c40f463dbb044 Mon Sep 17 00:00:00 2001 From: AlexanderJuestel Date: Sun, 28 Jul 2024 12:56:36 +0200 Subject: [PATCH 39/63] Format explode_polygons --- gemgis/vector.py | 49 +++++++++++++++++++++++++++++++++++++----------- 1 file changed, 38 insertions(+), 11 deletions(-) diff --git a/gemgis/vector.py b/gemgis/vector.py index 9bf1b281..dcbf3c47 100644 --- a/gemgis/vector.py +++ b/gemgis/vector.py @@ -3011,22 +3011,40 @@ def explode_polygon( def explode_polygons( gdf: gpd.geodataframe.GeoDataFrame, ) -> gpd.geodataframe.GeoDataFrame: - """Converting a GeoDataFrame containing elements of geom_type Polygons to a GeoDataFrame with LineStrings + """Convert a GeoDataFrame containing elements of ``geom_type`` Polygon to a GeoDataFrame with LineStrings. Parameters ___________ gdf : gpd.geodataframe.GeoDataFrame - GeoDataFrame created from vector data containing elements of geom_type Polygon + GeoDataFrame created from vector data containing elements of ``geom_type`` Polygon. + + +----+------------------------------------------------+ + | | geometry | + +----+------------------------------------------------+ + | 0 | POLYGON ((0.0 0.0, 1.0 1.0, 1.0 0.0, 0.0 0.0)) | + +----+------------------------------------------------+ + | 1 | POLYGON ((0.0 0.0, 1.0 1.0, 1.0 0.0, 0.0 0.0)) | + +----+------------------------------------------------+ Returns _______ gdf_linestrings : gpd.geodataframe.GeoDataFrame - GeoDataFrame containing elements of type MultiLineString and LineString + GeoDataFrame containing elements of type MultiLineString and LineString. + + +----+-------------------------------------------------+ + | | geometry | + +----+-------------------------------------------------+ + | 0 | LINESTRING (0.0 0.0, 1.0 1.0, 1.0 0.0, 0.0 0.0) | + +----+-------------------------------------------------+ + | 1 | LINESTRING (0.0 0.0, 1.0 1.0, 1.0 0.0, 0.0 0.0) | + +----+-------------------------------------------------+ .. versionadded:: 1.0.x + .. versionchanged:: 1.2 + Example _______ @@ -3035,25 +3053,34 @@ def explode_polygons( >>> import geopandas as gpd >>> gdf = gpd.read_file(filename='file.shp') >>> gdf - geometry - 0 POLYGON ((0.0 0.0, 1.0 1.0, 1.0 0.0, 0.0 0.0)) - 1 POLYGON ((0.0 0.0, 1.0 1.0, 1.0 0.0, 0.0 0.0)) + + +----+------------------------------------------------+ + | | geometry | + +----+------------------------------------------------+ + | 0 | POLYGON ((0.0 0.0, 1.0 1.0, 1.0 0.0, 0.0 0.0)) | + +----+------------------------------------------------+ + | 1 | POLYGON ((0.0 0.0, 1.0 1.0, 1.0 0.0, 0.0 0.0)) | + +----+------------------------------------------------+ + >>> # Exploding Polygons into LineStrings >>> gdf_exploded = gg.vector.explode_polygons(gdf=gdf) >>> gdf_exploded - geometry - 0 LINESTRING (0.0 0.0, 1.0 1.0, 1.0 0.0, 0.0 0.0) - 1 LINESTRING (0.0 0.0, 1.0 1.0, 1.0 0.0, 0.0 0.0) + +----+-------------------------------------------------+ + | | geometry | + +----+-------------------------------------------------+ + | 0 | LINESTRING (0.0 0.0, 1.0 1.0, 1.0 0.0, 0.0 0.0) | + +----+-------------------------------------------------+ + | 1 | LINESTRING (0.0 0.0, 1.0 1.0, 1.0 0.0, 0.0 0.0) | + +----+-------------------------------------------------+ See Also ________ - explode_polygon : Exploding a Polygon into single Points + explode_polygon : Explod a Polygon into single Points """ - # Checking that the input is a GeoDataFrame: if not isinstance(gdf, gpd.geodataframe.GeoDataFrame): raise TypeError("gdf must be a GeoDataFrame") From e73687f22c1b3db436dd20d5910fbc88d0f6ec84 Mon Sep 17 00:00:00 2001 From: AlexanderJuestel Date: Sun, 28 Jul 2024 12:58:03 +0200 Subject: [PATCH 40/63] Format explode_geometry_collection --- gemgis/vector.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/gemgis/vector.py b/gemgis/vector.py index dcbf3c47..37f51e2b 100644 --- a/gemgis/vector.py +++ b/gemgis/vector.py @@ -3108,22 +3108,24 @@ def explode_polygons( def explode_geometry_collection( collection: shapely.geometry.collection.GeometryCollection, ) -> List[shapely.geometry.base.BaseGeometry]: - """Exploding a Shapely Geometry Collection to a List of Base Geometries + """Explode a Shapely Geometry Collection to a List of Base Geometries. Parameters __________ collection : shapely.geometry.collection.GeometryCollection - Shapely Geometry Collection consisting of different Base Geometries + Shapely Geometry Collection consisting of different Base Geometries. Returns _______ collection_exploded : List[shapely.geometry.base.BaseGeometry] - List of Base Geometries from the original Geometry Collection + List of Base Geometries from the original Geometry Collection. .. versionadded:: 1.0.x + .. versionchanged:: 1.2 + Example _______ @@ -3153,10 +3155,9 @@ def explode_geometry_collection( See Also ________ - explode_geometry_collections : Exploding a GeoDataFrame containing different Base Geometries + explode_geometry_collections : Explode a GeoDataFrame containing different Base Geometries """ - # Checking that the Geometry Collection is a Shapely Geometry Collection if not isinstance(collection, shapely.geometry.collection.GeometryCollection): raise TypeError("Geometry Collection must be a Shapely Geometry Collection") From 75e4a11c7a14ac4018ce151bd03e1b8e2649df0b Mon Sep 17 00:00:00 2001 From: AlexanderJuestel Date: Sun, 28 Jul 2024 13:51:51 +0200 Subject: [PATCH 41/63] Format explode_geometry_collections --- gemgis/vector.py | 96 ++++++++++++++++++++++++++++++++++-------------- 1 file changed, 68 insertions(+), 28 deletions(-) diff --git a/gemgis/vector.py b/gemgis/vector.py index 37f51e2b..e789be85 100644 --- a/gemgis/vector.py +++ b/gemgis/vector.py @@ -3183,38 +3183,64 @@ def explode_geometry_collections( drop_level1: bool = True, remove_points: bool = True, ) -> gpd.geodataframe.GeoDataFrame: - """Exploding Shapely Geometry Collections stored in GeoDataFrames to different Shapely Base Geometries + """Explode Shapely Geometry Collections stored in a GeoDataFrame to different Shapely Base Geometries. Parameters ---------- gdf : gpd.geodataframe.GeoDataFrame - GeoDataFrame created from vector data containing elements of geom_type GeometryCollection - - reset_index : bool - Variable to reset the index of the resulting GeoDataFrame. - Options include: ``True`` or ``False``, default set to ``True`` + GeoDataFrame created from vector data containing elements of geom_type GeometryCollection. + + +----+--------------------------------------------------------------+ + | | geometry | + +----+--------------------------------------------------------------+ + | 0 | LINESTRING (0.00000 0.00000, 1.00000 1.00000, ...) | + +----+--------------------------------------------------------------+ + | 1 | LINESTRING (0.00000 0.00000, 1.00000 1.00000, ...) | + +----+--------------------------------------------------------------+ + | 2 | GEOMETRYCOLLECTION (POINT (2.00000 2.00000), LINESTRING ...) | + +----+--------------------------------------------------------------+ + | 3 | POLYGON ((0.00000 0.00000, 10.00000 0.00000, 1...) | + +----+--------------------------------------------------------------+ + + reset_index : bool, default: ``True``> + Variable to reset the index of the resulting GeoDataFrame, e.g. ``reset_index=True``. + Options include: ``True`` or ``False``, default set to ``True``. - drop_level0 : bool - Variable to drop the level_0 column. - Options include: ``True`` or ``False``, default set to ``True`` + drop_level0 : bool, default: ``True`` + Variable to drop the level_0 column, e.g. ``drop_level0=True``. + Options include: ``True`` or ``False``, default set to ``True``. - drop_level1 : bool - Variable to drop the level_1 column. - Options include: ``True`` or ``False``, default set to ``True`` + drop_level1 : bool, default: ``True`` + Variable to drop the level_1 column, e.g. ``drop_level1=True``. + Options include: ``True`` or ``False``, default set to ``True``. - remove_points : bool - Variable to remove points from exploded GeoDataFrame. - Options include: ``True`` or ``False``, default set to ``True`` + remove_points : bool, default: ``True`` + Variable to remove points from exploded GeoDataFrame, e.g. ``remove_points=True``. + Options include: ``True`` or ``False``, default set to ``True``. Returns ------- gdf : gpd.geodataframe.GeoDataFrame - GeoDataFrame containing different geometry types + GeoDataFrame containing different geometry types. + + +----+----------------------------------------------------+ + | | geometry | + +----+----------------------------------------------------+ + | 0 | LINESTRING (0.00000 0.00000, 1.00000 1.00000, ...) | + +----+----------------------------------------------------+ + | 1 | LINESTRING (0.00000 0.00000, 1.00000 1.00000, ...) | + +----+----------------------------------------------------+ + | 2 | LINESTRING (0.00000 0.00000, 1.00000 1.00000) | + +----+----------------------------------------------------+ + | 3 | POLYGON ((0.00000 0.00000, 10.00000 0.00000, 1...) | + +----+----------------------------------------------------+ .. versionadded:: 1.0.x + .. versionchanged:: 1.2 + Example _______ @@ -3230,28 +3256,42 @@ def explode_geometry_collections( >>> # Creating GeoDataFrame from Base Geometries >>> gdf = gpd.GeoDataFrame(geometry=[a, b, collection, polygon]) >>> gdf - geometry - 0 LINESTRING (0.00000 0.00000, 1.00000 1.00000, ... - 1 LINESTRING (0.00000 0.00000, 1.00000 1.00000, ... - 2 GEOMETRYCOLLECTION (POINT (2.00000 2.00000), L... - 3 POLYGON ((0.00000 0.00000, 10.00000 0.00000, 1.. + + +----+--------------------------------------------------------------+ + | | geometry | + +----+--------------------------------------------------------------+ + | 0 | LINESTRING (0.00000 0.00000, 1.00000 1.00000, ...) | + +----+--------------------------------------------------------------+ + | 1 | LINESTRING (0.00000 0.00000, 1.00000 1.00000, ...) | + +----+--------------------------------------------------------------+ + | 2 | GEOMETRYCOLLECTION (POINT (2.00000 2.00000), LINESTRING ...) | + +----+--------------------------------------------------------------+ + | 3 | POLYGON ((0.00000 0.00000, 10.00000 0.00000, 1...) | + +----+--------------------------------------------------------------+ + >>> # Explode Geometry Collection into single Base Geometries >>> gdf_exploded = gg.vector.explode_geometry_collections(gdf=gdf) >>> gdf_exploded - geometry - 0 LINESTRING (0.00000 0.00000, 1.00000 1.00000, ... - 1 LINESTRING (0.00000 0.00000, 1.00000 1.00000, ... - 2 LINESTRING (0.00000 0.00000, 1.00000 1.00000) - 3 POLYGON ((0.00000 0.00000, 10.00000 0.00000, 1... + + +----+----------------------------------------------------+ + | | geometry | + +----+----------------------------------------------------+ + | 0 | LINESTRING (0.00000 0.00000, 1.00000 1.00000, ...) | + +----+----------------------------------------------------+ + | 1 | LINESTRING (0.00000 0.00000, 1.00000 1.00000, ...) | + +----+----------------------------------------------------+ + | 2 | LINESTRING (0.00000 0.00000, 1.00000 1.00000) | + +----+----------------------------------------------------+ + | 3 | POLYGON ((0.00000 0.00000, 10.00000 0.00000, 1...) | + +----+----------------------------------------------------+ See Also ________ - explode_geometry_collection : Exploding a Shapely Geometry Collection Object into a list of Base Geometries + explode_geometry_collection : Explod a Shapely Geometry Collection Object into a list of Base Geometries """ - # Checking that gdf is of type GepDataFrame if not isinstance(gdf, gpd.geodataframe.GeoDataFrame): raise TypeError("Loaded object is not a GeoDataFrame") From afedf6bbd37d4cc19867f865a3f953eb928c503c Mon Sep 17 00:00:00 2001 From: AlexanderJuestel Date: Sun, 28 Jul 2024 13:58:42 +0200 Subject: [PATCH 42/63] Format create_linestring_from_xyz_points --- gemgis/vector.py | 45 +++++++++++++++++++++++++++++++++++---------- 1 file changed, 35 insertions(+), 10 deletions(-) diff --git a/gemgis/vector.py b/gemgis/vector.py index e789be85..305e47e7 100644 --- a/gemgis/vector.py +++ b/gemgis/vector.py @@ -3354,22 +3354,34 @@ def create_linestring_from_xyz_points( zcol: str = "Z", drop_nan: bool = True, ) -> shapely.geometry.linestring.LineString: - """ - Create LineString from an array or GeoDataFrame containing X, Y, and Z coordinates of points. + """Create LineString from an array or GeoDataFrame containing X, Y, and Z coordinates of points. Parameters __________ points : Union[np.ndarray, gpd.geodataframe.GeoDataFrame] NumPy Array or GeoDataFrame containing XYZ points. - nodata : Union[int, float]) + + +----+-------+-------+-------+ + | | X | Y | Z | + +----+-------+-------+-------+ + | 0 | 3.23 | 5.69 | 2.03 | + +----+-------+-------+-------+ + | 1 | 3.24 | 5.68 | 2.02 | + +----+-------+-------+-------+ + | 2 | 3.25 | 5.67 | 1.97 | + +----+-------+-------+-------+ + | 3 | 3.26 | 5.66 | 1.95 | + +----+-------+-------+-------+ + + nodata : Union[int, float]), default: ``9999.0`` Nodata value to filter out points outside a designated area, e.g. ``nodata=9999.0``, default is ``9999.0``. - xcol : str + xcol : str, default: ``'X'`` Name of the X column in the dataset, e.g. ``xcol='X'``, default is ``'X'``. - ycol : str + ycol : str, default: ``'Y'`` Name of the Y column in the dataset, e.g. ``ycol='Y'``, default is ``'Y'``. - zcol : str + zcol : str, default: ``'Z'`` Name of the Z column in the dataset, e.g. ``zcol='Z'``, default is ``'Z'``. - drop_nan : bool + drop_nan : bool, default: ``True`` Boolean argument to drop points that contain a ``nan`` value as Z value. Options include ``True`` and ``False``, default is ``True``. @@ -3377,12 +3389,11 @@ def create_linestring_from_xyz_points( _______ line : shapely.geometry.linestring.LineString - LineString Z constructed from provided point values + LineString Z constructed from provided point values. .. versionadded:: 1.0.x - .. versionchanged:: 1.1 - Adding argument `drop_nan` and code to drop coordinates that contain ``nan`` values as Z coordinates. + .. versionchanged:: 1.2 Example _______ @@ -3391,6 +3402,20 @@ def create_linestring_from_xyz_points( >>> import gemgis as gg >>> import numpy as np >>> points = np.array([[3.23, 5.69, 2.03],[3.24, 5.68, 2.02],[3.25, 5.67, 1.97],[3.26, 5.66, 1.95]]) + >>> points + + +----+-------+-------+-------+ + | | X | Y | Z | + +----+-------+-------+-------+ + | 0 | 3.23 | 5.69 | 2.03 | + +----+-------+-------+-------+ + | 1 | 3.24 | 5.68 | 2.02 | + +----+-------+-------+-------+ + | 2 | 3.25 | 5.67 | 1.97 | + +----+-------+-------+-------+ + | 3 | 3.26 | 5.66 | 1.95 | + +----+-------+-------+-------+ + >>> # Creating LineStrings from points >>> linestring = gg.vector.create_linestring_from_xyz_points(points=points) From 53cbfbe107e564e33cc169d4dc57a7b700e56d47 Mon Sep 17 00:00:00 2001 From: AlexanderJuestel Date: Sun, 28 Jul 2024 14:19:48 +0200 Subject: [PATCH 43/63] Format create_linestrings_from_xyz_points --- gemgis/vector.py | 93 +++++++++++++++++++++++++++++++++++------------- 1 file changed, 68 insertions(+), 25 deletions(-) diff --git a/gemgis/vector.py b/gemgis/vector.py index 305e47e7..49e37b69 100644 --- a/gemgis/vector.py +++ b/gemgis/vector.py @@ -3421,6 +3421,12 @@ def create_linestring_from_xyz_points( >>> linestring = gg.vector.create_linestring_from_xyz_points(points=points) >>> linestring.wkt 'LINESTRING Z (3.23 5.69 2.03, 3.24 5.68 2.02, 3.25 5.67 1.97, 3.26 5.66 1.95)' + + See Also + ________ + + create_linestrings_from_xyz_points : Create LineStrings from a GeoDataFrame containing X, Y, and Z coordinates of vertices of multiple LineStrings + """ # Checking that the points are of type GeoDataFrame or a NumPy array if not isinstance(points, (np.ndarray, gpd.geodataframe.GeoDataFrame)): @@ -3489,44 +3495,59 @@ def create_linestrings_from_xyz_points( return_gdf: bool = True, drop_nan: bool = True, ) -> Union[List[shapely.geometry.linestring.LineString], gpd.geodataframe.GeoDataFrame]: - """Creating LineStrings from a GeoDataFrame containing X, Y, and Z coordinates of vertices of multiple LineStrings + """Create LineStrings from a GeoDataFrame containing X, Y, and Z coordinates of vertices of multiple LineStrings. Parameters __________ gdf : gpd.geodataframe.GeoDataFrame - GeoDataFrame containing extracted X, Y, and Z coordinates of LineStrings + GeoDataFrame containing extracted X, Y, and Z coordinates of LineStrings. + + +----+-----------+------------------------+-------+-------+-------+ + | ID | Object_ID | geometry | X | Y | Z | + +----+-----------+------------------------+-------+-------+-------+ + | 0 | 1 | POINT (19.150 293.313) | 19.15 | 293.31| 364.99| + +----+-----------+------------------------+-------+-------+-------+ + | 1 | 1 | POINT (61.934 381.459) | 61.93 | 381.46| 400.34| + +----+-----------+------------------------+-------+-------+-------+ + | 2 | 1 | POINT (109.358 480.946)| 109.36| 480.95| 459.55| + +----+-----------+------------------------+-------+-------+-------+ + | 3 | 2 | POINT (157.812 615.999)| 157.81| 616.00| 525.69| + +----+-----------+------------------------+-------+-------+-------+ + | 4 | 2 | POINT (191.318 719.094)| 191.32| 719.09| 597.63| + +----+-----------+------------------------+-------+-------+-------+ + groupby : str - Name of a unique identifier the LineStrings can be separated from each other, e.g. ``groupby='Object_ID'`` + Name of a unique identifier the LineStrings can be separated from each other, e.g. ``groupby='Object_ID'``. - nodata : Union[int, float]) - Nodata value to filter out points outside a designated area, e.g. ``nodata=9999.0``, default is ``9999.0`` + nodata : Union[int, float]), default: ``9999.0`` + Nodata value to filter out points outside a designated area, e.g. ``nodata=9999.0``, default is ``9999.0``. - xcol : str - Name of the X column in the dataset, e.g. ``xcol='X'``, default is ``'X'`` + xcol : str, default: ``'X'`` + Name of the X column in the dataset, e.g. ``xcol='X'``, default is ``'X'``. - ycol : str - Name of the Y column in the dataset, e.g. ``ycol='Y'``, default is ``'Y'`` + ycol : str, default: ``'Y'`` + Name of the Y column in the dataset, e.g. ``ycol='Y'``, default is ``'Y'``. - zcol : str - Name of the Z column in the dataset, e.g. ``zcol='Z'``, default is ``'Z'`` + zcol : str, default: ``'Z'`` + Name of the Z column in the dataset, e.g. ``zcol='Z'``, default is ``'Z'``. - dem : Union[np.ndarray, rasterio.io.DatasetReader] + dem : Union[np.ndarray, rasterio.io.DatasetReader], default: ``None`` NumPy ndarray or rasterio object containing the height values, default value is ``None`` in case geometries - contain Z values + contain Z values. - extent : List[Union[float, int]] + extent : List[Union[float, int]], default: ``None`` Values for minx, maxx, miny and maxy values to define the boundaries of the raster, - e.g. ``extent=[0, 972, 0, 1069]`` + e.g. ``extent=[0, 972, 0, 1069]``, default is ``None``. - return_gdf : bool - Variable to either return the data as GeoDataFrame or as list of LineStrings. - Options include: ``True`` or ``False``, default set to ``True`` + return_gdf : bool, default: ``True`` + Variable to either return the data as GeoDataFrame or as list of LineStrings, e.g. ``return_gdf=True``. + Options include: ``True`` or ``False``, default set to ``True``. - drop_nan : bool - Boolean argument to drop points that contain a ``nan`` value as Z value. Options include ``True`` and - ``False``, default is ``True`` + drop_nan : bool, default: ``True`` + Boolean argument to drop points that contain a ``nan`` value as Z value, e.g. ``drop_nan=True`` + Options include ``True`` and ``False``, default is ``True``. Returns _______ @@ -3536,9 +3557,7 @@ def create_linestrings_from_xyz_points( .. versionadded:: 1.0.x - .. versionchanged:: 1.1 - Removed manual dropping of additional columns. Now automatically drops unnecessary coloumns. - Adding argument `drop_nan` and code to drop coordinates that contain ``nan`` values as Z coordinates. + .. versionchanged:: 1.2 Example _______ @@ -3549,14 +3568,38 @@ def create_linestrings_from_xyz_points( >>> gdf = gpd.read_file(filename='file.shp') >>> gdf + +----+-----------+------------------------+-------+-------+-------+ + | | Object_ID | geometry | X | Y | Z | + +----+-----------+------------------------+-------+-------+-------+ + | 0 | 1 | POINT (19.150 293.313) | 19.15 | 293.31| 364.99| + +----+-----------+------------------------+-------+-------+-------+ + | 1 | 1 | POINT (61.934 381.459) | 61.93 | 381.46| 400.34| + +----+-----------+------------------------+-------+-------+-------+ + | 2 | 1 | POINT (109.358 480.946)| 109.36| 480.95| 459.55| + +----+-----------+------------------------+-------+-------+-------+ + | 3 | 2 | POINT (157.812 615.999)| 157.81| 616.00| 525.69| + +----+-----------+------------------------+-------+-------+-------+ + | 4 | 2 | POINT (191.318 719.094)| 191.32| 719.09| 597.63| + +----+-----------+------------------------+-------+-------+-------+ + >>> # Creating LineStrings with Z component from gdf >>> gdf_linestring = gg.vector.create_linestrings_from_xyz_points(gdf=gdf, groupby='ABS') >>> gdf_linestring + +----+-----------+----------------------------------------------------------------------+ + | | formation | geometry | + +----+-----------+----------------------------------------------------------------------+ + | 0 | 1 | LINESTRING Z (19.150 293.310 364.990, 61.930 381.459 400.340, ...) | + +----+-----------+----------------------------------------------------------------------+ + | 1 | 2 | LINESTRING Z (157.810 616.000 525.690, 191.320 719.094 597.630, ...) | + +----+-----------+----------------------------------------------------------------------+ - """ + See Also + ________ + create_linestring_from_xyz_points : Create LineString from an array or GeoDataFrame containing X, Y, and Z coordinates of points. + """ # Checking that the input is a GeoDataFrame if not isinstance(gdf, gpd.geodataframe.GeoDataFrame): raise TypeError("Input must be provided as GeoDataFrame") From 021a2b14c2c43c5f1662effa409a5670f89e153d Mon Sep 17 00:00:00 2001 From: AlexanderJuestel Date: Tue, 30 Jul 2024 09:07:29 +0200 Subject: [PATCH 44/63] Format create_linestrings_from_contours --- gemgis/vector.py | 89 ++++++++++++++++++++++++++++++++++-------------- 1 file changed, 63 insertions(+), 26 deletions(-) diff --git a/gemgis/vector.py b/gemgis/vector.py index 49e37b69..1d89ce83 100644 --- a/gemgis/vector.py +++ b/gemgis/vector.py @@ -3686,29 +3686,51 @@ def create_linestrings_from_contours( return_gdf: bool = True, crs: Union[str, pyproj.crs.crs.CRS] = None, ) -> Union[List[shapely.geometry.linestring.LineString], gpd.geodataframe.GeoDataFrame]: - """Creating LineStrings from PyVista Contour Lines and save them as list or GeoDataFrame + """Create LineStrings from PyVista Contour Lines and save them as list or GeoDataFrame. Parameters __________ contours : pv.core.pointset.PolyData - PyVista PolyData dataset containing contour lines extracted from a mesh + PyVista PolyData dataset containing contour lines extracted from a mesh. + + +--------------+-----------------------+-------------+---------+---------+--------+------------+-----------+ + | Header | | Data Array | | | | | | + +--------------+-----------------------+-------------+---------+---------+--------+------------+-----------+ + | PolyData | Information | Name | Field | Type | N Comp | Min | Max | + +--------------+-----------------------+-------------+---------+---------+--------+------------+-----------+ + | N Cells | 580 | Depth [m] | Points | float64 | 1 | -1.710e+03 | 3.000e+02 | + +--------------+-----------------------+-------------+---------+---------+--------+------------+-----------+ + | N Points | 586 | | | | | | | + +--------------+-----------------------+-------------+---------+---------+--------+------------+-----------+ + | N Strips | 0 | | | | | | | + +--------------+-----------------------+-------------+---------+---------+--------+------------+-----------+ + | X Bounds | 2.952e+05, 3.016e+05 | | | | | | | + +--------------+-----------------------+-------------+---------+---------+--------+------------+-----------+ + | Y Bounds | 5.619e+06, 5.627e+06 | | | | | | | + +--------------+-----------------------+-------------+---------+---------+--------+------------+-----------+ + | Z Bounds | -1.710e+03, 3.000e+02 | | | | | | | + +--------------+-----------------------+-------------+---------+---------+--------+------------+-----------+ + | N Arrays | 1 | | | | | | | + +--------------+-----------------------+-------------+---------+---------+--------+------------+-----------+ - return_gdf : bool - Variable to create GeoDataFrame of the created list of Shapely Objects. - Options include: ``True`` or ``False``, default set to ``True`` + return_gdf : bool, default: ``True`` + Variable to create GeoDataFrame of the created list of Shapely Objects, e.g. ``return_gdf=True``. + Options include: ``True`` or ``False``, default set to ``True``. - crs : Union[str, pyproj.crs.crs.CRS] - Name of the CRS provided to reproject coordinates of the GeoDataFrame, e.g. ``crs='EPSG:4647'`` + crs : Union[str, pyproj.crs.crs.CRS], default: ``None`` + Name of the CRS provided to reproject coordinates of the GeoDataFrame, e.g. ``crs='EPSG:4647'``. Returns _______ linestrings : Union[List[shapely.geometry.linestring.LineString], gpd.geodataframe.GeoDataFrame] - List of LineStrings or GeoDataFrame containing the contours that were converted + List of LineStrings or GeoDataFrame containing the contours that were converted. .. versionadded:: 1.0.x + .. versionchanged:: 1.2 + Example _______ @@ -3717,30 +3739,45 @@ def create_linestrings_from_contours( >>> import pyvista as pv >>> contours = pv.read('file.vtk') >>> contours - Header - PolyData Information - N Cells 36337 - N Points 36178 - X Bounds 3.233e+07, 3.250e+07 - Y Bounds 5.704e+06, 5.798e+06 - Z Bounds -2.400e+03, 3.500e+02 - N Arrays 1 - Data Arrays - Name Field Type N Comp Min Max - Depth [m] Points float64 1 -2.400e+03 3.500e+02 + +--------------+-----------------------+-------------+---------+---------+--------+------------+-----------+ + | Header | | Data Array | | | | | | + +--------------+-----------------------+-------------+---------+---------+--------+------------+-----------+ + | PolyData | Information | Name | Field | Type | N Comp | Min | Max | + +--------------+-----------------------+-------------+---------+---------+--------+------------+-----------+ + | N Cells | 580 | Depth [m] | Points | float64 | 1 | -1.710e+03 | 3.000e+02 | + +--------------+-----------------------+-------------+---------+---------+--------+------------+-----------+ + | N Points | 586 | | | | | | | + +--------------+-----------------------+-------------+---------+---------+--------+------------+-----------+ + | N Strips | 0 | | | | | | | + +--------------+-----------------------+-------------+---------+---------+--------+------------+-----------+ + | X Bounds | 2.952e+05, 3.016e+05 | | | | | | | + +--------------+-----------------------+-------------+---------+---------+--------+------------+-----------+ + | Y Bounds | 5.619e+06, 5.627e+06 | | | | | | | + +--------------+-----------------------+-------------+---------+---------+--------+------------+-----------+ + | Z Bounds | -1.710e+03, 3.000e+02 | | | | | | | + +--------------+-----------------------+-------------+---------+---------+--------+------------+-----------+ + | N Arrays | 1 | | | | | | | + +--------------+-----------------------+-------------+---------+---------+--------+------------+-----------+ >>> # Extracting LineStrings from contours >>> gdf = gg.vector.create_linestrings_from_contours(contours=contours) >>> gdf - geometry Z - 0 LINESTRING Z (32409587.930 5780538.824 -2350.0... -2350.00 - 1 LINESTRING Z (32407304.336 5777048.086 -2050.0... -2050.00 - 2 LINESTRING Z (32408748.977 5778005.047 -2200.0... -2200.00 - 3 LINESTRING Z (32403693.547 5786613.994 -2400.0... -2400.00 - 4 LINESTRING Z (32404738.664 5782672.480 -2350.0... -2350.00 + +----+----------------------------------------------------+---------+ + | | geometry | Z | + +----+----------------------------------------------------+---------+ + | 0 | LINESTRING Z (32409587.930 5780538.824 -2350.0...) | -2350.00| + +----+----------------------------------------------------+---------+ + | 1 | LINESTRING Z (32407304.336 5777048.086 -2050.0...) | -2050.00| + +----+----------------------------------------------------+---------+ + | 2 | LINESTRING Z (32408748.977 5778005.047 -2200.0...) | -2200.00| + +----+----------------------------------------------------+---------+ + | 3 | LINESTRING Z (32403693.547 5786613.994 -2400.0...) | -2400.00| + +----+----------------------------------------------------+---------+ + | 4 | LINESTRING Z (32404738.664 5782672.480 -2350.0...) | -2350.00| + +----+----------------------------------------------------+---------+ - """ + """ # Checking that the input data is a PyVista PolyData dataset if not isinstance(contours, pv.core.pointset.PolyData): raise TypeError("Input data must be a PyVista PolyData dataset") From 1ff5556a6c9db0728b9eed08f949b1f10a1ccee0 Mon Sep 17 00:00:00 2001 From: AlexanderJuestel Date: Tue, 30 Jul 2024 09:15:16 +0200 Subject: [PATCH 45/63] Format interpolate_raster --- gemgis/vector.py | 92 ++++++++++++++++++++++++++++++++++-------------- 1 file changed, 65 insertions(+), 27 deletions(-) diff --git a/gemgis/vector.py b/gemgis/vector.py index 1d89ce83..d0b266d1 100644 --- a/gemgis/vector.py +++ b/gemgis/vector.py @@ -3727,6 +3727,20 @@ def create_linestrings_from_contours( linestrings : Union[List[shapely.geometry.linestring.LineString], gpd.geodataframe.GeoDataFrame] List of LineStrings or GeoDataFrame containing the contours that were converted. + +----+----------------------------------------------------+---------+ + | | geometry | Z | + +----+----------------------------------------------------+---------+ + | 0 | LINESTRING Z (32409587.930 5780538.824 -2350.0...) | -2350.00| + +----+----------------------------------------------------+---------+ + | 1 | LINESTRING Z (32407304.336 5777048.086 -2050.0...) | -2050.00| + +----+----------------------------------------------------+---------+ + | 2 | LINESTRING Z (32408748.977 5778005.047 -2200.0...) | -2200.00| + +----+----------------------------------------------------+---------+ + | 3 | LINESTRING Z (32403693.547 5786613.994 -2400.0...) | -2400.00| + +----+----------------------------------------------------+---------+ + | 4 | LINESTRING Z (32404738.664 5782672.480 -2350.0...) | -2350.00| + +----+----------------------------------------------------+---------+ + .. versionadded:: 1.0.x .. versionchanged:: 1.2 @@ -3858,36 +3872,50 @@ def interpolate_raster( seed: int = None, **kwargs, ) -> np.ndarray: - """Interpolating a raster/digital elevation model from point or line Shape file + """Interpolate a raster/digital elevation model from Point or LineString Shape file. Parameters __________ gdf : gpd.geodataframe.GeoDataFrame - GeoDataFrame containing vector data of geom_type Point or Line containing the Z values of an area - - value : str - Value to be interpolated, e.g. ``value='Z'``, default is ``'Z'`` - - method : string - Method used to interpolate the raster. - Options include: ``'nearest', 'linear', 'cubic', 'rbf'`` - - res : int - Resolution of the raster in X and Y direction, e.g. ``res=50`` - - seed : int - Seed for the drawing of random numbers, e.g. ``seed=1`` - - n : int - Number of samples used for the interpolation, e.g. ``n=100`` + GeoDataFrame containing vector data of geom_type Point or Line containing the Z values of an area. + + +----+------+---------------------------------------------------------+ + | ID | Z | geometry | + +----+------+---------------------------------------------------------+ + | 0 | None | 400 | LINESTRING (0.741 475.441, 35.629 429.247, 77.... | + +----+------+---------------------------------------------------------+ + | 1 | None | 300 | LINESTRING (645.965 0.525, 685.141 61.866, 724... | + +----+------+---------------------------------------------------------+ + | 2 | None | 400 | LINESTRING (490.292 0.525, 505.756 40.732, 519... | + +----+------+---------------------------------------------------------+ + | 3 | None | 600 | LINESTRING (911.433 1068.585, 908.856 1026.831... | + +----+------+---------------------------------------------------------+ + | 4 | None | 700 | LINESTRING (228.432 1068.585, 239.772 1017.037... | + +----+------+---------------------------------------------------------+ + + value : str, default: ``'Z'`` + Value to be interpolated, e.g. ``value='Z'``, default is ``'Z'``. + + method : string, default: ``'nearest'`` + Method used to interpolate the raster, e.g. ``method='nearest'``. + Options include: ``'nearest', 'linear', 'cubic', 'rbf'``. + + n : int, default: ``None`` + Number of samples used for the interpolation, e.g. ``n=100``, default is None. + + res : int, default: ``1`` + Resolution of the raster in X and Y direction, e.g. ``res=50``, default is ``'1'``. extent : List[Union[float, int]] Values for minx, maxx, miny and maxy values to define the boundaries of the raster, - e.g. ``extent=[0, 972, 0, 1069]`` + e.g. ``extent=[0, 972, 0, 1069]``. + + seed : int, default: ``None`` + Seed for the drawing of random numbers, e.g. ``seed=1``, default is None. **kwargs : optional keyword arguments - For kwargs for rbf and griddata see: https://docs.scipy.org/doc/scipy/reference/interpolate.html + For kwargs for rbf and griddata see: https://docs.scipy.org/doc/scipy/reference/interpolate.html. Returns _______ @@ -3897,6 +3925,8 @@ def interpolate_raster( .. versionadded:: 1.0.x + .. versionchanged:: 1.2 + Example _______ @@ -3905,12 +3935,21 @@ def interpolate_raster( >>> import geopandas as gpd >>> gdf = gpd.read_file(filename='file.shp') >>> gdf - id Z geometry - 0 None 400 LINESTRING (0.741 475.441, 35.629 429.247, 77.... - 1 None 300 LINESTRING (645.965 0.525, 685.141 61.866, 724... - 2 None 400 LINESTRING (490.292 0.525, 505.756 40.732, 519... - 3 None 600 LINESTRING (911.433 1068.585, 908.856 1026.831... - 4 None 700 LINESTRING (228.432 1068.585, 239.772 1017.037... + + +----+------+---------------------------------------------------------+ + | ID | Z | geometry | + +----+------+---------------------------------------------------------+ + | 0 | None | 400 | LINESTRING (0.741 475.441, 35.629 429.247, 77.... | + +----+------+---------------------------------------------------------+ + | 1 | None | 300 | LINESTRING (645.965 0.525, 685.141 61.866, 724... | + +----+------+---------------------------------------------------------+ + | 2 | None | 400 | LINESTRING (490.292 0.525, 505.756 40.732, 519... | + +----+------+---------------------------------------------------------+ + | 3 | None | 600 | LINESTRING (911.433 1068.585, 908.856 1026.831... | + +----+------+---------------------------------------------------------+ + | 4 | None | 700 | LINESTRING (228.432 1068.585, 239.772 1017.037... | + +----+------+---------------------------------------------------------+ + >>> # Interpolating vector data >>> raster = gg.vector.interpolate_raster(gdf=gdf, method='rbf') @@ -3921,7 +3960,6 @@ def interpolate_raster( 398.16690286, 400.12027997]]) """ - # Trying to import scipy but returning error if scipy is not installed try: from scipy.interpolate import griddata, Rbf From 70296a000141915dca9d756bc932db559e88fac5 Mon Sep 17 00:00:00 2001 From: AlexanderJuestel Date: Tue, 30 Jul 2024 09:22:50 +0200 Subject: [PATCH 46/63] Format clip_by_bbox --- gemgis/vector.py | 120 ++++++++++++++++++++++++++++++++--------------- 1 file changed, 83 insertions(+), 37 deletions(-) diff --git a/gemgis/vector.py b/gemgis/vector.py index d0b266d1..143aee11 100644 --- a/gemgis/vector.py +++ b/gemgis/vector.py @@ -3921,7 +3921,7 @@ def interpolate_raster( _______ array : np.ndarray - Array representing the interpolated raster/digital elevation model + Array representing the interpolated raster/digital elevation model. .. versionadded:: 1.0.x @@ -4074,49 +4074,79 @@ def clip_by_bbox( drop_level0: bool = True, drop_level1: bool = True, ) -> gpd.geodataframe.GeoDataFrame: - """Clipping vector data contained in a GeoDataFrame to a provided bounding box/extent + """Clip vector data contained in a GeoDataFrame to a provided bounding box/extent. Parameters __________ gdf : gpd.geodataframe.GeoDataFrame - GeoDataFrame containing vector data that will be clipped to a provided bounding box/extent + GeoDataFrame containing vector data that will be clipped to a provided bounding box/extent. + + +----+------------------------------------------------+ + | | geometry | + +----+------------------------------------------------+ + | 0 | POINT (281.526 902.087) | + +----+------------------------------------------------+ + | 1 | POINT (925.867 618.577) | + +----+------------------------------------------------+ + | 2 | POINT (718.131 342.799) | + +----+------------------------------------------------+ + | 3 | POINT (331.011 255.684) | + +----+------------------------------------------------+ + | 4 | POINT (300.083 600.535) | + +----+------------------------------------------------+ bbox : List[Union[float, int]] - Bounding box of minx, maxx, miny, maxy values to clip the GeoDataFrame, , e.g. ``bbox=[0, 972, 0, 1069]`` + Bounding box of minx, maxx, miny, maxy values to clip the GeoDataFrame, , e.g. ``bbox=[0, 972, 0, 1069]``. - reset_index : bool - Variable to reset the index of the resulting GeoDataFrame. - Options include: ``True`` or ``False``, default set to ``True`` + reset_index : bool, default: ``True`` + Variable to reset the index of the resulting GeoDataFrame, e.g. ``reset_index=True``. + Options include: ``True`` or ``False``, default set to ``True``. - drop_level0 : bool - Variable to drop the level_0 column. - Options include: ``True`` or ``False``, default set to ``True`` + drop_level0 : bool, default: ``True`` + Variable to drop the level_0 column, e.g. ``drop_level0=True``. + Options include: ``True`` or ``False``, default set to ``True``. - drop_level1 : bool - Variable to drop the level_1 column. - Options include: ``True`` or ``False``, default set to ``True`` + drop_level1 : bool, default: ``True`` + Variable to drop the level_1 column, e.g. ``drop_level0=True``. + Options include: ``True`` or ``False``, default set to ``True``. - drop_index : bool - Variable to drop the index column. - Options include: ``True`` or ``False``, default set to ``True`` + drop_index : bool, default: ``True`` + Variable to drop the index column, e.g. ``drop_index=True``. + Options include: ``True`` or ``False``, default set to ``True``. - drop_id : bool - Variable to drop the id column. - Options include: ``True`` or ``False``, default set to ``True`` + drop_id : bool, default: ``True`` + Variable to drop the id column, e.g. ``drop_id=True``. + Options include: ``True`` or ``False``, default set to ``True``. - drop_points : bool - Variable to drop the points column. - Options include: ``True`` or ``False``, default set to ``True`` + drop_points : bool, default: ``True`` + Variable to drop the points column, e.g. ``drop_points=True``. + Options include: ``True`` or ``False``, default set to ``True``. Returns _______ gdf : gpd.geodataframe.GeoDataFrame - GeoDataFrame containing vector data clipped by a bounding box + GeoDataFrame containing vector data clipped by a bounding box. + + +----+-----------------------------+---------+---------+ + | ID | geometry | X | Y | + +----+-----------------------------+---------+---------+ + | 0 | POINT (281.526 902.087) | 281.53 | 902.09 | + +----+-----------------------------+---------+---------+ + | 1 | POINT (925.867 618.577) | 925.87 | 618.58 | + +----+-----------------------------+---------+---------+ + | 2 | POINT (718.131 342.799) | 718.13 | 342.80 | + +----+-----------------------------+---------+---------+ + | 3 | POINT (331.011 255.684) | 331.01 | 255.68 | + +----+-----------------------------+---------+---------+ + | 4 | POINT (300.083 600.535) | 300.08 | 600.54 | + +----+-----------------------------+---------+---------+ .. versionadded:: 1.0.x + .. versionchanged:: 1.2 + Example _______ @@ -4125,12 +4155,21 @@ def clip_by_bbox( >>> import geopandas as gpd >>> gdf = gpd.read_file(filename='file.shp') >>> gdf - id geometry - 0 None POINT (281.526 902.087) - 1 None POINT (925.867 618.577) - 2 None POINT (718.131 342.799) - 3 None POINT (331.011 255.684) - 4 None POINT (300.083 600.535) + + +----+------------------------------------------------+ + | | geometry | + +----+------------------------------------------------+ + | 0 | POINT (281.526 902.087) | + +----+------------------------------------------------+ + | 1 | POINT (925.867 618.577) | + +----+------------------------------------------------+ + | 2 | POINT (718.131 342.799) | + +----+------------------------------------------------+ + | 3 | POINT (331.011 255.684) | + +----+------------------------------------------------+ + | 4 | POINT (300.083 600.535) | + +----+------------------------------------------------+ + >>> # Returning the length of the original gdf >>> len(gdf) @@ -4142,12 +4181,20 @@ def clip_by_bbox( >>> # Clipping data by bounding box >>> gdf_clipped = gg.vector.clip_by_bbox(gdf=gdf, bbox=bbox) >>> gdf_clipped - geometry X Y - 0 POINT (281.526 902.087) 281.53 902.09 - 1 POINT (925.867 618.577) 925.87 618.58 - 2 POINT (718.131 342.799) 718.13 342.80 - 3 POINT (331.011 255.684) 331.01 255.68 - 4 POINT (300.083 600.535) 300.08 600.54 + + +----+-----------------------------+---------+---------+ + | ID | geometry | X | Y | + +----+-----------------------------+---------+---------+ + | 0 | POINT (281.526 902.087) | 281.53 | 902.09 | + +----+-----------------------------+---------+---------+ + | 1 | POINT (925.867 618.577) | 925.87 | 618.58 | + +----+-----------------------------+---------+---------+ + | 2 | POINT (718.131 342.799) | 718.13 | 342.80 | + +----+-----------------------------+---------+---------+ + | 3 | POINT (331.011 255.684) | 331.01 | 255.68 | + +----+-----------------------------+---------+---------+ + | 4 | POINT (300.083 600.535) | 300.08 | 600.54 | + +----+-----------------------------+---------+---------+ >>> # Returning the length of the clipped gdf >>> len(gdf_clipped) @@ -4156,10 +4203,9 @@ def clip_by_bbox( See Also ________ - clip_by_polygon : Clipping vector data with a Shapely Polygon + clip_by_polygon : Clip vector data with a Shapely Polygon """ - # Checking that the input data is of type GeoDataFrame if not isinstance(gdf, gpd.geodataframe.GeoDataFrame): raise TypeError("Loaded object is not a GeoDataFrame") From 423582474ee224ea22006d30575cb27ad7a525fd Mon Sep 17 00:00:00 2001 From: AlexanderJuestel Date: Tue, 30 Jul 2024 09:29:09 +0200 Subject: [PATCH 47/63] Format clip_by_polygon --- gemgis/vector.py | 120 ++++++++++++++++++++++++++++++++--------------- 1 file changed, 83 insertions(+), 37 deletions(-) diff --git a/gemgis/vector.py b/gemgis/vector.py index 143aee11..78afa30d 100644 --- a/gemgis/vector.py +++ b/gemgis/vector.py @@ -4333,50 +4333,80 @@ def clip_by_polygon( drop_level0: bool = True, drop_level1: bool = True, ) -> gpd.geodataframe.GeoDataFrame: - """Clipping vector data contained in a GeoDataFrame to a provided bounding box/extent + """Clip vector data contained in a GeoDataFrame to a provided bounding box/extent. Parameters __________ gdf : gpd.geodataframe.GeoDataFrame - GeoDataFrame containing vector data that will be clipped to a provided bounding box/extent + GeoDataFrame containing vector data that will be clipped to a provided bounding box/extent. - polygon : polygon: shapely.geometry.polygon + +----+-----------------------------+ + | ID | geometry | + +----+-----------------------------+ + | 0 | POINT (281.526 902.087) | + +----+-----------------------------+ + | 1 | POINT (925.867 618.577) | + +----+-----------------------------+ + | 2 | POINT (718.131 342.799) | + +----+-----------------------------+ + | 3 | POINT (331.011 255.684) | + +----+-----------------------------+ + | 4 | POINT (300.083 600.535) | + +----+-----------------------------+ + + polygon : shapely.geometry.Polygon Shapely Polygon defining the extent of the data, - e.g. ``polygon = Polygon([[0, 0], [10, 0], [10, 10], [0, 10], [0, 0]])`` + e.g. ``polygon = Polygon([[0, 0], [10, 0], [10, 10], [0, 10], [0, 0]])``. - reset_index : bool - Variable to reset the index of the resulting GeoDataFrame. - Options include: ``True`` or ``False``, default set to ``True`` + reset_index : bool, default: ``True`` + Variable to reset the index of the resulting GeoDataFrame, e.g. ``reset_index=True``. + Options include: ``True`` or ``False``, default set to ``True``. - drop_level0 : bool - Variable to drop the level_0 column. - Options include: ``True`` or ``False``, default set to ``True`` + drop_level0 : bool, default: ``True`` + Variable to drop the level_0 column, e.g. ``drop_level0=True``. + Options include: ``True`` or ``False``, default set to ``True``. - drop_level1 : bool - Variable to drop the level_1 column. - Options include: ``True`` or ``False``, default set to ``True`` + drop_level1 : bool, default: ``True`` + Variable to drop the level_1 column, e.g. ``drop_level1=True``. + Options include: ``True`` or ``False``, default set to ``True``. - drop_index : bool - Variable to drop the index column. - Options include: ``True`` or ``False``, default set to ``True`` + drop_index : bool, default: ``True`` + Variable to drop the index column, e.g. ``drop_index=True``. + Options include: ``True`` or ``False``, default set to ``True``. - drop_id : bool - Variable to drop the id column. - Options include: ``True`` or ``False``, default set to ``True`` + drop_id : bool, default: ``True`` + Variable to drop the id column, e.g. ``drop_id=True``. + Options include: ``True`` or ``False``, default set to ``True``. - drop_points : bool - Variable to drop the points column. - Options include: ``True`` or ``False``, default set to ``True`` + drop_points : bool, default: ``True`` + Variable to drop the points column, e.g. ``drop_points=True``. + Options include: ``True`` or ``False``, default set to ``True``. Returns _______ gdf : gpd.geodataframe.GeoDataFrame - GeoDataFrame containing vector data clipped by a bounding box + GeoDataFrame containing vector data clipped by a bounding box. + + +----+-----------------------------+---------+---------+ + | | geometry | X | Y | + +----+-----------------------------+---------+---------+ + | 0 | POINT (281.526 902.087) | 281.53 | 902.09 | + +----+-----------------------------+---------+---------+ + | 1 | POINT (925.867 618.577) | 925.87 | 618.58 | + +----+-----------------------------+---------+---------+ + | 2 | POINT (718.131 342.799) | 718.13 | 342.80 | + +----+-----------------------------+---------+---------+ + | 3 | POINT (331.011 255.684) | 331.01 | 255.68 | + +----+-----------------------------+---------+---------+ + | 4 | POINT (300.083 600.535) | 300.08 | 600.54 | + +----+-----------------------------+---------+---------+ .. versionadded:: 1.0.x + .. versionchanged:: 1.2 + Example _______ @@ -4385,12 +4415,21 @@ def clip_by_polygon( >>> import geopandas as gpd >>> gdf = gpd.read_file(filename='file.shp') >>> gdf - id geometry - 0 None POINT (281.526 902.087) - 1 None POINT (925.867 618.577) - 2 None POINT (718.131 342.799) - 3 None POINT (331.011 255.684) - 4 None POINT (300.083 600.535) + + +----+-----------------------------+ + | ID | geometry | + +----+-----------------------------+ + | 0 | POINT (281.526 902.087) | + +----+-----------------------------+ + | 1 | POINT (925.867 618.577) | + +----+-----------------------------+ + | 2 | POINT (718.131 342.799) | + +----+-----------------------------+ + | 3 | POINT (331.011 255.684) | + +----+-----------------------------+ + | 4 | POINT (300.083 600.535) | + +----+-----------------------------+ + >>> # Returning the length of the original gdf >>> len(gdf) @@ -4405,12 +4444,20 @@ def clip_by_polygon( >>> # Clipping data by the polygon >>> gdf_clipped = gg.vector.clip_by_polygon(gdf=gdf, polygon=polygon) >>> gdf_clipped - geometry X Y - 0 POINT (281.526 902.087) 281.53 902.09 - 1 POINT (925.867 618.577) 925.87 618.58 - 2 POINT (718.131 342.799) 718.13 342.80 - 3 POINT (331.011 255.684) 331.01 255.68 - 4 POINT (300.083 600.535) 300.08 600.54 + + +----+-----------------------------+---------+---------+ + | | geometry | X | Y | + +----+-----------------------------+---------+---------+ + | 0 | POINT (281.526 902.087) | 281.53 | 902.09 | + +----+-----------------------------+---------+---------+ + | 1 | POINT (925.867 618.577) | 925.87 | 618.58 | + +----+-----------------------------+---------+---------+ + | 2 | POINT (718.131 342.799) | 718.13 | 342.80 | + +----+-----------------------------+---------+---------+ + | 3 | POINT (331.011 255.684) | 331.01 | 255.68 | + +----+-----------------------------+---------+---------+ + | 4 | POINT (300.083 600.535) | 300.08 | 600.54 | + +----+-----------------------------+---------+---------+ >>> # Returning the length of the clipped gdf >>> len(gdf_clipped) @@ -4419,10 +4466,9 @@ def clip_by_polygon( See Also ________ - clip_by_bbox : Clipping vector data with a bbox + clip_by_bbox : Clip vector data with a bbox """ - # Checking if the gdf is of type GeoDataFrame if not isinstance(gdf, gpd.geodataframe.GeoDataFrame): raise TypeError("gdf must be of type GeoDataFrame") From 86bcbc230fd3f608e159be7787ea6c8cb5080be0 Mon Sep 17 00:00:00 2001 From: AlexanderJuestel Date: Tue, 30 Jul 2024 09:33:08 +0200 Subject: [PATCH 48/63] Format create_buffer --- gemgis/vector.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/gemgis/vector.py b/gemgis/vector.py index 78afa30d..5e6714e9 100644 --- a/gemgis/vector.py +++ b/gemgis/vector.py @@ -4525,25 +4525,27 @@ def clip_by_polygon( def create_buffer( geom_object: shapely.geometry.base.BaseGeometry, distance: Union[float, int] ) -> shapely.geometry.polygon.Polygon: - """Creating a buffer around a Shapely LineString or a Point + """Create a buffer around a Shapely LineString or a Shapely Point. Parameters __________ geom_object : shapely.geometry.base.BaseGeometry - Shapely LineString or Point, e.g. ``geom_object=Point(0, 0)`` + Shapely LineString or Point, e.g. ``geom_object=Point(0, 0)``. distance : float, int - Distance of the buffer around the geometry object, e.g. ``distance=10`` + Distance of the buffer around the geometry object, e.g. ``distance=10``. Returns _______ polygon : shapely.geometry.polygon.Polygon - Polygon representing the buffered area around a geometry object + Polygon representing the buffered area around a geometry object. .. versionadded:: 1.0.x + .. versionchanged:: 1.2 + Example _______ @@ -4563,10 +4565,9 @@ def create_buffer( See Also ________ - create_unified_buffer : Creating a unified buffer around Shapely LineStrings or Points + create_unified_buffer : Create a unified buffer around Shapely LineStrings or Points """ - # Checking that the geometry object is a Shapely LineString or Point if not isinstance(geom_object, shapely.geometry.base.BaseGeometry): raise TypeError( From 805a09521e1fed5c6fb3654734c9c9fd61c55906 Mon Sep 17 00:00:00 2001 From: AlexanderJuestel Date: Tue, 30 Jul 2024 09:35:30 +0200 Subject: [PATCH 49/63] Format create_unified_buffer --- gemgis/vector.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/gemgis/vector.py b/gemgis/vector.py index 5e6714e9..0cb8dce8 100644 --- a/gemgis/vector.py +++ b/gemgis/vector.py @@ -4565,7 +4565,7 @@ def create_buffer( See Also ________ - create_unified_buffer : Create a unified buffer around Shapely LineStrings or Points + create_unified_buffer : Create a unified buffer around Shapely LineStrings or Shapely Points """ # Checking that the geometry object is a Shapely LineString or Point @@ -4590,25 +4590,27 @@ def create_unified_buffer( ], distance: Union[np.ndarray, List[Union[float, int]], Union[float, int]], ) -> shapely.geometry.multipolygon.MultiPolygon: - """Creating a unified buffer around Shapely LineStrings or Points + """Create a unified buffer around Shapely LineStrings or Shapely Points. Parameters __________ geom_object : Union[gpd.geodataframe.GeoDataFrame, List[shapely.geometry.base.BaseGeometry]] - GeoDataFrame or List of Shapely objects + GeoDataFrame or List of Shapely objects. distance : Union[np.ndarray, List[Union[float, int]], Union[float, int]] - Distance of the buffer around the geometry object, e.g. ``distance=10`` + Distance of the buffer around the geometry object, e.g. ``distance=10``. Returns _______ polygon : shapely.geometry.multipolygon.MultiPolygon - Polygon representing the buffered area around a geometry object + Polygon representing the buffered area around a geometry object. .. versionadded:: 1.0.x + .. versionchanged:: 1.2 + Example _______ @@ -4636,10 +4638,9 @@ def create_unified_buffer( See Also ________ - create_buffer : Creating a buffer around a Shapely LineString or Point + create_buffer : Create a buffer around a Shapely LineString or a Shapely Point """ - # Checking that the geometry object is a Shapely LineString or Point if not isinstance( geom_object, From 832c43737a14afae14e1df56a3ac88494e28e472 Mon Sep 17 00:00:00 2001 From: AlexanderJuestel Date: Tue, 30 Jul 2024 09:36:58 +0200 Subject: [PATCH 50/63] Format subtract_geom_objects --- gemgis/vector.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/gemgis/vector.py b/gemgis/vector.py index 0cb8dce8..6cbdbc03 100644 --- a/gemgis/vector.py +++ b/gemgis/vector.py @@ -4684,27 +4684,29 @@ def subtract_geom_objects( geom_object1: shapely.geometry.base.BaseGeometry, geom_object2: shapely.geometry.base.BaseGeometry, ) -> shapely.geometry.base.BaseGeometry: - """Subtracting Shapely geometry objects from each other and returning the left over object + """Subtract Shapely geometry objects from each other and returning the left over object. Parameters __________ geom_object1 : shapely.geometry.base.BaseGeometry Shapely object from which other object will be subtracted, - e.g. ``geom_object1 = Polygon([[0, 0], [10, 0], [10, 10], [0, 10], [0, 0]])`` + e.g. ``geom_object1 = Polygon([[0, 0], [10, 0], [10, 10], [0, 10], [0, 0]])``. geom_object2 : shapely.geometry.base.BaseGeometry Shapely object which will be subtracted from other object - e.g. ``geom_object2 = Polygon([[5, 0], [15, 0], [15, 10], [5, 10], [5, 0]])`` + e.g. ``geom_object2 = Polygon([[5, 0], [15, 0], [15, 10], [5, 10], [5, 0]])``. Returns _______ result : shapely.geometry.base.BaseGeometry - Shapely object from which the second object was subtracted + Shapely object from which the second object was subtracted. .. versionadded:: 1.0.x + .. versionchanged:: 1.2 + Example _______ @@ -4726,7 +4728,6 @@ def subtract_geom_objects( 'POLYGON ((5 0, 0 0, 0 10, 5 10, 5 0))' """ - # Checking that the first geometry object is a Shapely Point, LineString or Polygon if not isinstance(geom_object1, shapely.geometry.base.BaseGeometry): raise TypeError( From 4f984d913472cbc3cdf1a35765c2d44cb3e76a5d Mon Sep 17 00:00:00 2001 From: AlexanderJuestel Date: Tue, 30 Jul 2024 09:38:55 +0200 Subject: [PATCH 51/63] Format remove_object_within_buffer --- gemgis/vector.py | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/gemgis/vector.py b/gemgis/vector.py index 6cbdbc03..2edc92b0 100644 --- a/gemgis/vector.py +++ b/gemgis/vector.py @@ -4752,36 +4752,38 @@ def remove_object_within_buffer( distance: Union[int, float] = None, buffer: bool = True, ) -> Tuple[shapely.geometry.base.BaseGeometry, shapely.geometry.base.BaseGeometry]: - """Removing object from a buffered object by providing a distance + """Remove object from a buffered object by providing a distance. Parameters __________ buffer_object : shapely.geometry.base.BaseGeometry - Shapely object for which a buffer will be created, e.g. ``buffer_object=Point(0, 0)`` + Shapely object for which a buffer will be created, e.g. ``buffer_object=Point(0, 0)``. buffered_object: shapely.geometry.base.BaseGeometry Shapely object that will be removed from the buffer, - e.g. ``buffered_object=LineString([(0, 0), (10, 10), (20, 20)])`` + e.g. ``buffered_object=LineString([(0, 0), (10, 10), (20, 20)])``. - distance : Union[float, int] - Distance of the buffer around the geometry object, e.g. ``distance=10``, default is ``None`` + distance : Union[float, int], default: ``None`` + Distance of the buffer around the geometry object, e.g. ``distance=10``, default is ``None``. - buffer : bool + buffer : bool, default: ``True`` Variable to create a buffer. - Options include: ``True`` or ``False``, default set to ``True`` + Options include: ``True`` or ``False``, default set to ``True``. Returns _______ result_out : shapely.geometry.base.BaseGeometry - Shapely object that remains after the buffering (outside the buffer) + Shapely object that remains after the buffering (outside the buffer). result_in : shapely.geometry.base.BaseGeometry - Shapely object that was buffered (inside the buffer) + Shapely object that was buffered (inside the buffer). .. versionadded:: 1.0.x + .. versionchanged:: 1.2 + Example _______ @@ -4811,8 +4813,8 @@ def remove_object_within_buffer( See Also ________ - remove_objects_within_buffer : Removing several objects from one buffered object - remove_interfaces_within_fault_buffers : Removing interfaces of layer boundaries within fault line buffers + remove_objects_within_buffer : Remove several objects from one buffered object + remove_interfaces_within_fault_buffers : Remove interfaces of layer boundaries within fault line buffers """ From c6e403bc1dc7ee71164a915b8468727a723bb07d Mon Sep 17 00:00:00 2001 From: AlexanderJuestel Date: Thu, 1 Aug 2024 17:46:30 +0200 Subject: [PATCH 52/63] Format remove_objects_within_buffer --- gemgis/vector.py | 39 ++++++++++++++++++++------------------- 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/gemgis/vector.py b/gemgis/vector.py index 2edc92b0..a45a731b 100644 --- a/gemgis/vector.py +++ b/gemgis/vector.py @@ -4817,7 +4817,6 @@ def remove_object_within_buffer( remove_interfaces_within_fault_buffers : Remove interfaces of layer boundaries within fault line buffers """ - # Checking that the buffer object is a Shapely Point or LineString if not isinstance(buffer_object, shapely.geometry.base.BaseGeometry): raise TypeError("Buffer object must be a Shapely Point or LineString") @@ -4877,48 +4876,50 @@ def remove_objects_within_buffer( Union[List[shapely.geometry.base.BaseGeometry], gpd.geodataframe.GeoDataFrame], Union[List[shapely.geometry.base.BaseGeometry], gpd.geodataframe.GeoDataFrame], ]: - """Removing objects from a buffered object by providing a distance + """Remove objects from a buffered object by providing a distance. Parameters __________ buffer_object : shapely.geometry.base.BaseGeometry - Shapely object for which a buffer will be created, e.g. ``buffer_object=Point(0, 0)`` + Shapely object for which a buffer will be created, e.g. ``buffer_object=Point(0, 0)``. buffered_object_gdf: Union[gpd.geodataframe.GeoDataFrame, List[shapely.geometry.base.BaseGeometry]] GeoDataFrame or List of Base Geometries containing Shapely objects that will be buffered by the buffer - object + object. - distance : float, int - Distance of the buffer around the geometry object, e.g. ``distance=10`` + distance : float, int, default: ``10`` + Distance of the buffer around the geometry object, e.g. ``distance=10``. - return_gdfs : bool + return_gdfs : bool, default: ``False`` Variable to create GeoDataFrames of the created list of Shapely Objects. - Options include: ``True`` or ``False``, default set to ``False`` + Options include: ``True`` or ``False``, default set to ``False``. - remove_empty_geometries : bool + remove_empty_geometries : bool, default: ``False`` Variable to remove empty geometries. - Options include: ``True`` or ``False``, default set to ``False`` + Options include: ``True`` or ``False``, default set to ``False``. - extract_coordinates : bool + extract_coordinates : bool, default: ``False`` Variable to extract X and Y coordinates from resulting Shapely Objects. - Options include: ``True`` or ``False``, default set to ``False`` + Options include: ``True`` or ``False``, default set to ``False``. - buffer : bool + buffer : bool, default: ``True`` Variable to create a buffer. - Options include: ``True`` or ``False``, default set to ``True`` + Options include: ``True`` or ``False``, default set to ``True``. Returns _______ result_out : list, gpd.geodataframe.GeoDataFrame - List or GeoDataFrame of Shapely objects that remain after the buffering (outside the buffer) + List or GeoDataFrame of Shapely objects that remain after the buffering (outside the buffer). result_in : list, gpd.geodataframe.GeoDataFrame - List or GeoDataFrame of Shapely objects that was buffered (inside the buffer) + List or GeoDataFrame of Shapely objects that was buffered (inside the buffer). .. versionadded:: 1.0.x + .. versionchanged:: 1.2 + Example _______ @@ -4939,7 +4940,7 @@ def remove_objects_within_buffer( >>> linestring2.wkt 'LINESTRING (0 0, 10 10, 20 20)' - >>> # Create list of buffer objects + >>> # Creating list of buffer objects >>> buffer_objects = [linestring1, linestring2] >>> # Removing objects within buffer @@ -4958,8 +4959,8 @@ def remove_objects_within_buffer( See Also ________ - remove_object_within_buffer : Removing one object from one buffered object - remove_interfaces_within_fault_buffers : Removing interfaces of layer boundaries within fault line buffers + remove_object_within_buffer : Remove one object from one buffered object + remove_interfaces_within_fault_buffers : Remove interfaces of layer boundaries within fault line buffers """ From e97499ce7fc7ffbc8d7454702ddbb0a8483a3c0d Mon Sep 17 00:00:00 2001 From: AlexanderJuestel Date: Thu, 1 Aug 2024 17:49:38 +0200 Subject: [PATCH 53/63] Format pooch functions --- gemgis/download_gemgis_data.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/gemgis/download_gemgis_data.py b/gemgis/download_gemgis_data.py index b83f19e2..1c327a7d 100644 --- a/gemgis/download_gemgis_data.py +++ b/gemgis/download_gemgis_data.py @@ -24,8 +24,7 @@ def create_pooch(storage_url: str, files: List[str], target: str): - """ - Create pooch class to fetch files from a website. + """Create pooch class to fetch files from a website. Parameters __________ @@ -44,6 +43,7 @@ def create_pooch(storage_url: str, files: List[str], target: str): See also ________ download_tutorial_data: Download the GemGIS data for each tutorial. + """ try: import pooch @@ -65,22 +65,24 @@ def download_tutorial_data( dirpath: str = "", storage_url: str = "https://rwth-aachen.sciebo.de/s/AfXRsZywYDbUF34/download?path=%2F", ): - """ - Download the GemGIS data for each tutorial. + """Download the GemGIS data for each tutorial. Parameters __________ filename : str File name to be downloaded by pooch, e.g. ``filename='file.zip'``. + dirpath : str, default: ``''`` Path to the directory where the data is being stored, default to the directory where the notebook is located, e.g. ``dirpath='Documents/gemgis/'``. + storage_url : str, default 'https://rwth-aachen.sciebo.de/s/AfXRsZywYDbUF34/download?path=%2F' URL to the GemGIS data storage, default is the RWTH Aachen University Sciebo Cloud Storage. See also ________ create_pooch : Create pooch class to fetch files from a website. + """ try: from pooch import HTTPDownloader From 38ea9997d4ad6c8360c2b8ad0ebef06909531567 Mon Sep 17 00:00:00 2001 From: AlexanderJuestel Date: Thu, 1 Aug 2024 18:24:49 +0200 Subject: [PATCH 54/63] Format web.py functions --- gemgis/web.py | 167 ++++++++++++++++++++++++++------------------------ 1 file changed, 87 insertions(+), 80 deletions(-) diff --git a/gemgis/web.py b/gemgis/web.py index 96b20a09..8f9b60b8 100644 --- a/gemgis/web.py +++ b/gemgis/web.py @@ -34,7 +34,7 @@ def load_wms(url: str, version: str = "1.3.0"): # -> owslib.wms.WebMapService: - """Loading a WMS Service by URL + """Loading a WMS Service by URL. Parameters __________ @@ -49,7 +49,7 @@ def load_wms(url: str, version: str = "1.3.0"): # -> owslib.wms.WebMapService: _______ wms : owslib.map.wms111.WebMapService - OWSLib WebMapService Object + OWSLib WebMapService Object. .. versionadded:: 1.0.x @@ -123,53 +123,53 @@ def load_as_map( overwrite_file: bool = False, create_directory: bool = False, ): # -> owslib.util.ResponseWrapper: - """Loading a portion of a WMS as array + """Load a portion of a WMS as array. Parameters __________ url : str - Link of the WMS Service, e.g. ``url='https://ows.terrestris.de/osm/service?'`` + Link of the WMS Service, e.g. ``url='https://ows.terrestris.de/osm/service?'``. - version : str, default: ``'1.3.0'`` + version : str, default: ``'1.3.0'``. Version of the WMS Service, e.g. ``version='1.3.0'``. layer : str - Name of layer to be requested, e.g. ``layer='OSM-WMS'`` + Name of layer to be requested, e.g. ``layer='OSM-WMS'``. style : str - Name of style of the layer, e.g. ``style='default'`` + Name of style of the layer, e.g. ``style='default'``. crs : str - String or dict containing the CRS, e.g. ``crs='EPSG:4647'`` + String or dict containing the CRS, e.g. ``crs='EPSG:4647'``. bbox : List[Union[float,int]] - List of bounding box coordinates, e.g. ``bbox=[0, 972, 0, 1069]`` + List of bounding box coordinates, e.g. ``bbox=[0, 972, 0, 1069]``. size : List[int] - List of x and y values defining the size of the image, e.g. ``size=[1000,1000]`` + List of x and y values defining the size of the image, e.g. ``size=[1000,1000]``. filetype : str - String of the image type to be downloaded, e.g. ``filetype='image/png'`` + String of the image type to be downloaded, e.g. ``filetype='image/png'``. - transparent : bool + transparent : bool, default: ``True`` Variable if layer is transparent. - Options include: ``True`` or ``False``, default set to ``True`` + Options include: ``True`` or ``False``, default set to ``True``. - save_image : bool + save_image : bool, default: ``False`` Variable to save image. - Options include: ``True`` or ``False``, default set to ``False`` + Options include: ``True`` or ``False``, default set to ``False``. - path : str - Path and file name of the file to be saved, e.g. ``path=map.tif`` + path : str, default: ``None`` + Path and file name of the file to be saved, e.g. ``path=map.tif``, default is ``None``. - overwrite_file : bool + overwrite_file : bool, default: ``False`` Variable to overwrite an already existing file. - Options include: ``True`` or ``False``, default set to ``False`` + Options include: ``True`` or ``False``, default set to ``False``. - create_directory : bool - Variable to create a new directory of directory does not exist - Options include: ``True`` or ``False``, default set to ``False`` + create_directory : bool, default: ``False`` + Variable to create a new directory of directory does not exist. + Options include: ``True`` or ``False``, default set to ``False``. Returns _______ @@ -197,7 +197,6 @@ def load_as_map( load_as_array : Load Map as array from WMS Service """ - # Trying to import owslib but returning error if owslib is not installed try: from owslib import util @@ -333,7 +332,7 @@ def load_as_array( overwrite_file: bool = False, create_directory: bool = False, ) -> np.ndarray: - """Loading a portion of a WMS as array + """Load. a portion of a WMS as array. Parameters __________ @@ -362,22 +361,22 @@ def load_as_array( filetype : str String of the image type to be downloaded, e.g. 'filetype='image/png'``. - transparent : bool + transparent : bool, default: ``True`` Variable if layer is transparent. Options include: ``True`` or ``False``, default set to ``True``. - save_image : bool + save_image : bool, default: ``False`` Variable to save image. Options include: ``True`` or ``False``, default set to ``False``. - path : str - Path and file name of the file to be saved, e.g. ``path=map.tif``. + path : str, default: ``None`` + Path and file name of the file to be saved, e.g. ``path=map.tif``, default is ``None``. - overwrite_file : bool + overwrite_file : bool, default: ``False`` Variable to overwrite an already existing file. Options include: ``True`` or ``False``, default set to ``False``. - create_directory : bool + create_directory : bool, default: ``False`` Variable to create a new directory of directory does not exist. Options include: ``True`` or ``False``, default set to ``False``. @@ -385,9 +384,7 @@ def load_as_array( _______ wms_array: np.ndarray - OWSlib map object loaded as np.ndarray - - .. versionadded:: 1.0.x + OWSlib map object loaded as np.ndarray. Example _______ @@ -415,7 +412,6 @@ def load_as_array( .. versionchanged:: 1.2 """ - # Trying to import owslib but returning error if owslib is not installed try: from owslib import util @@ -542,22 +538,24 @@ def load_as_array( def load_wfs(url: str): # -> owslib.wfs.WebFeatureService: - """Loading a WFS Service by URL + """Load a WFS Service by URL. Parameters __________ url : str - Link of the WFS Service, e.g. ``url="https://nibis.lbeg.de/net3/public/ogc.ashx?NodeId=476&Service=WFS&"`` + Link of the WFS Service, e.g. ``url="https://nibis.lbeg.de/net3/public/ogc.ashx?NodeId=476&Service=WFS&"``. Returns _______ wfs : owslib.feature.wfs100.WebFeatureService_1_0_0 - OWSLib Feature object + OWSLib Feature object. .. versionadded:: 1.0.x + .. versionchanged:: 1.2 + Example _______ @@ -573,7 +571,6 @@ def load_wfs(url: str): # -> owslib.wfs.WebFeatureService: load_as_gpd : Load information of a WFS Service as GeoDataFrame """ - # Trying to import owslib but returning error if owslib is not installed try: from owslib import util @@ -613,28 +610,30 @@ def load_wfs(url: str): # -> owslib.wfs.WebFeatureService: def load_as_gpd( url: str, typename: str = None, outputformat: str = None ) -> gpd.geodataframe.GeoDataFrame: - """Requesting data from a WFS Service + """Request data from a WFS Service Parameters __________ url : str - URL of the Web Feature Service, e.g. ``url="https://nibis.lbeg.de/net3/public/ogc.ashx?NodeId=476&Service=WFS&"`` + URL of the Web Feature Service, e.g. ``url="https://nibis.lbeg.de/net3/public/ogc.ashx?NodeId=476&Service=WFS&"``. - typename : str - Name of the feature layer, e.g. ``typename='iwan:L383'``, default is ``None`` + typename : str, default: ``None`` + Name of the feature layer, e.g. ``typename='iwan:L383'``, default is ``None``. - outputformat : str - Output format of the feature layer, e.g. ``outputformat='xml/gml2'``, default is ``None`` + outputformat : str, default: ``None`` + Output format of the feature layer, e.g. ``outputformat='xml/gml2'``, default is ``None``. Returns _______ feature : gpd.geodataframe.GeoDataFrame - GeoDataFrame containing the feature data of the WFS Service + GeoDataFrame containing the feature data of the WFS Service. .. versionadded:: 1.0.x + .. versionchanged:: 1.2 + Example _______ @@ -642,8 +641,12 @@ def load_as_gpd( >>> import gemgis as gg >>> wfs = gg.web.load_as_gpd(url="https://nibis.lbeg.de/net3/public/ogc.ashx?NodeId=476&Service=WFS&") >>> wfs - gml_id OBJECTID ID SURVEYNAME ARCHIV MESSJAHR OPERATOR OP_NACHFOL MESSFIRMA MESSPUNKTE UP_DATE geometry - 0 1541 1541 112 Jemgum 2007 0127494 2007 GdF Produktion Exploration Deutschland GmbH Neptune Energy Deutschland GmbH Geophysik und Geotechnik Leipzig GmbH 1340 2020-01-20T00:00:00+01:00 MULTIPOLYGON (((32395246.839 5907777.660, 3239... + + +---------+----------+----+------------+----------+----------+--------------------------------------------------------+--------------------------------------+----------------------------------------------+------------+----------------------------+----------------------------------------------------+ + | gml_id | OBJECTID | ID | SURVEYNAME | ARCHIV | MESSJAHR | OPERATOR | OP_NACHFOL | MESSFIRMA | MESSPUNKTE | UP_DATE | geometry | + +---------+----------+----+------------+----------+----------+--------------------------------------------------------+--------------------------------------+----------------------------------------------+------------+----------------------------+----------------------------------------------------+ + | 1541 | 1541 | 112| Jemgum 2007| 0127494 | 2007 | GdF Produktion Exploration Deutschland GmbH | Neptune Energy Deutschland GmbH | Geophysik und Geotechnik Leipzig GmbH | 1340 | 2020-01-20T00:00:00+01:00 | MULTIPOLYGON (((32395246.839 5907777.660, 3239... | + +---------+----------+----+------------+----------+----------+--------------------------------------------------------+--------------------------------------+----------------------------------------------+------------+----------------------------+----------------------------------------------------+ See Also ________ @@ -651,7 +654,6 @@ def load_as_gpd( load_wfs : Load WFS Service """ - # Trying to import owslib but returning error if owslib is not installed try: from owslib import util @@ -721,22 +723,24 @@ def load_as_gpd( def load_wcs(url: str): # -> owslib.wcs.WebCoverageService: - """Loading Web Coverage Service + """Load Web Coverage Service. Parameters __________ url : str - Link of the Web Coverage Service, e.g. ``url='https://www.wcs.nrw.de/geobasis/wcs_nw_dgm'`` + Link of the Web Coverage Service, e.g. ``url='https://www.wcs.nrw.de/geobasis/wcs_nw_dgm'``. Returns _______ wcs : owslib.coverage.wcs201.WebCoverageService_2_0_1 - OWSLib Web Coverage Object + OWSLib Web Coverage Object. .. versionadded:: 1.0.x + .. versionchanged:: 1.2 + Example _______ @@ -754,7 +758,6 @@ def load_wcs(url: str): # -> owslib.wcs.WebCoverageService: load_as_files : Download WCS data files """ - # Trying to import owslib but returning error if owslib is not installed try: from owslib import util @@ -784,38 +787,40 @@ def create_request( extent: List[Union[float, int]], name: str = "test.tif", ) -> str: - """Creating URL to request data from WCS Server + """Create URL to request data from WCS Server. Parameters __________ wcs_url : str - Url of the WCS server, e.g. ``url='https://www.wcs.nrw.de/geobasis/wcs_nw_dgm'`` + Url of the WCS server, e.g. ``url='https://www.wcs.nrw.de/geobasis/wcs_nw_dgm'``. version : str - Version number of the WCS as string, e.g. ``version='2.0.1'`` + Version number of the WCS as string, e.g. ``version='2.0.1'``. identifier : str - Name of the layer, e.g. ``identifier='nw_dgm'`` + Name of the layer, e.g. ``identifier='nw_dgm'``. form : str - Format of the layer, e.g. ``form='image/tiff'`` + Format of the layer, e.g. ``form='image/tiff'``. extent : List[Union[float,int]] Extent of the tile to be downloaded, size may be restricted by server, - e.g. ``extent=[0, 972, 0, 1069]`` + e.g. ``extent=[0, 972, 0, 1069]``. - name : str - Name of file, e.g. ``name='tile1.tif'``, default is ``'test.tif'`` + name : str, default: ``'test.tif'`` + Name of file, e.g. ``name='tile1.tif'``, default is ``'test.tif'``. Returns _______ url : str - Url for the WCS request + Url for the WCS request. .. versionadded:: 1.0.x + .. versionchanged:: 1.2 + Example _______ @@ -836,7 +841,6 @@ def create_request( load_as_files : Download WCS data files """ - # Trying to import owslib but returning error if owslib is not installed try: from owslib import util @@ -910,27 +914,29 @@ def create_request( def load_as_file( url: str, path: str, overwrite_file: bool = False, create_directory: bool = False ): - """Executing WCS request and downloading file into specified folder + """Execute WCS request and downloading file into specified folder. Parameters __________ url: str - Url for request + Url for request. path: str - Path where file is saved, e.g. ``path='tile.tif'`` + Path where file is saved, e.g. ``path='tile.tif'``. - overwrite_file : bool + overwrite_file : bool, default: ``False`` Variable to overwrite an already existing file. - Options include: ``True`` or ``False``, default set to ``False`` + Options include: ``True`` or ``False``, default set to ``False``. - create_directory : bool + create_directory : bool, default: ``False`` Variable to create a new directory of directory does not exist - Options include: ``True`` or ``False``, default set to ``False`` + Options include: ``True`` or ``False``, default set to ``False``. .. versionadded:: 1.0.x + .. versionchanged:: 1.2 + Example _______ @@ -954,7 +960,6 @@ def load_as_file( load_as_files : Download WCS data files """ - # Trying to import owslib but returning error if owslib is not installed try: from owslib import util @@ -1020,39 +1025,41 @@ def load_as_files( path: str = "", create_directory: bool = False, ): - """Executing WCS requests and downloading files into specified folder + """Execute WCS requests and downloading files into specified folder. Parameters __________ wcs_url : str - Url of the WCS server, e.g. ``url='https://www.wcs.nrw.de/geobasis/wcs_nw_dgm'`` + Url of the WCS server, e.g. ``url='https://www.wcs.nrw.de/geobasis/wcs_nw_dgm'``. version : str - Version number of the WCS as string, e.g. ``version='2.0.1'`` + Version number of the WCS as string, e.g. ``version='2.0.1'``. identifier : str - Name of the layer, e.g. ``identifier='nw_dgm'`` + Name of the layer, e.g. ``identifier='nw_dgm'``. form : str - Format of the layer, e.g. ``form='image/tiff'`` + Format of the layer, e.g. ``form='image/tiff'``. extent : List[Union[float,int]] Extent of the tile to be downloaded, size may be restricted by server, - e.g. ``extent=[0, 972, 0, 1069]`` + e.g. ``extent=[0, 972, 0, 1069]``. size : int - Size of the quadratic tile that is downloaded, e.g. ``size=2000`` + Size of the quadratic tile that is downloaded, e.g. ``size=2000``. path : str - Path where the file is going to be downloaded, e.g. ``name='tile1'`` + Path where the file is going to be downloaded, e.g. ``name='tile1'``. - create_directory : bool + create_directory : bool, default: ``False`` Variable to create a new directory of directory does not exist - Options include: ``True`` or ``False``, default set to ``False`` + Options include: ``True`` or ``False``, default set to ``False``. .. versionadded:: 1.0.x + .. versionchanged:: 1.2 + Example _______ From 01bce2e0d4bd0e77a5e52a6c695813c5b757a75f Mon Sep 17 00:00:00 2001 From: AlexanderJuestel Date: Wed, 16 Oct 2024 16:04:47 +0200 Subject: [PATCH 55/63] EditTSReader --- gemgis/raster.py | 5 ++++- gemgis/visualization.py | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/gemgis/raster.py b/gemgis/raster.py index 5eeb7e94..8a5b5f17 100644 --- a/gemgis/raster.py +++ b/gemgis/raster.py @@ -1915,7 +1915,10 @@ def read_ts(path: Union[str, Path]) -> Tuple[list, list]: if line_type == "PROPERTIES": columns += values - elif line_type == "TFACE": + # Deleting duplicate column names + columns = list(dict.fromkeys(columns)) + + elif line_type == "TFACE" or line_type == "END": # Creating array for faces faces = np.array(faces, dtype=np.int32) diff --git a/gemgis/visualization.py b/gemgis/visualization.py index b1aff416..badd13bf 100644 --- a/gemgis/visualization.py +++ b/gemgis/visualization.py @@ -1193,7 +1193,7 @@ def create_polydata_from_ts( for i in range(len(data[0])): # Creating faces for PyVista PolyData faces = np.hstack( - np.pad(data[1][i], ((0, 0), (1, 0)), "constant", constant_values=3) + np.pad(data[1][i]-1, ((0, 0), (1, 0)), "constant", constant_values=3) ) # Creating vertices for PyVista Polydata From 023f64bff1b1fb9b1e763f8e061cec0b40f598fa Mon Sep 17 00:00:00 2001 From: AlexanderJuestel Date: Thu, 17 Oct 2024 13:36:04 +0200 Subject: [PATCH 56/63] FixAzimuthCalculation --- gemgis/postprocessing.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/gemgis/postprocessing.py b/gemgis/postprocessing.py index 4031cc8e..cdfe72e1 100644 --- a/gemgis/postprocessing.py +++ b/gemgis/postprocessing.py @@ -566,9 +566,12 @@ def calculate_dip_and_azimuth_from_mesh( # Calculating the azimuths azimuths = [ - np.rad2deg(np.arctan(mesh["Normals"][i][0] / mesh["Normals"][i][1])) + 180 + np.rad2deg(np.arctan2(mesh["Normals"][i][0], mesh["Normals"][i][1])) for i in range(len(mesh["Normals"])) ] + + # Shifting values + azimuths[azimuths < 0] += 360 # Assigning dips and azimuths to scalars mesh["Dips [°]"] = dips From 5a8737ef71c363a7649050419fd40abadbaeaafd Mon Sep 17 00:00:00 2001 From: AlexanderJuestel Date: Tue, 3 Dec 2024 21:00:36 +0100 Subject: [PATCH 57/63] Updating Dependencies --- README.md | 6 +++--- environment.yml | 16 ++++++++-------- environment_dev.yml | 14 +++++++------- pyproject.toml | 2 +- requirements.txt | 14 +++++++------- 5 files changed, 26 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index bee3d694..c12e25b3 100644 --- a/README.md +++ b/README.md @@ -41,9 +41,9 @@ Furthermore, many [example models](https://gemgis.readthedocs.io/en/latest/getti ## Installation -It is recommended to use GemGIS with **python">=3.10"** in a separated environment. The main packages and its dependencies can be installed via the conda-forge channel. GemGIS is then available through PyPi or Conda. -1) `conda install -c conda-forge geopandas">=0.13.2" rasterio">=1.3.8"` -2) `conda install -c conda-forge pyvista">=0.42.1"` +It is recommended to use GemGIS with **python">=3.11"** in a separated environment. The main packages and its dependencies can be installed via the conda-forge channel. GemGIS is then available through PyPi or Conda. +1) `conda install -c conda-forge geopandas">=1.0.1" rasterio">=1.4.3"` +2) `conda install -c conda-forge pyvista">=0.44.2"` 3) `pip install gemgis` / `conda install -c conda-forge gemgis` Check out the [Installation Page](https://gemgis.readthedocs.io/en/latest/getting_started/installation.html) for more detailed instructions. diff --git a/environment.yml b/environment.yml index 65fc12a2..a9af2506 100644 --- a/environment.yml +++ b/environment.yml @@ -1,4 +1,4 @@ -# Requirements as of July 2024 +# Requirements as of December 2024 name: gemgis_env channels: - conda-forge @@ -6,14 +6,14 @@ dependencies: - python>=3.11 # geopandas will also install numpy, pandas, shapely, pyogrio, and pyproj - geopandas>=1.0.1 - - shapely>=2.0.5 - - pandas>=2.2.2 - - numpy>=2.0.1 + - shapely>=2.0.6 + - pandas>=2.2.3 + - numpy>=2.1.3 - affine>=2.4.0 - - pyproj>=3.6.1 + - pyproj>=3.7.0 # rasterio will also install affine - - rasterio>=1.3.10 + - rasterio>=1.4.3 # pyvista also install pooch and matplotlib - - pyvista>=0.44.1 - - matplotlib>=3.9.1 + - pyvista>=0.44.2 + - matplotlib>=3.9.3 - gemgis>=1.1 diff --git a/environment_dev.yml b/environment_dev.yml index 526ebcf8..a67ec7c1 100644 --- a/environment_dev.yml +++ b/environment_dev.yml @@ -5,16 +5,16 @@ dependencies: - python>=3.11 # geopandas will also install numpy, pandas, shapely, pyogrio, and pyproj - geopandas>=1.0.1 - - shapely>=2.0.5 - - pandas>=2.2.2 - - numpy>=2.0.1 + - shapely>=2.0.6 + - pandas>=2.2.3 + - numpy>=2.1.3 - affine>=2.4.0 - - pyproj>=3.6.1 + - pyproj>=3.7.0 # rasterio will also install affine - - rasterio>=1.3.10 + - rasterio>=1.4.3 # pyvista also install pooch and matplotlib - - pyvista>=0.44.1 - - matplotlib>=3.9.1 + - pyvista>=0.44.2 + - matplotlib>=3.9.3 - gemgis>=1.1 - rioxarray - scipy diff --git a/pyproject.toml b/pyproject.toml index 8c7c83e4..f3104f8c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,7 +22,7 @@ keywords = ["dataprocessing", "modeling", "geospatial", "geographic-data", "spat readme = "README.md" license = {file = "LICENSE"} dynamic = ['version'] -requires-python = ">=3.8" +requires-python = ">=3.9" classifiers = [ 'Development Status :: 5 - Production/Stable', 'Intended Audience :: Science/Research', diff --git a/requirements.txt b/requirements.txt index f37d8f01..f2d0859a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,15 +2,15 @@ # geopandas will also install numpy, pandas, shapely, pyogrio, and pyproj geopandas>=1.0.1 -shapely>=2.0.5 -pandas>=2.2.2 -numpy>=2.0.1 -pyproj>=3.6.1 +shapely>=2.0.6 +pandas>=2.2.3 +numpy>=2.1.3 +pyproj>=3.7.0 # rasterio will also install affine -rasterio>=1.3.10 +rasterio>=1.4.3 affine>=2.4.0 # pyvista also install pooch and matplotlib -pyvista>=0.44.1 -matplotlib>=3.9.1 \ No newline at end of file +pyvista>=0.44.2 +matplotlib>=3.9.3 \ No newline at end of file From 6975212917a7a1ca04964e5e0824c81b262684d7 Mon Sep 17 00:00:00 2001 From: AlexanderJuestel Date: Thu, 2 Jan 2025 10:10:04 +0100 Subject: [PATCH 58/63] Update Notebook 01 --- .../tutorial/01_extract_xy.ipynb | 311 ++++++++++-------- 1 file changed, 178 insertions(+), 133 deletions(-) diff --git a/docs/getting_started/tutorial/01_extract_xy.ipynb b/docs/getting_started/tutorial/01_extract_xy.ipynb index 9820960d..01c61375 100644 --- a/docs/getting_started/tutorial/01_extract_xy.ipynb +++ b/docs/getting_started/tutorial/01_extract_xy.ipynb @@ -6,7 +6,9 @@ "source": [ "# 01 Extract XY Coordinates\n", "\n", - "Vector data is commonly provided as ``shape`` files. These files can be loaded with ``GeoPandas`` as ``GeoDataFrames``. Each geometry object is stored as ``shapely`` object within the ``GeoSeries`` ``geometry`` of the ``GeoDataFrames``. The basic ``shapely`` objects, also called Base Geometries, used here are:\n", + "In this notebook, we illustrate how to extract X and Y coordinates from Vector data. We utilize the ``GeoPandas`` package (version >=1.0.1, January 2025) to extract the information.\n", + "\n", + "Vector data is commonly provided as ESRI ``shape`` files, geopackages, or in other vector formats. These files can be loaded with ``GeoPandas`` as ``GeoDataFrames``. Each geometry object is stored as ``shapely`` object within the ``GeoSeries`` ``geometry`` of the ``GeoDataFrames``. The basic ``shapely`` objects, also called ``Base Geometries``, used here are:\n", "\n", "* Points/Multi-Points\n", "* Lines/Multi-Lines\n", @@ -52,15 +54,7 @@ "start_time": "2021-03-17T10:51:49.659546Z" } }, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "Downloading file '01_extract_xy.zip' from 'https://rwth-aachen.sciebo.de/s/AfXRsZywYDbUF34/download?path=%2F01_extract_xy.zip' to 'C:\\Users\\ale93371\\Documents\\gemgis\\docs\\getting_started\\tutorial\\data\\01_extract_xy'.\n" - ] - } - ], + "outputs": [], "source": [ "gg.download_gemgis_data.download_tutorial_data(filename=\"01_extract_xy.zip\", dirpath=file_path)" ] @@ -71,7 +65,7 @@ "source": [ "## Point Data\n", "\n", - "The point data stored as shape file will be loaded as GeoDataFrame. It contains an ``id``, ``formation`` and the ``geometry`` column." + "The point data stored as shape file will be loaded as ``GeoDataFrame``. It contains an ``id``, ``formation`` and the ``geometry`` column." ] }, { @@ -113,31 +107,31 @@ " \n", " \n", " 0\n", - " None\n", + " NaN\n", " Ton\n", " POINT (19.15013 293.31349)\n", " \n", " \n", " 1\n", - " None\n", + " NaN\n", " Ton\n", " POINT (61.93437 381.45933)\n", " \n", " \n", " 2\n", - " None\n", + " NaN\n", " Ton\n", " POINT (109.35786 480.94557)\n", " \n", " \n", " 3\n", - " None\n", + " NaN\n", " Ton\n", - " POINT (157.81230 615.99943)\n", + " POINT (157.8123 615.99943)\n", " \n", " \n", " 4\n", - " None\n", + " NaN\n", " Ton\n", " POINT (191.31803 719.09398)\n", " \n", @@ -146,12 +140,12 @@ "" ], "text/plain": [ - " id formation geometry\n", - "0 None Ton POINT (19.15013 293.31349)\n", - "1 None Ton POINT (61.93437 381.45933)\n", - "2 None Ton POINT (109.35786 480.94557)\n", - "3 None Ton POINT (157.81230 615.99943)\n", - "4 None Ton POINT (191.31803 719.09398)" + " id formation geometry\n", + "0 NaN Ton POINT (19.15013 293.31349)\n", + "1 NaN Ton POINT (61.93437 381.45933)\n", + "2 NaN Ton POINT (109.35786 480.94557)\n", + "3 NaN Ton POINT (157.8123 615.99943)\n", + "4 NaN Ton POINT (191.31803 719.09398)" ] }, "execution_count": 3, @@ -260,7 +254,9 @@ "source": [ "### Extracting the Coordinates\n", "\n", - "The resulting GeoDataFrame has now an additional ``X`` and ``Y`` column containing the coordinates of the point objects. These can now be easily used for further processing. The geometry types of the shapely objects remained unchanged. The ``id`` column was dropped by default.\n" + "To make the coordinates easier accessible, we use the GemGIS function ``extract_xy`` to append the stored ``X`` and ``Y`` coordinate information to the GeoDataFrame.\n", + "\n", + "The resulting GeoDataFrame has now an additional ``X`` and ``Y`` column containing the coordinates of the point objects. These can now be easily used for further processing. The geometry types of the shapely objects remained unchanged. The ``id`` column was dropped by default but can be kept if needed (``drop_id=False``).\n" ] }, { @@ -325,7 +321,7 @@ " \n", " 3\n", " Ton\n", - " POINT (157.81230 615.99943)\n", + " POINT (157.8123 615.99943)\n", " 157.81\n", " 616.00\n", " \n", @@ -345,7 +341,7 @@ "0 Ton POINT (19.15013 293.31349) 19.15 293.31\n", "1 Ton POINT (61.93437 381.45933) 61.93 381.46\n", "2 Ton POINT (109.35786 480.94557) 109.36 480.95\n", - "3 Ton POINT (157.81230 615.99943) 157.81 616.00\n", + "3 Ton POINT (157.8123 615.99943) 157.81 616.00\n", "4 Ton POINT (191.31803 719.09398) 191.32 719.09" ] }, @@ -381,7 +377,7 @@ "outputs": [ { "data": { - "image/png": "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\n", + "image/png": "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", "text/plain": [ "

" ] @@ -396,10 +392,16 @@ "fig, (ax1,ax2) = plt.subplots(1,2)\n", "\n", "gdf.plot(ax=ax1, aspect='equal')\n", + "ax1.set_xlabel('X [m]')\n", + "ax1.set_ylabel('Y [m]')\n", "ax1.grid()\n", "\n", "gdf_xy.plot(ax=ax2, aspect='equal')\n", - "ax2.grid()" + "ax2.set_xlabel('X [m]')\n", + "ax2.set_ylabel('Y [m]')\n", + "ax2.grid()\n", + "\n", + "plt.tight_layout()" ] }, { @@ -450,19 +452,19 @@ " \n", " \n", " 0\n", - " None\n", + " NaN\n", " Sand1\n", " LINESTRING (0.25633 264.86215, 10.59347 276.73...\n", " \n", " \n", " 1\n", - " None\n", + " NaN\n", " Ton\n", " LINESTRING (0.18819 495.78721, 8.84067 504.141...\n", " \n", " \n", " 2\n", - " None\n", + " NaN\n", " Ton\n", " LINESTRING (970.67663 833.05262, 959.37243 800...\n", " \n", @@ -471,10 +473,10 @@ "" ], "text/plain": [ - " id formation geometry\n", - "0 None Sand1 LINESTRING (0.25633 264.86215, 10.59347 276.73...\n", - "1 None Ton LINESTRING (0.18819 495.78721, 8.84067 504.141...\n", - "2 None Ton LINESTRING (970.67663 833.05262, 959.37243 800..." + " id formation geometry\n", + "0 NaN Sand1 LINESTRING (0.25633 264.86215, 10.59347 276.73...\n", + "1 NaN Ton LINESTRING (0.18819 495.78721, 8.84067 504.141...\n", + "2 NaN Ton LINESTRING (970.67663 833.05262, 959.37243 800..." ] }, "execution_count": 9, @@ -584,7 +586,9 @@ "source": [ "### Extracting the Coordinates to Point Objects\n", "\n", - "The resulting GeoDataFrame has now an additional ``X`` and ``Y`` column. These values represent the single vertices of each LineString. The geometry types of the shapely objects in the GeoDataFrame were converted from LineStrings to Points to match the X and Y column data. The ``id`` column was dropped by default. The index of the new GeoDataFrame was reset.\n" + "To make the coordinates easier accessible, we use the GemGIS function ``extract_xy`` to append the stored ``X`` and ``Y`` coordinate information to the GeoDataFrame. \n", + "\n", + "The resulting GeoDataFrame has now an additional ``X`` and ``Y`` column. These values represent the single vertices of each LineString. The geometry types of the Shapely objects in the GeoDataFrame were converted from LineStrings to Points to match the X and Y column data. The ``id`` column was dropped by default but can be kept if needed (``drop_id=False``). Information stored in additional columns, here the ``formation``, are populated to the respective points extracted from each LineString. The index of the new GeoDataFrame was reset. If the original index is needed, you can set ``reset_index=False``.\n" ] }, { @@ -705,7 +709,7 @@ "outputs": [ { "data": { - "image/png": "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\n", + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAnYAAAELCAYAAABUAMt7AAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjkuMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8hTgPZAAAACXBIWXMAAA9hAAAPYQGoP6dpAAB0nElEQVR4nO3dd1yT1/4H8E8SQhiyAkKCsgQHiHviqgMVV63aYautHb/aWm2rXdbeWldbre2trdXacVu1t1rb3qqtC8VRJ0NRVAQRlaHIkBk2IXl+f4SkgECeQMaTh+/79errXpPDk3PIw8k3Z3yPgGEYBoQQQgghxOoJLV0BQgghhBBiHBTYEUIIIYTwBAV2hBBCCCE8QYEdIYQQQghPUGBHCCGEEMITFNgRQgghhPAEBXaEEEIIITxhY+kKWAO1Wo179+7ByckJAoHA0tUhhNcYhkFpaSm8vb0hFNJ3T3OgPo4Q8zBH/0aBHQv37t2Dj4+PpatBSLty584ddO7c2dLVaBeojyPEvEzZv1Fgx4KTkxMAzRvh7OzcbDmlUokjR45gwoQJEIvF5qqeyfCpPXxqC8Cv9jRui0KhgI+Pj+7vjphee+zj+NQWgF/t4VNbgIbtqaysNHn/RoEdC9qpCWdnZ72dnoODA5ydnXlzM/KlPXxqC8Cv9jTXFpoSNJ/22MfxqS0Av9rDp7YATbfHlP0bLWAhhBBCCOEJCuwIIYQQQniCpmIJIYQQACo1g7i0QuSVVsHTyQ6DA6QQCa1rSUBcWiHyK2qttv6k7SiwI4QQ0u5FJmZj1b4kZJdU6R6Tu9hhxbQQRITKLVgzdo4m5wIAnt9+HtUqTTBnTfUnxkNTsYQQQtq1yMRsLPj5YoOgDgBySqqw4OeLiEzMtlDN2IlMzMaSXxMeeNxa6k+MiwI7Qggh7ZZKzWDVviQwTTynfWzVviSo1E2VsDxrrz8xPgrsiEF2xmbiUmaRpavRJolZJXj798uoVaktXRVCiIWdTy98YKSuPgZAdkkV4tIKzVcpA8Slsat/zO0C81WKWBQFdoS1zIIKrPzrGmZ8fQ6JWSWWrk6rVClVmPdjHH6Pv4tt59ItXR1CiIW99dtlVuXySpsPniyJbb0W7qAp2faCAjvC2vrD11GjUmNEkAd6ejefxJTL7MQivD2xOwDg30du4G5RhYVrRAixBO1mg+IqJavynk52pqxOq7GtV3GlktbbtRMU2BFWLmYWYf+VbAgEwHuTg636VIDHB/pgsL8UlUoVVvx5DQxDa08IaU9UagbrDl1nVVYAze7SwQFS01aqlQYHSCF3sQPbHpnW2/EfBXZEL4Zh8PGBZADAo/07I8RKR+u0hEIBPp4ZCrFIgGPX83AoMcfSVSKEmFFcWiFyFOynVldMC+FsPjiRUIAV00JYleX6ekFiHBTYEb2ikvNwIaMIdmIh3pzQ3dLVMYogTycseCgQALBq3zWUVddauEaEEHNhuy7N1UGMLXP7cz4PXESoHBue6Mu6PFfXCxLjoMCOtEilBj47kgoA+L8RXSBz4eY6k9Z4ZUwQ/NwdkKuoxudHbli6OoQQM0nPL2dVbvOT3A/qtMKDvViX5ep6QWIcFNiRFkXnCZBWUAGpoy1eeqiLpatjVHZiEdZMDwUAbDuXZrU7fQkh7EUmZmPD0dQWy2jX1Q0NdDdPpYxI5tz8ejuurxckxkGBHWlWeXUtIu9qbpHXxgbByU5s4RoZ36huHTG1txxqBvjX3kRaVEwIj2mT+bLB5XV1LXl3Ug8AeCC40/7bWttF2KPAjjRre3QmSpUC+Ert8dQQP0tXx2SWTw2Bk8QGl+8U45e4TEtXhxBiIvqS+WotDu9mNVOwjYUHe2HL3P4PLJuRudhZxXpB0nY2lq4A4aaaWjV+jtUEOa+NDYKtDX+/A3g52+HNCd2wcl8SPom8jok9ZejoJLF0tQghRsZ204C/h4OJa2JaEaFyjA+RIS6tEHmlVfB00ky/0khd+2DRT+tTp05h2rRp8Pb2hkAgwN69exs8zzAMPvjgA8jlctjb2yM8PBypqQ3XRhQWFmLOnDlwdnaGq6srXnjhBZSVlTUoc+XKFYwcORJ2dnbw8fHB+vXrTd00q3coMRv3y2rgLGYwqSf7RbnW6ukwf/Tq5ILSqlp8dIDdVA0h+lAfxy1sNw3wYXOBSChAWKA7pvfthLBAdwrq2hGLBnbl5eXo06cPNm/e3OTz69evx8aNG/HNN98gNjYWjo6OmDhxIqqq/vnWNWfOHFy7dg1RUVHYv38/Tp06hfnz5+ueVygUmDBhAvz8/BAfH49PP/0UK1euxHfffWfy9lmz7XXHbQ3zUvN6tE5LJBTgoxmhEAiAvQn3cPZmvqWrRHiA+jhuGeDnBqmjbbPPt5fNBSo1g+hbBfgzIQvRtwpobTHPWHQqdtKkSZg0aVKTzzEMgy+++ALvv/8+pk+fDgD46aef4OXlhb1792L27NlITk5GZGQkzp8/j4EDBwIAvvrqK0yePBmfffYZvL29sWPHDtTU1ODHH3+Era0tevbsiYSEBHz++ecNOkfyj6t3S3AxsxhikQDDvdrPH3zvzq54eqgfforOwPK9iTi0eCQkNiJLV4tYMerjuCMyMRur9iWhsLymyefby+YC7e+h/lpDuYsdVkwLofV3PMHZNXZpaWnIyclBeHi47jEXFxcMGTIE0dHRmD17NqKjo+Hq6qrr8AAgPDwcQqEQsbGxmDFjBqKjozFq1CjY2v7zLW3ixIn45JNPUFRUBDc3twdeu7q6GtXV1bp/KxQKAIBSqYRS2fy5gtrnWipjDbaevQ0AmBDsCWfbLKtvD8D+vVk8tgsOXc3G7fxyfH08FYvGBJqjegbjy70GPNgWPrSJDerjzOdoci6W/JoABoCk7ruaRMg0+F+Zsx3endQD47p7WF37AHbvTVO/BwAoKqvE4l/iseGJvgblwzMVa73PmlO/PeZoE2cDu5wczTFPXl4NbzIvLy/dczk5OfD09GzwvI2NDaRSaYMyAQEBD1xD+1xTnd7atWuxatWqBx4/cuQIHBz0L6qNiorSW4arypTAXwkiAAJ0RRYA625PY2zaMkkuwE+pImw+cRNOhSnoaG+GirUSH9+biooKC9fEPKiPM69PBjf9+JqB6rr/V46atHgcTDNblUxC33vT3O8BAOfab433WUuioqLM0r9xNrCzpGXLluGNN97Q/VuhUMDHxwcTJkyAs3Pz56QqlUpERUVh/PjxEIutM+fbt6fSUMukItTbGS/O6I+jR49adXu0DHlvJjEMbm6Px7lbhThR6oWtM/tDIODW1Awf7jWtxm3Rjh4R02lPfVxcWiGe337+gcclQgZrBqqx/IIQ1WoBfpw3yKrX1ul7b5r7PTTGhd+DNd5nLanfnsrKSpO/HmcDO5lMBgDIzc2FXP7PvH9ubi769u2rK5OXl9fg52pra1FYWKj7eZlMhtzc3AZltP/WlmlMIpFAInkw3YVYLGZ1k7EtxzUqNYNdF+4CAJ4Z5q+b2rHW9jSFbVs+mtEbE784hbO3CnAo6T6m9+1khtoZjo/vDV/aow/1ceaRX1GLalXzX8yq1QJUqwTIr6i1mja1pLn3Rt/voX45rvwerOk+Y0MsFqO21vTnknN2u2NAQABkMhmOHTume0yhUCA2NhZhYWEAgLCwMBQXFyM+Pl5X5vjx41Cr1RgyZIiuzKlTpxrMa0dFRaF79+5NTlG0Z6dS7+NuUSWc7WzwcB9vS1fHogI8HLFoTBAAYM3+ZJRU8GOtB+EO6uPMgwspTriwC5ULvwdiHhYdsSsrK8PNmzd1/05LS0NCQgKkUil8fX2xePFifPjhh+jatSsCAgKwfPlyeHt745FHHgEABAcHIyIiAi+++CK++eYbKJVKLFq0CLNnz4a3tyYweeqpp7Bq1Sq88MILWLp0KRITE/Hll19iw4YNlmgyp+2sS0g8a0Bn2IlFUCrVen6C3156qAv+TMjCrfvl+OTwdXw8o5elq0SsDPVxljc4QAq5ix1ySqrQVDhlihQnKjWjSw6cnl+BX+IykaP4Zxeqq70Yzw33x6KxXc22A5fN70HWDlK9tAcWDewuXLiAMWPG6P6tXfMxb948bNu2De+88w7Ky8sxf/58FBcXY8SIEYiMjISd3T/fKHbs2IFFixZh3LhxEAqFmDVrFjZu3Kh73sXFBUeOHMHChQsxYMAAeHh44IMPPqA0AI1kl1TiWLJm+mbOEF8L14YbJDYifDSjF2Z/F4OdsZmY1b8zBvjRCAhhj/o4bpg9yBcbjt5o9nljpjhpKp1IY8WVSmw4morvTt/G7IE+CA+RmfxkCJFQgBXTQrDg54sQAA2Cu/aS6qW9sGhgN3r0aDBM80PSAoEAq1evxurVq5stI5VKsXPnzhZfp3fv3jh9+nSr69ke7Iq7AzUDDAmQIsjTydLV4YyhXdzx2IDO+D3+Lt7bfRX7XxsBsYizKxgIx1AfZ1lsgqwNT/Q1Wv62g1ey8crOi6zLl1er8MPZdPxwNt0sueQiQuXYMrf/A78TGeWx4xXObp4g5lOrUmPXec007JyhfmZ//btFFYi5XYjkbAU8OkgQ4OGAUd06wsGWG7fnssnBOJqci5TcUvxwJg0vP8TN3HaEkH9EJmZjwc8Xm5x2BICFowOBihttztumnXY9fC0b26MzWn2d7JIqvPzzRSwJ72rSKVo6R5b/uPHJSSzq+PU85CqqIXW0xUQznQubmFWC/0Zn4OytfNwtenD7t4u9GHOG+GLeMH94OVt2Ma/U0Rb/mhKCt36/jC+O3sCUXnL4SK37kHBC+EylZrBqX1KzQZ0AwB8Xs/BGj7a9DpsRQUNtOJqKX+LuYOXDphtB054jS/iJ5pQIdtRtmnhsYGeTH6F1PUeB57edx9SvzuDXC3dwt6gSIqEA/Xxd8ewwfzzS1xs+UnuUVCrx9d+3MPKTE1h7MBkllZbdlTqrfycM7SJFlVKND/5MbHF6jRBiWXFphS0GWwzQYDNDa2hHBI0Z1GnlKDSjd2v2XaOzXInBaMSuncssqMCp1PsAgKcGm27ThErN4LtTt/F5VAqUKgZCATCtjzdm9u+MgX5ucJTYNCgblZSL70/fRnxGEb49dRu/XbiD18d1xdyhfrCxwBo3gUCADx/phclfnsaJlPs4eDUHU3rTehRCuCiv1PjBlpZKzSDmVgHe/eNqsyOCxmLO9XeEP2jErp375XwmGAYY2dUDfu6OJnmNm3llmP1dND6JvA6likF4sCeOvzkaX87uh4e6dWwQ1AGaaYKIUBn+93IYfnx2III8O6CoQomV+5Iw65to3MwrM0k99Qny7IAFozXr61btuwZFFeW2I4SLTJWLLTIxGyM+OY45P8Si2IyzCDklVVjw80VEJmab7TWJ9aLArh2rqVXjt/N3AABzhhh/00RJpRJr9ich4otTOJ9ehA4SG6x/tDe+f2Yg/D30B5ECgQBje3gh8vWR+PCRUDjZ2eDynWJM2Xga/zl9G2oLTE8sGB2IAA9H5JVW47PDKWZ/fUKIftqcbc1tBxAAkBmwdlelZvDl0VS8bKKpV32Yuv/e23MVNbXtO78o0Y8Cu3bs8LUcFJTXwMtZgvBgT/0/wBLDMPj1fCbGfvY3fjiThlo1g/BgLxx6fSQeH+hj8LmrNiIh5g71w5ElozCqW0dU16rx4YFkPPl9DHLbuE7GUHZiET56JBQA8N+YDCTcKTbr6xNC9NPmbAPwQHCn/fe7k9jtnIhMzMbwdcdazIOnjwDA1F5yuNq37XiswnIlhq49ZraROy6cmEEMR4FdO/ZzjGZr/uxBvkZbt1ZRU4tXf7mEpX9cRUF5DYI8O+Cn5wfjP/MGtnknqdzFHtufG4S1M3vB0VaE2LRCTNl4BjG3C4xSd7aGBXlgZr9OYBjgvd1XUauib9CEcI02Z5vMpeHInMzFDlvm9meV5kS7QSJHUd2mumx+qh82zemP+OXj8cuLQ/H8cH+4ObQuyCssrzHLtKx22vnJ72Pw+q4EPPl9DEZ8cpymg60AbZ5op27mlSI2rRBCATB7sI9RrpmeX46X/huPlNxS2AgFeCeiO54bHmDUhL4CgQBPDvZFWBd3vPxzPK7nlGLOf2LxzsTumD+qi8Gjga31rynBOJ6Sh6RsBbadS8f/jexiltclhLDXUs62+mfrNqWmVo339iS2aYNE400P2jQjYYHu+NeUEGw6frNVI4HaadmxPbxga2P88ZnmcgBq1/ptmdufNnJwGI3YtVPaFCfjgr0gd7Fv8/UupBfi4U1nkJJbio5OEuyaPxTzRwWa7JQGfw9H7HllOGb26wSVmsHaQ9fx/t5Es00VuHeQ4L1JwQCAfx+5gaziB3PxEUIsTxtMTe/bCWGB7qwS8UYmZmPo2qMoLK8x+PWkjmK8MNwfv7w4FGeWjm02ABIJBXg9vCu+mdsfchfDN3uYalq2pRyA2sdW7UuiaVkOo8CuHaqsUeGP+LsAjHMu7InreZj7QywUVbXo5+uK/a+OwEB/0x8kbW8rwr8f74NVD/eEQKAJVl/75RKqa1Umf21Ak/dvsL8UlUoV1h5MNstrEkJMSztaVVhu2K5XV3sxdvzfEJz/13gsn9aTdRAZESrHmaVjdVO0hjDFtCybHIDZJVWISys02msS46LArh3af+UeFFW18JHaY1TXjm261p8JWXjxpwuoUqoxpntH7Py/oWY9KUIgEGDeMH9serI/xCIBDlzNxgvbLqC8utYsr71quiao3H8lG0n3FCZ/TUKI6eg7saI5AgDrZvXC8CCPVh3NpR1V/GBaT3wztz+kjoatvzPmCBrbHICmzBVI2oYCu3ZIOw371GA/CNtwPuBv5+9g8a8JqFUzmN7XG989MxD2tqY9uaI5U3rLsfXZwXCwFeHMzXw8/UOsWdICBMudMbW3NwDg86jW75ojhFievtGqpsjrNmMYa81ZRKgcMcvCIXW0ZVVeO4K2M7b159TWxzYHoKlyBZK2o8CunUnMKkHCnWKIRQI8NrBzm67zr71XwTDAvDA/bHi8r8nW07E1oqsHfnlxKFzsxbiYWYxPD183y+suDu8KoQA4mpxL6U8IsWKGjkItCe/a4jq61rK1EeLjGaHN5uFryid1eTWPJue26bXZ5ACUu2g2oRBuosCundGO1kWEyuHRQdKqa5RVa1KaKFUMJvb0wsqHe7Zp5M+Y+vi44rPH+gAAvj+dhhMpeSZ/zcCOHTCzvyZI/vcRSlpMiDVSqRnkl7JLa+LuaItv5vbH6+HdWjX1yoY2XYuh07JLfk1o05o7NjkAV0wLMVm7SdtRYNeOlFYp8WdCFoC2bZpY8ec1pOWXQ+5ih09m9TZbihG2xod4YV6Y5iSNt367bJa1IK+P6woboQCnU/MRa+a8eoSQttHmbFtzQP8mKKmjGNHLxpkl3Yeh07JabV1zpy8HIKU64TYK7NqRvQn3UFGjQpBnBwxp5TD63ktZ+OPiXQgFwJez+8HVwbAOx1yWTQ5GD5kTCspr8OZvl01+/JiP1AFPDNLkA/z3kRtgGEoFQIg1OHItl9VRYYK6/z6e0cskueOaU39als1XaO2au5hbbfuCWX+37pez++pN30K4gwK7doJhGOyoO2lizhDfVo2yZRSU4197rgIAXhvXldNrLOzEImx6qh/sxEKcTs3H1ydvm/w1Xx3bFbY2QsSlF+J0ar7JX48Q0nZv/S+BVTlLjlY1N4LWkoU7254GpbkcgHTUGLdRYNdOXMwsxvWcUtiJhZjZz/BNE2o1g7d+v4zyGhUGB0jx6tiuJqilcQV5OmHNdM25rhtP3EJKsWmnjGUudnh6qGYK+N9HUmjUjhAO024yYBOTLJ8SbPHRKu0I2vIpwazKF1cqTXL0GB01xn0U2LUTO+q2wk/r7Q2XVpxRuDMuE+fTi+BgK8Lnj/exmoWzjw30wexBPmAYYHuq0OBUBoZaMDoQ9mIRLt8twdFk02/cIKQ9a+3IkUrNYO1B9rvmPZwknOjzREIBnh0e0OKu1foYACv/uma0ETVt8ubG/aj2qDEK7riBArt2oLiiBvuvaP7g5tSNKBkiV1GFTw5pOsG3J3ZHZzcHo9bP1FY+3BPBMieU1wrw+q+XTXoyhUcHCZ6ryx7/7yMpJl/bR0h71ZaRo03HU5FrwKYqLuVsq79rlY0cRTU2Hb/Z5telo8asBwV27cD/4u+iplaNnt7O6NPZxeCf//hQCkqra9HXxxXPhPkbv4ImZicW4asn+8BexODSnRK8vyfRpNOk80d1gZPEBtdzSrHvyj2TvQ4h7VVbRo4iE7Ox4Wgq69fiYs427Zo7V3t2sy8bjt5o82gaHTVmPSiw4zmGYbCzLnfdnCF+Bm+auF4swMHEXAgFwEczQjkxHdEaflIHzOuqhlAA/B5/Fz+cSTPZa7k62OLl0YEAgE8Pp5jt7FpC2oO2jBxpf9YQXM3ZFhEqx+Y5/VmXb+toGh011jIubSihwI7nzt0qwO38cnSQ2GB6X2+DfrZaqcLvaZpb5NlhAejpbfhoH5cEuzFYNqk7AODjg8km/Wb53HB/eDpJcLeoEj/HZJrsdQhpb9oycmTIkWFCAfD1U9zO2Ta0izvkLHfKtnU0jY4aax7XNpRQYMdzP0WnAwBm9OsER4mNQT/7zak05FcJ4OUkwZLx3N8Fy8a8ob6Y2b8T1AywfG8ilCrTnCfrYGuDJeO7AdCs51FUKU3yOoS0N20ZOcpRsB9N2vRkP0zuzd2gDmjFeruSyla/Fh011jQubiihwI7HsoorEZWk2dI/b5hhmyZu5pXh29Oa6cr3p/SAk53hO2m5SCAQYPmUELg5iJGSW4rt59JN9lqPDeiMwI6OKKpQ4tuTt0z2OoS0J60dOYpMzMaa/ddY/eyS8G6Y3NuwGQ5LiQiVY0k4uy/eaw4ktzrQoKPGHsTVDSUU2PHYjpgMqBlgeJA7gjydWP8cwzB4b89VKFUMerqpMTHE04S1ND83R1ssjegBANgQdQM5JkqBYiMS6l7nhzNpJnsda6BSM7iRW2rpahAeaM3IkXZUpbBc/8i5zFmCRWODjFNZM1k0tiu8WAS8ReU1bRpFoqPGGmK7LCA+o8h8lQIFdrxVpVRh1/k7AGDwTtbf4+8iLq0Q9mIhHg1Qc+4sWGN4fKAP+vm6orxGhY8O6j8fsrXGh3hhgJ8bqpRqfHH0hsleh+tibxdgwoZTeOLbaEtXhVg5Q0eOWhpVafyzAmjSI1nbqJNIKMCyyT30ljPGKBIdNfYPtssC8suqTVyThiiw46n9V7JRWF6DTq72GNeD/Yhbflk1Pq4LdF4fFwSpxFQ1tCyhUIA100MhFAD7Lt/D2ZumOQJMIBDgvboO97cLd5DaTkettGlfunR0tHBNCB8YMnLEdsOE1NHWqkedwoO9AABuehLQGyMtSXNHjbU3bJcFeHQw7wcpBXY8xDCMbu3YnKG+sBGxf5tX7UtCcYUSwXJnzBvqa6IackNoJxfdEWDv701EldI0aUkG+EkxIcQLagZYfzjFJK/BZTW1ahxKzAGgOfmEEGNgO3LEdsPE+1OCrTaoq29pBLsjx9qykYJoDPBzg9Sx+UBauyxggJ+b+SoFjgd2KpUKy5cvR0BAAOzt7REYGIg1a9Y0SC7LMAw++OADyOVy2NvbIzw8HKmpDZNPFhYWYs6cOXB2doarqyteeOEFlJWVmbs5ZpNwpxhXs0pgayPE7EHsg7OopFzsu3wPIqEAnz7a26CA0Fq9ObE7PJ0kSMsvx+YTbc/O3px3InpAJBQgKikXF9LbVwLPszfzUVyhhEcHCYZ0cbd0dTiF+ri20TdyZMiGCZmLvSmqaHaeTuxGh9qykYJo7q2HPj3R7LpNS24o4fQn9yeffIItW7Zg06ZNSE5OxieffIL169fjq6++0pVZv349Nm7ciG+++QaxsbFwdHTExIkTUVX1z7e0OXPm4Nq1a4iKisL+/ftx6tQpzJ8/3xJNMoufojXnwj7cxxtSR1tWP1NSqcT7e68CAF4c2QWhnaw7Zx1bznZirHq4JwDgm5O3TLbAP8izAx4f6ANAk0PPlCdfcI12GnZKL1m7nbJpDvVxpsN2wwTf0nQM8HNjdZZsWzdStGfNpTipz5IbSjgd2J07dw7Tp0/HlClT4O/vj0cffRQTJkxAXFwcAM032S+++ALvv/8+pk+fjt69e+Onn37CvXv3sHfvXgBAcnIyIiMj8Z///AdDhgzBiBEj8NVXX2HXrl24d49/xz3dL63G/roP0nkGbJpYdygZuYpqBHg4YjHLrfN8EREqQ3iwJ5QqBst2XzXZ+a6Lw7vCTizExcxiHL6Wa5LX4JoqpQpH6to6tQ9NwzZGfZxpsN0wocWnNB1sc9uZMh0Hl05hMDY295a7oy1Ovj3GYlP7hmWsNbNhw4bhu+++w40bN9CtWzdcvnwZZ86cweeffw4ASEtLQ05ODsLDw3U/4+LigiFDhiA6OhqzZ89GdHQ0XF1dMXDgQF2Z8PBwCIVCxMbGYsaMGQ+8bnV1Naqr/9nFolAoAABKpRJKZfPf/rTPtVTG1HbEpEOpYtDXxwU9vBxY1SU2rRC/xGl20H44PRgiqKFUqjnRHmPR15blk7vj3K0CxGcU4eeYNDw5yMfodZDai/DcMD9sOZmG9ZHJeCjIrdXT3dby3kRdy0VZdS1kzhL0lndosr6N28L1NhkT9XGmEZdWiMKySkhEzZeRCDUfzZ8/1gvjuntwvk361H9vxnX3wNdP9cGqfddQVNFyuwrLKhFzM89oI5ZHk3Ox7tD1BmsbZc52eHdSD90GD324fJ+xubfKqqpx/vZ93e+0fnvM0SZOB3bvvvsuFAoFevToAZFIBJVKhY8++ghz5swBAOTkaBZke3k1vFm8vLx0z+Xk5MDTs+GuUBsbG0ilUl2ZxtauXYtVq1Y98PiRI0fg4OCgt95RUVH6G2cCKjWw9ZIIgAC9JIU4ePCg/p9hgPWXNT8z3EuN/KQYHGx0lKKl2mMKLbVlorcAe9JFWHsgCci6Chd2s9gG8a8FHG1EuJ1fgZU/HcYwr7Z9k+X6e7MlSQhAiJ4dKhEZeajFstq2VFRUmKFm3EB9nOmsH8yunDIjAQczEkxaF3Oq/94sC2X3M/nJMTBm1qc3Hsi8Uo6atHgcNPCIbq7eZ2zuraZ+p1FRUWbp3zgd2P3222/YsWMHdu7ciZ49eyIhIQGLFy+Gt7c35s2bZ7LXXbZsGd544w3dvxUKBXx8fDBhwgQ4Ozs3+3NKpRJRUVEYP348xGLzn9RwKDEHJbFX4O5oi6VzRkFio3806KeYTORUXoebgxhfPD8CrvW2ylu6PcbEpi0T1QxSv41F4j0FYqo74ctH+pikLqUdM/DhwRQcz7PHe3NGwMHW8D9Da3hv7hRVICXmDABg2ROj4CdtOmBo3Bbt6FF7QH2c8R1NzmU1UiURMlgzUM3pthiiqfcmLq0Qz28/r/dn3RxssWJaCOsRtaao1AwmfnGq2V3IAgBeznY4vHiU3mlvLt9nbH+nP84b1GDETtueykrT70bmdGD39ttv491338Xs2bMBAL169UJGRgbWrl2LefPmQSaTAQByc3Mhl/8zl52bm4u+ffsCAGQyGfLy8hpct7a2FoWFhbqfb0wikUAieXBnkVgsZnWTsS1nbDvi7gIAnhriiw72+ndGFZbX4Mtjmp2gb03sjo4uTX/wWqo9ptBSW8QA1s3qjYc3ncHBxFw8NrAIYwzIAcjW08MCsD0mE3cKK/FTzF28Oq71axq5/N78cSkbDAOMCPJAkJf+zTjatnC1PaZAfZxxRSZm45Wdl+vWPzUfPAigmR4Eyjnbltaq356hQZ6QdrBHTklVi2vCckuVeGXn5TYt9r9wqwAZRdVo6feeUVSNS3dLERbIbnc8F98bfb9TATQbJ4YGeT4QwIrFYtTW1pq8jpzePFFRUQGhsGEVRSIR1GrNwe0BAQGQyWQ4duyY7nmFQoHY2FiEhYUBAMLCwlBcXIz4+HhdmePHj0OtVmPIkCFmaIV5JGcrEJtWCJFQgDlD2J0L+9mRFCiqahEidzYoLQqfhXZywfPDAwBocttV1Bj/j1BiI8JbE7oDAL49dRuF5TVGfw1LU6rU+O2C5ovGk4Pp3moO9XHGY8gJEwDw7iT9JzVYO3NupGB7CgPbclxlDWfmcjqwmzZtGj766CMcOHAA6enp2LNnDz7//HPdYmCBQIDFixfjww8/xF9//YWrV6/imWeegbe3Nx555BEAQHBwMCIiIvDiiy8iLi4OZ8+exaJFizB79mx4e/Nnl542xUlET9kD2dibkpJTil/iMgFY5xE6prRkfDd0crVHVnEl1keaJqHwtN7eCJE7o6y6Flv+Nl3+PEs5lpyH+6XV8Ohgi/EhrZ/e4Tvq44zH0BMm2jLtaE20p3S0lEgX+OdEiphbBa16HbanMLAtx0Xa3b7VtWosDu8KL+eGo95cOTOX01OxX331FZYvX45XXnkFeXl58Pb2xksvvYQPPvhAV+add95BeXk55s+fj+LiYowYMQKRkZGws/vn5tmxYwcWLVqEcePGQSgUYtasWdi4caMlmmQSJRVK7L2UBQB4JozdaN3Ws2lgGE0gyJf8TcbiKLHBRzNC8ezW89h2Lh1je3hiVLeORn0NoVCAdyK649mt57E9OgPPDQ+Atys/EqQCwM66Lw2PDvCBLYu1nu0V9XEtU6kZxKUVIq+0Cp5OmlxzzX0JZTsSpD1hgos7Lk0lIlSOSqUaS35N0Ft24c6LWDerl8HByeAAKeQudnqnKK318yYyMRur9iU1+PIgc7bDkvBu8Pdw0Ht/mhOnAzsnJyd88cUX+OKLL5otIxAIsHr1aqxevbrZMlKpFDt37jRBDbnh9/g7qFSq0EPmxOqPpriiBnsTNIHgCyMDTF09qzS6uyeeHuqH/8Zk4K3fL+Pw4lFwY5nsma2HunXE4AAp4tIKsfFYKtbN6m3U61vKncIKnE69DwCYbYK0MXxCfVzzmvoglbvYYcW0kCaDjvT8clbX5csJE4bSrCnUr7hSiQU/XzR45Ek7Rbng54sQAA2CO65MUbaWNiFx44A1V1GFL47ewJa5/VmvGzQH+ipt5dRqBv+N0UzDPhPmD4FA/x/N7xfuokqpRrDcGQPNfIadNXlvcjC6dHREXmk13ttz1einRQgEAiyN0Ky1++3CHdzM48cRUL+ev6PbNOHv4Wjp6hAr1Fxm/5ySqiZPS4hMzMaGow2PWWuMbydMGEo7osY2rGrNejvttG/j5UAyFztsfqo/XOxtrS5pcUtrN02Z5LktOD1iR/Q7eeM+Mgoq4GRng0f66V9PUz8QnBfmxyoQbK/sbUX48ol+mPH1WRxKzMEfF7Pw6IDORn2NAX5ShAd74WhyLj6PSsHXcwYY9frmVqtS49cLmmTXtGmCtIa+D1IBNB+k40M0R9Rpy7NhrSNGxlB/RE0f7Xq7uLRCg0eiIkLlGB8iazCFXlRejTUH2I++com+tZtt+V2ZCo3YWTltkPb4QB9W+dBOpt5HZmEFnO1sML1vJ1NXz+r16uyCJeO7AdCc8VpSafx1OW9P7A6BADh4NQdX7hYb/frmdDo1H/dLqyF1pE0TpHUM+SBlU15rcXg3zgcRpqYdUXO1Z5dCpLU7WEVCAcIC3TG9byeUVNZg4c5LrEdfucYad/tSYGfF8hRV+DtFk7/qqSHsRkcOXdX8Ec3o1wn2ti2ciUJ05o/qgsCOjigsr8HmE8bfwdpd5oQZdUH2p4dNswvXXP64qElx8nAfb9o0QVrF0A9StuX9PfSfqNEeRITKsXlOf1Zl80ur2zTFaI3TmI1Z425f6nmt2J5LWVAzwAA/NwR27KC3vFrN4Ph1zaL28SFNJy4lDxKLhHh/qiZv0dazaawXaRtiyfhuEIsEOJ2aj3M3841+fXMoqVTiSFIuAGBWf+NOWZP2w9APUmv84LW0oV3cWa23W3MgGSM+Od7qUTVDR1+5SN/aRC6u3aTAzkoxDIPf4zWjI2zXfV3JKkF+WTU6SGw4dRNagzHdNSlPlCoGaw8Z8VDFOj5SB92atA1Hbxh9o4Y5HLqajZpaNbp6dkBop+aPpSKkJYZ+kBaVV6OlZXNc/OC1tJaS7DbWlilTa5zGbMwaEhI3RoGdlbp8twQ388pgJxZiSm9260aOJWtGU0Z186BpslZ4f0owREIBDl/Lxblbxh9Ve2V0EGxFQpxPL0L07dYlCbWk3Rc1KXRm9u9Mm3JIqxnyQRqZmI2FOy9B30we1z54uaC5HayNtWXKlA+jqSo1Axd7Wzw/3B9ujZI8cyUhcWP06W6l/hev2XkY0VMGZzt2C2GPJWvW443rQYvaW6OblxOeqhtV+3B/stHXhchc7DB7sCbv25d6UjdwzZ3CCsSlF0Ig0KzfJKQtWkqbof0gZXOEmFAAbH6Kex+8XBERKseZpWOxfEpwi+VaO2VqjdOY9UUmZmPEJ8fx5Pcx+OFsOgrLlZA6aoK8X14cijNLx3Ly3qJ0J1aoSqnCXwn3AGgy+7Nxr7gSSdkKCATA6O7GPUWhPVkyvhv2JmQhKVuB3y7cMXpKjwWjA7Er7g5i0woRc7sAQ7twY/u8PnvqTj4ZEeTB6kg7QvRpKm1G/cz+bHbDqhkYPbE434iEAng4SfQXhOFTptactLi5pMRF5TXYejadM6dMNIVG7KxQVFIuFFW18HaxwzCWeXOOXMsBAPTzcYV7B3Z/xORBUkdbLAnXpD/59HAKSiqMm/5E7mKPxwZq1kxuPGYdo3YMw2B33W7Ymf1ptI4YT/20GWGB7g0+SPmwfosr2E6FpudXGHxtNqOvXGPtu3lpxM4K/a9u08SsAZ0hZPmNYW/dCN/U3u3nUHBTeTrMD7/EZSI1rwwbjt7Ayod7GvX6r4wJwm8X7uDcrQKcTy/EIH9uTlNoxWcUIb2gAg62IkzsSbutiXnwYf0WV+g751Xri6M30F3WweBgrLnRVwCIvlXA6ixgc7LGpMT10YidlckpqdKdw8l2N2xGQTkS7hRDKACm9uHetyNrIxYJdcHcf2MykJJTatTrd3K1102xW8Nauy1/3wIATO0tZ5Ukm5CmqNQMom8VsD5yanCAtMXzT7m+fotLtFOmbMafWjtS1Xj0NSopR7d+7fVdCXjy+5g2pVYxJmsfDabAzsrsvnQXagYY7C+Fnzu7czi16/GGB3nQt1cjGR7kgUmhMqjUDD48wO44I0O8MjoQNkIBztzMR3wGd3M8JWaV4Nj1PAgFwMsPBVq6OsRK1V+kzvZDPiopB1W1qiaf4/r6LS6KCJVjSXjXFssYK++cvrOAj9ZlcLAUax8NpsDOijAMg/9dMCx3HcMw2JugWdhOR4gZ13uTg2ErEuJ0aj5O3bhv1Gv7SB10SX6/PGb80y6M5avjmhHFaX280YVFkmxCGtP3Id9UcKf9meJm1ri6Oog5u36Ly/w92A0W5JRUtvo12KxfW3foequvbwwD/NwgdWw+2wTXR4NZzZsoFAqDL+zsTAlKje1iZjFu55fDXizCZJa5667dU+DW/XJIbISY2JPSnBiTj9QBc4f64cezaVh36DpGBHmwXvPIxsIxQfjfxbs4deM+Eu4Uo6+Pq9GubQzXcxQ4fC0XAgGwaEyQpavTJtTHWYa+D3kBNFN/40NkupE3NmlOJDZCOl2nFdiOQK05kAx7W1GrAmc269dyFJab4oxMzMaqfUkoLG/6S4M1jAazGrFzdXWFm5sb6/+kUilu375t6rq3O9rcdZN6ydBBwm4t01+XNdOw4cFecGKZ746w9+rYIDjZ2SApW4E/L2cZ9dq+7g54pG6U9WsTnFHbVl8d19RpcqgcXb2cLFybtqE+zjJac+QUmzQnOYpqTh9TxVX68s5pFZXXmPw0CktobvS4Pi7v5tVivdL5f//7H6RS/cOODMNg8uTJbaoUedD90mpdZv8nBrLLXccwDA7V/eFNo00TJuHmaIsFowOxPjIFnx2+gUmhctiJRUa7/oLRXbD70l0cScpFam4pZwKom3mlOHhVc28tGmvdo3Va1MeZX2sWqVv7wnYuq593riXNjaaywdV1aWxGgt0dbXHy7TGcP7mJVWDn5+eHUaNGwd2d3bbeLl26QCym0SFj+uFMGqpr1ejn68p6Xj8ltxR3CishsRFiVDdKSmwqzw8PwE/nMpBVXImtZ9OxYLTxNhEEeTphYogMkddysOXvW/j8ib5Gu3ZbbDp+EwwDTAjxQrDc+qckqY+zjNYsUrf2he1cp807996eq81ORwKtT/nBJrWK1MEWQOvX8bUGm5HggvIaxGcUcTLFSX2sws60tDTWHR4AJCYmwseH3agS0a+kQomfYzIAAAtHB7E+hzPqmmZn0YggD0pDYUJ2YhHemtgdgGYzQXYbFhY35ZUxmkDxz8v3cKfQ8AShxlZcUYMDdaN1r45teRedtaA+zjJac+QUpTkxvYhQOZZPZZefMyopx6Brt3QWsFZhRQ0AmHV3LJ9Ggrk9nkgAANuj01FWXYseMieMC/Zk/XNRdX8UE2jThMnN7NcJA/zcUFGjwkcHko167d6dXTGyqwdUagbfnbL8uq7IxBwoVQx6yJzQq7OLpatDrFhLH/LNLVKnNCfm0VLwXN+PZ9MNXmvX3GkUjS35NcFsee34NBLcqmGc8+fP48SJE8jLy4NarW7w3Oeff26UihGN8upa/Hg2DYDmRAK2o3XZJZW4crcEAgEwtgcFdqYmFAqwenpPTPvqDPZfycaTg/MxPMjDaNdfMDoQp1Pz8euFO3h1XBDc7Iy3js9Q+65oNuRM68PfU0yojzMf7Yf8qn1JDabCZC52WDEtpMEi9ebO79RydRBj7cxenF7Ybi20o6n6picBYOVf1wxeaxcRKsfYHl4YuvZoi1O+rVnH1xpF5TUQCjTnCzdFAM09aQ0jwQYHdh9//DHef/99dO/eHV5eXg0CDbZBB2Hvl7hMFFco4e/ugCm92HdWR5M0o3X9fd3QkeUBz6Rtenq74OmhftgenYGVf13DoddHwkZknEHxsC7u6OvjioQ7xfjP6TS8Pd4yGxbySqsQfasAADCNp8fTUR9nfs0dOVX/w5zSnJiXdjT1ZT0bKQDNLuRNx2/idT0JjhuLzygyyTo+Q0UmZmPhzua/MGhZy0iwwYHdl19+iR9//BHPPvusCapD6qtSqnRTbwtGBxp0Qx2pC+zGh9BonTm9MaE7/rx8D6l5Zdh9MQuPDzLOOiyBQIDXxgXh+W0XsO1cOuYMskyy6UNXc6BmgD4+rvB1d7BIHUyN+jjL0B451RxD0pxwfXG7tYgIleOF4f744Wy63rIbWnGOLBfWtbH5wiAUAJue7Gc1I8EGDycIhUIMHz7cFHUhjfxx8S7ySqshd7HDjH7sTpoAgJJKpW5UZQIFdmblYi/WJevdcPQGqpRNrwVqjTHdPTE4QIqaWjW+OH7LaNdli2EY7L6oOfnkYR5Pw1IfZ1qGngmrxYUgoD0KN2AE1NBzZNmuV8svrW7V+bRssPnCoGYAN0frmfkyOLBbsmQJNm/ebIq6kHpqVWp8c1Lz4T1/VBeD8ub8nZKHWjWDrp4d6JgnC5g71A/edWtTtp9LN9p1BQIB3pscDADYm3APWeVGuzQr+65k4/LdEtiJhZjG8uQTa0R9nOm05kxYLT4tbrcm2rV2bBh6jizbhMhrDiSzvk8MoVIzOHuT3XGQ1vSFweDA7q233kJKSgoCAwMxbdo0zJw5s8F/xDj2XbmHO4WVcHe0xexBvgb97OFrmu3ntBvWMuzEIiwZ3w0A8P3pNFQ3s4OvNfr6uGJKLzkYBtiXab5N7RU1tfi4brfvK6OD4Mlyx5w1oj7ONFpzJmx9rUmNQtqu/s5lNgwJgNikPtFie5+wpf2SsekEu9kPa/rCYPAnw2uvvYYTJ06gW7ducHd3h4uLS4P/SNup1Qy+rrvZnh8RAHtb9jsgq5Qq/J2i+QYysSctIraUR/p1gszZDvll1ThwxbjfMt+e2B02QgGSi4WIvl1g1Gs3Z/OJm8hRVKGzmz3mj+pilte0FOrjjI/Nwe9spvFmD/Jt8hqU5sS0IkLlWMJyY4SHgVOWbFOfGHKf6MPm6DAta/zCYPDmie3bt+OPP/7AlClTTFEfAs3Gh9S8MjjZ2eDpMD+DfvbszXxU1Kggd7FDr070IWQpYpEQT4f54dPDKdh6Nh0z+nUy2o5Kfw9HPDmoM/4bewfrD6diRFcvCE34YZZRUI7vT2lS7iyfGmLUI9O4iPo44zPkTNimNj5oD2Zv7hpNpUYhxrVobFf8EncHOYqWg6E3f7+MlQ8b9l5od0VvO5uGNS3kATXGLlk2myW0rPULg8EjdlKpFIGBxjsyiTRUWqXEhweSAADzwvzhbGfYsUVH6k6bmBDiRakZLOypwb6Q2AhxNasE8RlFRr32wtFdIBExSLynwI7YDKNeu7E1+5NRo1JjZFePdrEZh/o449MXDGg1NY2nb3RlSXhXnFk6loI6ExMJBVj5cAgEaHnaNFfRuilTkVAAD5apudjeT01hs1lCS+Zihy1z+1vdvWVwYLdy5UqsWLECFRWWP9qIj1b8dQ13iyo1U14PGTblpVIzuiNYJtA0rMW5OdpiRj9NWpKtLNIFGMK9gwRTfTSJc9ceum6yo8YupBfiaHIubOrWwrSHLwvUxxlXZGI21uy/xqps43VMbEZXdp2/04baEUNop029nJsPwNoyZcp2Hdua/ddavdaO7RrARWOCrPYLg8GB3caNG3Ho0CF4eXmhV69e6N+/f4P/jC0rKwtz586Fu7s77O3t0atXL1y4cEH3PMMw+OCDDyCXy2Fvb4/w8HCkpqY2uEZhYSHmzJkDZ2dnuLq64oUXXkBZWZnR69pW+y7fw+6LWRAKgC+e6GvwaF3CnWIUlNfAxV5sVesB+OzZ4f4AgMhrOcgqNu4ZsiNkDAb6uaKiRoWlf1wBwxg/HcDG4zcBAI8N7IwgTyejX5+LqI8zHu1oW0tJaIHm1zGxGV0xdCcmaZuIUDn+/XjfFsvUnzI1BNtdsoXlylZvpGAbPA4P8rCq6df6DF5j98gjj5igGk0rKirC8OHDMWbMGBw6dAgdO3ZEamoq3NzcdGXWr1+PjRs3Yvv27QgICMDy5csxceJEJCUlwc5O8wbOmTMH2dnZiIqKglKpxHPPPYf58+dj586dZmuLPlnFlfjXnqsANN8UBvobHpjFZ2j+iIYESCE20okHpG16yJwxLNAd524V4LuTt7BqeqjRri0UAOtmhGLq5nM4d6sAO+MyMWeIYWsyW5JwpxinbtyHSCjAgocsc9KFJVAfZxxs1zK1tI6J7ZRbW6bmiOHyy6pZlYtKyjFoLZx2l+ziX+JZlW/NcWN8OjqsOQYHditWrDBFPZr0ySefwMfHB1u3btU9FhAQoPv/DMPgiy++wPvvv4/p06cDAH766Sd4eXlh7969mD17NpKTkxEZGYnz589j4MCBAICvvvoKkydPxmeffQZvb8snWmUYBm//fhmKqlr09XHFq+MMO5ZF62JGMQCgv59bywWJWS0aG4RztwrwS9wdLBgdpHf3lyH83B3wzsQeWL0/CR8fSMbwQA/4ezga5dpfHdOMCs3o14m3p0w0hfo442C7lknqaIuPZoQ2OeVVyDKAYFuOGAfbUa8fz6ZjcIDU4I0UG57oi5q0loM77ahgzO0C1udy8+3osOYYHNiZ019//YWJEyfisccew8mTJ9GpUye88sorePHFFwEAaWlpyMnJQXh4uO5nXFxcMGTIEERHR2P27NmIjo6Gq6urrsMDgPDwcAiFQsTGxmLGjBkPvG51dTWqq//pKBQKBQBAqVRCqWx+SkH7XEtlmnI85T7O3SqAxEaIz2aFAmoVlGrDcp+p1Axi0zSpL3p7Oxlch6a0tj1cZMm2DPRxxiB/N5xPL8Lm4zfwwdTgNl+zfnvmDOqEg1fv4UJGMZ7dGodfXxwMqaNtm65/7Z4Cx67nQSgAXhrpZ9LfW+P3hg/3G1t87uPySsohEelfHvD+pG4Y192jyWtK7UWsriG1Fxl83/DtfjNne/p1doKfm0TvSKkAwNoD1zC6q7tBgdJDQVJEpQESof73/o1f4rFyek+EB7e8sUulZrD2wDXYtnA/CQXAZ4/2afZ+bK3674053h9WgZ1UKsWNGzfg4cEuKvb19cXp06fh59e2aaHbt29jy5YteOONN/Dee+/h/PnzeO2112Bra4t58+YhJ0eTiNfLq+Eb6uXlpXsuJycHnp6eDZ63sbGBVCrVlWls7dq1WLVq1QOPHzlyBA4O+kcuoqKiWLUP0AwHr78iAiDACM9aXIv9G+yWGTd0swQoqrCBgw2DnMRoHExqxUWaYUh7uM5SbRniIMB5iLAzLhNByjS4Gul0Gm17pnsAt3NESC+owBObTmBhiAoGpD9soFYNbEkWAhCin7saSbEnYcTbqVnatlhi0wL1cRrG7uPWD2ZRKCsBB7MSmnxKaIRr6MOn/g0wX3ve6MG2ZDkORx5q1WusGahmUUqFmrR4HEzTX5JNnWszLuKgiRINREVFmaV/YxXYFRcX49ChQ6yTcxYUFEClanu2fbVajYEDB+Ljjz8GAPTr1w+JiYn45ptvMG/evDZfvznLli3DG2+8ofu3QqGAj48PJkyYAGdn52Z/TqlUIioqCuPHj4dYzG7jw5+Xs5EdcxXOdjb45NmRcLE3bMOE1uoD1wFkIqJXJ0ybapx1XK1pD1dZui0MwyD2xws4n16EDLtAPDWpe5uu11R7Bg4rwxPfxyG9rBZRZd7Y+EQfg6cTqmvVeHVXAm4q8mEnFuLjp4ajS0fjTO02p3FbtKNH5kR9nPH7OJWawcQvTiFXUdVsUmEvZzscXjyq2fv0yLUcvPW/y82uhwIAmZ5rNMfSfYKxWaI96w9dx08s0i09PcQPSyexjgR1bdl0wwF3iqtZ5ZxztRfjs0f7YFDd2rj4jCLkl1Ujo6Ac/4u/i9xSdtP162f1xuRext0JW/+9qaw07ia6prCeijVlJ9McuVyOkJCGR5kEBwfjjz/+AADIZJqUHrm5uZDL/3kjcnNz0bdvX12ZvLy8Bteora1FYWGh7ucbk0gkkEgeHFIRi8Ws/mDYlqupVePLul2HLz0UCA/n1q1jUqsZRCVp2ji5l7fR/6jZtscaWLItr4wJwnNbz+PXC3fxenh3uDi0vR712xPcyQ3fPzMQT/8QhyNJeVj+VzLenxLC+nVqatVY/NtlnEjJh8RGiB/mDUJ3b9c215EtbVss9f5QH2fcPk4MYNmUnljw80UAaPDhrA3Blk3pCTtJ08sGIhOzsWjXFTAt7JEU6LkGG3zq3wDztmdsT298fy5Tb7n/nMvEwC4eBqcOeWNiMF7ZeZlVYJdbVount8XDta6/K65oPOXJLvD3dHE02e9PLBajtrbWJNeuj9XWSbVabfB/Xbq0/dih4cOHIyUlpcFjN27c0E1/BAQEQCaT4dixY7rnFQoFYmNjERYWBgAICwtDcXEx4uP/WYh5/PhxqNVqDBkypM11bItd5zNxp7ASHZ0keK4uLUZrXMkqQY6iCo62Iozoym4qiZjf6G4d0UPmhPIaFf4bk26S1xjSxR2fPd4HAPB7/F2MXH8c35y8hSply6NL+WXVWLTzIo4m58LWRoj/zBvIekEyH1AfZxrNHRelL/Ermx21QgGw+SnrSx7LJ9r0JGy8u/uqwXntwoO9sGVuf7gaMJNVXKFsIqjTzxqPDmsOpzdPLFmyBMOGDcPHH3+Mxx9/HHFxcfjuu+/w3XffAQAEAgEWL16MDz/8EF27dtWlAvD29talLAgODkZERARefPFFfPPNN1AqlVi0aBFmz55t0R2x5dW12HhMM1r32riucLBt/VtxqC6Xz5genrw/7smaCQQCvPxQIBb/moCtZ9PxfyO7mOT9eriPN5zsbLD2YDJu5JZh3aHr2Ho2Dc+E+aNXJxf0kDuhYwcJGAaIvl2AnbGZOJKUA6WKga2NEN8/MxAju3Y0er3Ig/jcx2lpj4uKSytEXmkVPJ00H54tTZ2y2VGrZjRJwInlaNOTvFw3KtuS4golNh2/iddZnjmrFREqh5NEjDk/xLa2mnpZ69FhzeF0YDdo0CDs2bMHy5Ytw+rVqxEQEIAvvvgCc+bM0ZV55513UF5ejvnz56O4uBgjRoxAZGSkLr8TAOzYsQOLFi3CuHHjIBQKMWvWLGzcuNESTdLZejYN+WXV8HN3wOxBPq2+jlrNYP9lTWA3ib65ct7U3nJ8ejgFWcWV+OPiXaPmnatvTHdPjOraEXsvZeHzqBvIKq7Ep4f/GRlyd7SFxEaIe/U+PPv4uGJpRHcMC2w/I3WWxuc+rj6RUGBQPjO2pwOwLUdMJyJUjheG++MHFqfrbD2XhkVjgwwOnoYGukPuYoeckqbXa7YV384a5nRgBwBTp07F1KlTm31eIBBg9erVWL16dbNlpFIpp5IRF5XX4NuTtwEAb4zv1qZkwvGZRcgqrkQHiQ3GBXvq/wFiUTYiIV4YEYDV+5Pwn9NpmD3I12TfEEVCAWYN6IypfeT47cJdxNwqQHKOAmn55SgorwEAOEls8Ei/Tpg92Ac9vdltHCDGxZc+TqVmDBqVawnbPGlsyxHTCg+RsQrsiiuUiEsrNCjIB/4ZGVzw80UIAKMGd8unBOPZ4QG8GKnTYh3Y3bt3jxPD+nyw5eQtlFbXIljujGm92/Y7/TMhCwAwsaeMpmGtxBODfPDlsVSk5ZfjUGI2prbxHtBHYiPC00P98PRQzehgZY0KqXmlKCirwZAu0jYtA+AT6uNaLzIxG6v2JTWYPpW3YRRkcIAUMme7ZvOk8eF0AD4ZHCCFq70YxZUs8hu2cpRVu16z8X3WWtp7iG9BHWDAWbE9e/a0+DdCPsguqcS2c+kAgHciukPYhhtKqVLjwBXNNOz0vvSBZC0cJTZ4frjmdIEvj6YavKC4rextRejd2RVjenhSUFcP9XGtoz0PtvGHbU5JVavP84xKykFVbdMbfvi2HooPREIB6w2A6fmtz+MWESrHmaVjseP/hhi0oaIxvt9DrAO7jz76CC+99BIee+wxFBbSgcuttfFYKmpq1RgcIMXobm1boH469T6KKpTw6CDBMAOHtollPTfCH852NkjNK8PBq4Z/8BHjoz7OcC3tXtU+tmpfkkFfXrSBYnM7G10dxC3uqCWWsWhsV12qkZZ8cfRGq4J9LZFQgOFBHlg3qxcEYJvEpCF9u7INpVIziL5VgD8TshB9q8DsX9YbYx3YvfLKK7hy5QoKCgoQEhKCffv2mbJevJSYVYLfLtwFACyN6A6BoG3fFPZeugcAmNZHDps2rNMj5udsJ8b/jdSky/jymPlH7ciDqI8znL7dq9rzPOPS2AXKbNKcSGyEGB/SdH4+YjkioQDrZvZiVdbQYL8pzaXScXMQPxBgyl3ssCS8G76c3Re/vDgUZ5aONVpQF5mYjRGfHMeT38fg9V0JePL7GIz45Hibgte2MmgeJiAgAMePH8emTZswc+ZMBAcHw8am4SUuXtS/7bk9KqlUYuHOi1CpGUwKlWGAX9vWhiiqlIhKygUATO/byRhVJGb27HB//Of0bdzMK8OBq9l4uA9Np1sa9XGGMfbuVTZpTnIU1a1agE9MLyJUjiXhXbHhaGqzZeoH+219D5tLpQPAaBt5WqIdXW4comqXIVhqZNngBTYZGRnYvXs33NzcMH369Ac6PfIghmHw9u+XkVFQgc5u9ljL8ltNS/5MuIdKpQpdPTugT2fazWiNtKN2n0fdwKbjqZjaS96mNZfEOKiPY8/Yu1cpzYn18/dgdwRhTolxjtZqLpWOqQN/fcsQBNCMTFpidNmgHuv777/Hm2++ifDwcFy7dg0dO1ISUzZ+OJOGI0m5sBUJ8fWc/nB1aFtSTYZh8Eus5hiX2YN92zylSyxn3jB/fH/qNm7kluFIUg6tG7Iw6uMMoz15oLn8YobuXvXo8OAxZ02hNCfcxfa9WXMgGfa2Iqvt8wxZhjDQt/nzl02B9cKsiIgILF26FJs2bcLu3bupw2PpQnoh1h26DgBYPjUYvTu7tvmaV7NKkJStgK2NEDP70TSsNXOxF2PeMH8AwFfHb4Jh+LnW7l5xJZ76PgYbjzU/RWNp1McZTptfDHhwEbuhOw8jE7Px5m8JLZbh07FPfKUN9vW940XlNa3eNc0FXB5dZh3YqVQqXLlyBc8884wp68MrBWXVWLTzEmrVDKb18cbcocY5ZeCXOM1o3eRQGR2pwwPPjwiAg60I1+4pcPx6nv4fsEIXMopw7laBbl0oF1Efx15cWqFuB+D4EFmrzoOtT7tWKUdR3WwZvqeo4Iv6wX5LWrtrmiu4nESb9VRsVFSUKevBOyo1g8W/JiBHUYXAjo5YO7OXUaZMy6pr8WeCZjfsk4N923w9YnlSR1s8PdQP3566jY3Hb2JsD0/eTa/Hp2t2RQ7wc7NwTZpHfVzLVGoGW/6+BT8Az28/j2qV5h7VJiI+s3Rsqxass9kJC/Dv2Cc+0+5YfW/PVRSWN5+02JgbKcxtgJ8bhALNmcXNEQrq+jym6ZyMpkI5Mkzkq+OpOJ2aD3uxCFvmDkAHiXEWYP+VcA8VNSp06ehI0xE88n8ju0BiI8TlO8WsU0NYk/jMIgDAQH/uBnakeZGJ2RjwYRQ2/33zgee0OwCjknIQFuiO6X07ISzQnfWoGpudsADw2aN9KKizIhGhciyf2pNVWWvcDBOfUdRiUAdogr74jCLzVKgeCuxM4MzNAnxZt5booxmh6OblZJTrMgyD/8ZkAACeok0TvNLRSYIZdeslf4rOsHBtjKukUomkewoAwMA2pvkh5heZmI2XW0gY3NYpNbYf6vnlzU/TEm6SOXN3urKtjiblsCrH6TV2hJ3bCuDVXZfBMJqp0pn9Oxvt2vEZRUjOVkBiI8SjA4x3XcIN2k0UkddykG2kVABcEH2rAGoG6NLR8YF1WITbtNOk+hiaiLg+2gnLX2w2UggFmo0U1kSlZrCn7px2fSxx31JgZ0T3iiuxJVmEsupaDAmQslpAagjtSM70vt5tTplCuCdY7ozBAVKo1Ax2xGRaujpGc+bmfQDAyCAPC9eEGIrtNKmWoaMTtBOW39hspFAzwMKd1rU7Ni6tsMW1g1rujrYWuW8psDMib1d7jPFmMDzQHdueGww7scho175fWo1DdTf+M2H+Rrsu4ZZn60btfonLRJXSvAtuTYFhGJy4XhfYdaX0IdbG0EDNkNEJ2gnbPkSEyrH5qX7Q9/ZZ0+5Ytn8X0/t6W+S+pcDOyCZ1VuP7p/vB3tZ4QR0A7IrLhFLFoJ+vK0I70UkTfDUhxAtyFzsUlNfgwBXr+QbbnOTsUmQVV8JOLMRwGrGzOoYEaoaMqhmyE9ZSxzIR43FzlLS40aAtU/mWkJ5fzqqcpc40psDOyAQCQCwy7q+1VqXGzrrcdc+EGScXHuEmG5EQc4Zo0tjsOm/907FHkzV560Z27Wj0LzvE9NgmmxXAsFE12gnbvnA5ma+hIhOzWzwLV8uSywcosLMC/4u/i+ySKrg72mJyL+rk+O7RAT4QCoDz6UW4fb/M0tVpE21C4vHBXhauCWmNlk6W0HJzEBs8qkY7YdsXtiO/6fkVJq5J27DdTGToFx1jo8CO48qra/HvqBsAgAWjAyGxoVEPvpO52OGhbpr1aP+Lv2vh2rRerqIKV7NKIBAAY3p4Wro6pJW0yWYb72h2tRdjSXhXXHh/vMGjamynsmgnLD+wHfn94ugNTm+iYDvSvDi8m0VHmimw47hvT97C/dJq+Ls70KaJduSxgT4AgD8u3rWaBcWNnbuVDwAI9XZBRyd2KS0IN0WEynFm6Vj8OG8QAODHeYMQv3w8Xg/vZvCohErN6I5FbAnthOUP7cgvm56My5so2I40+3s4mLgmLaPAjsOKK2rw/ek0AMC7k3rA1obervZiXLAn3BzEyFVU41TqfUtXp1XO3SwAAAwLsq6jgkjTREKBLtBie1xYU+LSClvcCas1e5Av7YTlkYhQOZaEd22xjHYThSVOa2DDWkaaKVLgsB2xmahUqhAsd8bEnpbZXUMsQ2IjwvS+mpMorHU6Nvp2XWAXSLthyT9yFNYx6kGMz9/DkVW5/DLura20ppFmCuw4qqZWje3n0gEAL44MoOPD2iHt6SJRSbkoaeY4J67KLKjA3aJK2AgFGETnw5I6kYnZWLP/Gquylh71IMbH9j3NKODeJgprGmmmwI6j/rp8D3ml1fBylmBqb29LV4dYQE9vZ/SQOaGmVo39V+9ZujoGOVi3AHqAnxscbG0sXBvCBdqExPoy9tNJE/w1OEAKmbP+9bZ/XOTeLAXbs2G5MNJMgR0HMQyD/5y+DQB4dlgAra1rpwQCAWbVnTX8hxVNxzIMo6vvjH6dLFwbwgVsExLTSRP8JhIK8ORgX73l2E7XmwvXz4ZtjCIGDjp3qwDXc0rhYCvCUyz+CAh/Te+nOZLmYmYxbuSWWro6rCRnlyI1rwwSGyEm96a8i4R9mgipoy2dNMFzbNfZcQnXz4ZtjAI7Dtp6Nh0A8NiAznBxEFu2MsSiPJ3sdMl9f47JsHBt2DlW77QJZzu6fwn7NBHvTwmmoI7nDBnR0p5cY2lcPxu2MQrsOCazoALHrmtu5mfqDoQn7dvcoZpj5HZfzEJ5da2Fa6Pfset5ADQpWwgB2H+Yy1zsTVwTYmnaZMVsrDt0nRM57djev5Y6G7YxCuw45r8x6WAYYFS3jgjs2MHS1SEcMCzQHV08HFFWXYv9V7i9iSK/rBqX7xYDAMZ0p8COaAzwc4PU0bbZ52nDRPtR/5g6fXIUVYhLKzRxjfQrKq+GvoE4Lt2/FNhxSEVNLX49fwcA8OwwPwvXhnCFUCjArLrUJ/uvcPe4HQD4O+U+GAYI7eT8wBFUpH2KTMzGQ5+eQGF5TZPP04aJ9iciVI4XhvuzKst2GtRUIhOzsXDnJbQ0cGjps2Ebs6rAbt26dRAIBFi8eLHusaqqKixcuBDu7u7o0KEDZs2ahdzchvPymZmZmDJlChwcHODp6Ym3334btbXcm9LacykLiqpa+Lk7YHQ3Gu0g/5jSS7Pu6NytAhRwMHmn1vG6ZQRjabSuVfjWx2lTnLS0cULmYkcbJtqhcJbTlpbcZcpmN7dQAGx+ilv3r9UEdufPn8e3336L3r17N3h8yZIl2LdvH37//XecPHkS9+7dw8yZM3XPq1QqTJkyBTU1NTh37hy2b9+Obdu24YMPPjB3E1rEMIwuIfEzYf4QciTyJ9zg7+GI0E7OUKkZRF5jl0/J3Gpq1Th9Q3M+7Ni6DR+EPb71cWw+FKWOYpx8ewynPhSJeWjX2rX0SeflJLHo9Cab3dxqBnBrYZmBJVhFYFdWVoY5c+bg+++/h5vbP1nsS0pK8MMPP+Dzzz/H2LFjMWDAAGzduhXnzp1DTEwMAODIkSNISkrCzz//jL59+2LSpElYs2YNNm/ejJqapqcGLOHcrQLcyC2Dg60Ijw3sbOnqEA7SJqo+wNHp2EuZRSitroW7oy16d3KxdHWsCh/7ODYfioXlSs6eC0pMq/5au+aCu+paNaJYJgY2BbbTwJaeLm7MKlLCL1y4EFOmTEF4eDg+/PBD3ePx8fFQKpUIDw/XPdajRw/4+voiOjoaQ4cORXR0NHr16gUvr39GECZOnIgFCxbg2rVr6Nev3wOvV11djerqf6a7FAoFAECpVEKpbD6Xjfa5lso0hWEY/PtICgBgVj9v2IsMv4YptLY9XMSHtkwI9sC6Q0DM7QJkF2kOo+ZSe07f0OyGHRoghUpVC5WK3c81fm+41CZz4WMfl1dSDolI/47GvJJyKJXOessZG9/uN2tsz7juHvj6qT5Y+dc1FFf+U2+JUHPfVFXXYPEv8djwRF+EW2AWwMPBhtU97OFgw/rvxhzvD+cDu127duHixYs4f/78A8/l5OTA1tYWrq6uDR738vJCTk6Orkz9Dk/7vPa5pqxduxarVq164PEjR47AwUH/cSFRUVF6y9SXWCjAxUwRxEIGXWvTcPBgmkE/b2qGtofLrL0tvo4iZJYL8NWeUxgh41Z7DiSKAAjgVJGFgwcNPylD25aKCu6dE2lKfO7j1g9mUejOJRy8c4nV9UyBS39DxmCN7XmvV9OPrx6oBgDUpMXDUh+LbO7h/OQYHEzWXy4qKsos/RunA7s7d+7g9ddfR1RUFOzszLeActmyZXjjjTd0/1YoFPDx8cGECRPg7Nz8N0ulUomoqCiMHz8eYjG7xKxqNYOvv44GUIbnhgfgyQnd2lp9o2lNe7iKL22555yOTw7fwK1aKUaggDPtKa+uxZuxJwAwmP/IQ/BxY39eYuP3Rjt61B7wtY/7/Mh1/Hiu5YTaAgBeznY4vHiURXYT8qVP0LLW9sSlFeL57Q2/1EiEDNYMVGP5BSGq1Zp748d5g8y+3u5oci4W/5rQYpkvWIwm1n9vKisrjVjDpnE6sIuPj0deXh769++ve0ylUuHUqVPYtGkTDh8+jJqaGhQXFzf4RpubmwuZTLPjRiaTIS4ursF1tTvKtGUak0gkkEgePKhYLBaz+oNhWw4A9l7KQkpuGZzsbLBwTDdO/kEa0h6us/a2zOjvg8+iUnHxTgnC3bjTnku3ilCrZuAjtUcXz9atr9O2hQvtMRc+9nEHr9zDltOZaH7llIYAwLIpPWEnsezCc77dc9bWnvyKWlSrmr5XqtUC3XP5FbVmbZdKzWD1gZRm6wYArg5iTAjtxPqLiVgsNstudU5vnhg3bhyuXr2KhIQE3X8DBw7EnDlzdP9fLBbj2LFjup9JSUlBZmYmwsLCAABhYWG4evUq8vLydGWioqLg7OyMkBB2SRJNaePxVADAyw8F0vFhRC+Zix0iemo+rE/ncOfP9+SN+wCA4YEeFq6JdeFbH6dSM3j7jyusyi4O70a7YQnrdCYejg9+ETElNpt/iiuUnEig3BinR+ycnJwQGhra4DFHR0e4u7vrHn/hhRfwxhtvQCqVwtnZGa+++irCwsIwdOhQAMCECRMQEhKCp59+GuvXr0dOTg7ef/99LFy4sMlvrOaUU1KF2/fLIRAAz4RRQmLCzrxh/jhwNRsX8gUorlCio4tlvxCo1QwiEzVrucaHUJoTQ/Ctj9t0PBXl1ex2zfh7sJ+uJ/ylTXuSU1LVYmqcN3+/jJUPh5jty4C17ogFOD5ix8aGDRswdepUzJo1C6NGjYJMJsPu3bt1z4tEIuzfvx8ikQhhYWGYO3cunnnmGaxevdqCtdbYEatZg9LXxxVOdFg6YWmQvxuCZU5QqgX4/aLhmxSM7dKdYuQoqtBBYoMRXWnEztispY9TqRlsPZvOurwlE88S7mCT9gQAchVVWPDzRUQmmifdU3p+OatyXLyPOT1i15S///67wb/t7OywefNmbN68udmf8fPzw8GDB01cM8OUV9fip2hNYDd/ZBcL14ZYE4FAgKeH+uK9vdewI/YOXnqoq0WPsjl0VdPRhgd7QmIjslg9+MJa+7i4tMIGKSta4u5oy5lzNYnlRYTKsWVuf6z86xpyFE2frMNAE/it2peE8SEyk/Z5kYnZ2HA0tcUyAmiWxnDxPrb6ETtrtT06HSWVSgR4OGJCT3ZHqxCiNa23DI42DLKKq3A0OVf/D5gIwzA4VDcNO6kXrZdqz44akEh2zfRQzpyrSbghIlSOfz/et8UyDIDskiqTrmvTnpjCBpfOh62PAjsLKKlU4pu/bwEAXhsXxMkbg3CbnViEME/NipQfzlgu7+GVuyXIKq6Eg60ID3XraLF6EMtSqRnsvpTFquzU3nJM7k1fAsiD8lmeg23K0yjYbJoAuL35hwI7C/j675tQVNWim1cHPNynk6WrQ6zUSJkaNkIB4tIKceVusUXqcDpVsxv2oW4dYSemadj2atPxVBRV6J+GdbQV4cvZD56EQQjAfr3aj2fTTbbWju1mCC5v/qHAzsxSc0vxw2nNCMvSiB40WkdazVUCTOmlmcb/z2nLjNol3CkGAAz05946E2IebNYjaT0xyIf6PNIs7Q5ZNlbtS4JKrf+4L0NZ86YJLQrszIhhGCz/MxG1agbhwV4YZ4Gz7wi/PD9ckybnwNVs3Cs2fUbz+hiGQcKdEgCand2k/TFkPRIAjA+h9cSkefV3yOpjirV2KjWDX+Iy9ZaTc3TThBYFdmZ0JCkXMbcLYScWsr55CWlJiNwZg/2lUKkZHL5munUnTblTWIn8smrYCAXo6W3+Q9yJ5bFdjwRw/8OQcENEqBzPDGGX19XYa+3i0gqb3ZVb3+xBvpweeabAzoy0i9yfHRYAHyl35+eJddEmBT5+PU9PSePad+UeAGCgvxutr2unDNkJy9UdhIR7RvfwZFXuz4R7Rp2O5cP6OoACO7NJzCpBXFohbIQCPDvM39LVITwypq4TjL1diLJq059DCGimYf8Xr0mO/OgAH7O8JuEWlZrBrgt3WJVdwuEdhIR7Bvi5sSpXUF5j1OlYPqyvAyiwM5vvT98GAEzpLYeM5eJQQtgI7OiIAA9H1KjUZhu1i88oQlp+ORxsRZgUSuum2iO2x4dJHcRYNDbIDDUifGHIyK6xjvTiy/o6gAI7s0jLL8e+y5ppqxfplAliZAKBAJPrdsceqJseNbW/6u7nSaFyOEqs7gAb0kaGHB/2SL9ONAVLTMZYo2d8WV8HUGBnFptP3ISaAcb28ERoJxdLV4fw0OS6Ux/+TrlvlunYc7cKAPyzvo+0L/EZRayPD6OdsKS1ZM76g7ai8hqjvBZf1tcBFNiZXGZBBfbUZWR/laYjiImEyJ0R4OGI6lo1jpn4iLH7pdW4mVcGgQAYwvEpCWIax5PZTfm7Oog5P21FuOudid31lllzwDj57NiO/HF9fR1AgZ3JbTqRCpWawUPdOqKfL7sFoYQYqv507JEk0wZ2sWma0boeMme4Odqa9LUIN/0cl8Gq3HPDAjg/bUW4y9VBf/9irHx2ReX6p2GtYX0dQIGdSWUUlOOPi5rRutfDu1q4NoTvRnXVnNUal1YIhjF+Rnat6Lpp2KFduN/BEeM6YkCuRFfaNEHaiO3ZsW3dQKFSM3hvb6LecsunWEfKHgrsTGjT8Zu60br+NFpHTKyPjytsRULcL61GZmGFSV6DYRicqjsfNqyLu0leg3CTSq05OYetdTN7WcWHIOEujw4SVuXaOj266XgqilmcdWwtMxQU2JnIncJ/1tbRaB0xBzuxCL06azbnGPuoHa3ELAXuFFbCTizEiK4eJnkNwk2bjqeivEZ/ehMAeH64P+WtI202wM+N1dmxbdlAYcgOb2OlVjE1CuxMZMPRG6hVMxgR5EGjdcRsBvlrpkfPp5smsNtfl05lbA9PONhSmpP2QqVm8CPLDz+AdsIS4xAJBVg+JVhvubZsoIhLK2S9w9saNk4AFNiZxI3cUt1o3dssdvUQYiyDAzRfIkwxYlerUuvu6+l9Oxn9+oS7Nh1PRQnLDz93R1urWGBOrIObo/7p2LZsoGB7LJ417fCmwM7IypXAq7sug2GAiJ4y9PFxtXSVSDsyyF8KsUiA9IIK3MwrNeq1z9zMR15pNdwcxBjTnd1ZjsT6RSZmY8PRVNbl10wPpbV1xGjYTn+2ZppUpWawJyGLVVlr2uFNgZ0RVdeq8eMNEW7nV8DbxQ6rpve0dJVIO+NkJ8bwIM3at8PXjJv2ZHfdDu+H+3jD1oa6jvZApWawal8S6/JTe8sxuTetrSPGY8r8cnFphSgs1z8S3UFiY1U7vKl3NqK80ioUVGlugh+fGwQvFlmzCTG2iJ6a9U2RiexTU+ijUjM4kaJJSvswTcO2G3FphcguYTcS4mJvgy9n9zNxjUh7MzhACrmLHVoaK5M6ijHAz/C17GxH+R4f2NlqRusACuyMysfNAUt6qfD90/3QQ+Zs6eqQdio8xAtCAXA1qwR3i4yT9iTpngKlVbVwktigT2c6Fq+9MGR665NZva3qw49YB5FQgBXTQgCg2eCusFyJhz49gcjEbIOuzXaUz9o2A1FgZ2QutsDAVnxzIMRYPDpIMLBud+wRI03HRt/OB6D59mwjom6jvWD7wbckvBulNyEmExEqx5a5/SFrIfVJTkkVFvx80aDgrqi8Gvq+i1jLaRP1UQ9NCA/ppmMNOCmgJdrTJsICKSlxe8JmGkzmLLGq9UfEOkWEynHy7TGQNpMkWJvsZNU+dqlPIhOzsXDnJbRUVABgxTTrOG2iPgrsCOGhiaGawO58eiHul7I7lqc5tSo1zqcXAQCG0mkT7UpL02CCuv9WPtzT6j74iHWKzyhCYQvJiBmwS32i3RTUUvgnFACbn+pvlSPRFNgRwkOdXO3Rx8cVDAMcvGrYupPGrmSVoKy6Fs52NgiW09rR9qa5aTCZix22zLXODz5inYyV+oTNpiA1Yz1HiDVGqeMJ4amH+3jj8p1i7Lt8D/OG+bf6OofrdteO7NqRRmbaqYhQOcaHyBBzMw/5yTH4cd4gDA3ypPuBmJWxUp+YMjceF9CIHSE8NbW3HAIBcCGjCFnFla26BsMw2H8lW3c90n6JhALdIvLBAVIK6ojZsVnzKRToPzvWlLnxuIACO0J4ysvZDkPqPoj3X77XqmtczCxGVnElHG1FGNODTpsghFhO/TWfzVEzwMKdLe+O1RcgCmCdu2G1KLAjhMem9fEGAOy70rrAbl9dQDihpwx2YpHR6kUIIa0RESrH5qf66U1T0tLuWJFQgOVTQprcPKG9rDXuhtXidGC3du1aDBo0CE5OTvD09MQjjzyClJSUBmWqqqqwcOFCuLu7o0OHDpg1axZycxvm7srMzMSUKVPg4OAAT09PvP3226itrTVnUwixiEmhctgIBUjMUuDW/TKDflalZnQbL2ga1jSojyPEcG6OkhbTlOjbHRuZmI01B5o+Ko8Pm4I4HdidPHkSCxcuRExMDKKioqBUKjFhwgSUl5fryixZsgT79u3D77//jpMnT+LevXuYOXOm7nmVSoUpU6agpqYG586dw/bt27Ft2zZ88MEHlmgSIWYldbTFyK6as2P/SjBs1C4urRB5pdVwtrPByK4dTVG9do/6OEIM15bND5GJ2Vjw88Vmd8UunxJs1UEdwPFdsZGRkQ3+vW3bNnh6eiI+Ph6jRo1CSUkJfvjhB+zcuRNjx44FAGzduhXBwcGIiYnB0KFDceTIESQlJeHo0aPw8vJC3759sWbNGixduhQrV66Era11bmcmhK2H+3rjRMp9/HX5HhaHd4VAwG56YX/d9G1EqAy2Npz+Dmi1qI8jxHBsNzWk5zc8UlGlZrDyr+bz1wkArDmQjImhcqudhgU4Htg1VlJSAgCQSjULGuPj46FUKhEeHq4r06NHD/j6+iI6OhpDhw5FdHQ0evXqBS8vL12ZiRMnYsGCBbh27Rr69Xvw0Orq6mpUV/+T1FWhUAAAlEollEpls/XTPtdSGWvCp/bwqS2AYe0Z3dUddmIh0vLLcSmjAL066T/rtby69p/ArqenSX9vjdvCl/eoNaiPMx8+tQXgV3v0taVfZyf4utoiV0/y9Z0xt/HSSD9dkLbl75soKq+EpIXlwoVllYi5mWfUjRP122OO98dqAju1Wo3Fixdj+PDhCA0NBQDk5OTA1tYWrq6uDcp6eXkhJydHV6Z+h6d9XvtcU9auXYtVq1Y98PiRI0fg4OCgt65RUVF6y1gTPrWHT20B2Lcn2FmISwVCfLo7GrMD1XrLn7gnQEmlCB3tGJSkxOHgjbbWVD9tWyoqKvSU5Cfq4yyDT20B+NWeltryZjCbK6hwOPKQ7l9+ANYP1v9T+ckxOJjM5vqGiYqKMkv/ZjWB3cKFC5GYmIgzZ86Y/LWWLVuGN954Q/dvhUIBHx8fTJgwAc7OzWfeVyqViIqKwvjx4yEWi01eT1PjU3v41BbA8PZ49izCk/85jwsFInzyzGjIWzhMW6lSY+3npwFU47UJPTF1UGcj1ryJ12vUFu3oUXtDfZx58aktAL/aw6YtB69m450/rui9loudDU68NQaTvjyNXJZr836cN8joI3ba9lRWti6nqCGsIrBbtGgR9u/fj1OnTqFz538+ZGQyGWpqalBcXNzgG21ubi5kMpmuTFxcXIPraXeUacs0JpFIIJFIHnhcLBaz+oNhW85a8Kk9fGoLwL49YUGeGBwgRVxaIf5zNgOrp4c2W3Zb9G3kKKrh0UGCxwb5QmymNCfatvDp/WGL+jjL4VNbAH61p6W2eLo4olqlfx1cXrkK07+ORmZxNR488fhBchc7k52qIhaLzbJbndMrohmGwaJFi7Bnzx4cP34cAQEBDZ4fMGAAxGIxjh07pnssJSUFmZmZCAsLAwCEhYXh6tWryMvL05WJioqCs7MzQkJaTnRICJ+8Pq4rAOC/MRmIvlXQZJmUnFJ8eliTbuOtCd0od52JUR9HSOsMDpDC1Z5dAHvzfrn+QnWsOX+dFqcDu4ULF+Lnn3/Gzp074eTkhJycHOTk5OiGMl1cXPDCCy/gjTfewIkTJxAfH4/nnnsOYWFhGDp0KABgwoQJCAkJwdNPP43Lly/j8OHDeP/997Fw4cImv7ESwlfDgzzw+MDOYBjgjd8SkF/WcOFxlVKFxb8moEalxrgennhikI+Fatp+UB9HSOuIhAI8N9zfqNdcEt7N6lOdABwP7LZs2YKSkhKMHj0acrlc99+vv/6qK7NhwwZMnToVs2bNwqhRoyCTybB7927d8yKRCPv374dIJEJYWBjmzp2LZ555BqtXr7ZEkwixqBXTeiLAwxHZJVWYuOEU9ly6C5Wawe6LdzHu3yeRnK2Am4MYa2f1Yp0WhbQe9XGEtN6isV3h6mCcaWeZswSLxgYZ5VqWxuk1dgzTQmrpOnZ2dti8eTM2b97cbBk/Pz8cPHjQmFUjxCo5Smzw3dMDsHDnRdzILcOSXy/jw/3JKKg7NFvuYod/P97Hag+/tjbUxxHSeiKhAOtm9sLLP19s87VWPtzT6qdgtTg9YkcIMb6uXk7Y/+pIvD2xOyQ2QhSU18BJYoN3IrrjxFujMSzQw9JVJIQQViJC5VgS3rVN1+DLFKwWp0fsCCGmYWsjxMIxQZjW2xunUu9jUqgM7h1oPRYhxPosGtsVv8TdQY6CXTqT+vg0BatFI3aEtGO+7g6YO9SPgjpCiNUSCQVY+XAIi2Qm/xDU/cenKVgtCuwIIYQQYtUiQuXYMrd/i8nX65O52GHL3P68moLVoqlYQgghhFi9iFA5xofIEJdWiKikHOxNuIfCuo1hACB1FGNG304ID5FhcICUdyN1WhTYEUIIIYQXREIBwgLdERbojn9NCUFcWiHySqvg6WTH62CuPgrsCCGEEMI72iCvvaE1doQQQgghPEGBHSGEEEIIT9BULAva7PAKhaLFckqlEhUVFVAoFBCLjXPMiSXxqT18agvAr/Y0bov274zNqQzEONpjH8entgD8ag+f2gI0bI/2HGhT9m8U2LFQWloKAPDxoUPRCTGX0tJSuLi4WLoa7QL1cYSYlyn7NwFDX4v1UqvVuHfvHpycnFo8GF2hUMDHxwd37tyBs7OzGWtoGnxqD5/aAvCrPY3bwjAMSktL4e3tDaGQVouYQ3vs4/jUFoBf7eFTW4CG7XFycjJ5/0YjdiwIhUJ07tyZdXlnZ2de3IxafGoPn9oC8Ks99dtCI3Xm1Z77OD61BeBXe/jUFuCf9pi6f6Ovw4QQQgghPEGBHSGEEEIIT1BgZ0QSiQQrVqyARMKPA9X51B4+tQXgV3v41Ba+49N7xae2APxqD5/aApi/PbR5ghBCCCGEJ2jEjhBCCCGEJyiwI4QQQgjhCQrsCCGEEEJ4ggI7QgghhBCeoMDOiDZv3gx/f3/Y2dlhyJAhiIuLs3SVHrB27VoMGjQITk5O8PT0xCOPPIKUlJQGZUaPHg2BQNDgv5dffrlBmczMTEyZMgUODg7w9PTE22+/jdraWnM2BStXrnygnj169NA9X1VVhYULF8Ld3R0dOnTArFmzkJuby7l2aPn7+z/QHoFAgIULFwLg9vty6tQpTJs2Dd7e3hAIBNi7d2+D5xmGwQcffAC5XA57e3uEh4cjNTW1QZnCwkLMmTMHzs7OcHV1xQsvvICysrIGZa5cuYKRI0fCzs4OPj4+WL9+vambRupQ/2b+foFPfZw192+AlfVxDDGKXbt2Mba2tsyPP/7IXLt2jXnxxRcZV1dXJjc319JVa2DixInM1q1bmcTERCYhIYGZPHky4+vry5SVlenKPPTQQ8yLL77IZGdn6/4rKSnRPV9bW8uEhoYy4eHhzKVLl5iDBw8yHh4ezLJly8zalhUrVjA9e/ZsUM/79+/rnn/55ZcZHx8f5tixY8yFCxeYoUOHMsOGDeNcO7Ty8vIatCUqKooBwJw4cYJhGG6/LwcPHmT+9a9/Mbt372YAMHv27Gnw/Lp16xgXFxdm7969zOXLl5mHH36YCQgIYCorK3VlIiIimD59+jAxMTHM6dOnmaCgIObJJ5/UPV9SUsJ4eXkxc+bMYRITE5lffvmFsbe3Z7799luTt6+9o/7NMv0Cn/o4a+7fGMa6+jgK7Ixk8ODBzMKFC3X/VqlUjLe3N7N27VoL1kq/vLw8BgBz8uRJ3WMPPfQQ8/rrrzf7MwcPHmSEQiGTk5Oje2zLli2Ms7MzU11dbcrqNrBixQqmT58+TT5XXFzMiMVi5vfff9c9lpyczABgoqOjGYbhTjua8/rrrzOBgYGMWq1mGMZ63pfGnZ5arWZkMhnz6aef6h4rLi5mJBIJ88svvzAMwzBJSUkMAOb8+fO6MocOHWIEAgGTlZXFMAzDfP3114ybm1uDtixdupTp3r27iVtEqH+zTL/A5z7OWvs3huF+H0dTsUZQU1OD+Ph4hIeH6x4TCoUIDw9HdHS0BWumX0lJCQBAKpU2eHzHjh3w8PBAaGgoli1bhoqKCt1z0dHR6NWrF7y8vHSPTZw4EQqFAteuXTNPxeukpqbC29sbXbp0wZw5c5CZmQkAiI+Ph1KpbPCe9OjRA76+vrr3hEvtaKympgY///wznn/++QaHslvL+1JfWloacnJyGrwXLi4uGDJkSIP3wtXVFQMHDtSVCQ8Ph1AoRGxsrK7MqFGjYGtrqyszceJEpKSkoKioyEytaX+of7Ps3xEf+zg+9W8A9/o4m7Y2iAD5+flQqVQNbjgA8PLywvXr1y1UK/3UajUWL16M4cOHIzQ0VPf4U089BT8/P3h7e+PKlStYunQpUlJSsHv3bgBATk5Ok23VPmcuQ4YMwbZt29C9e3dkZ2dj1apVGDlyJBITE5GTkwNbW1u4uro+UE9tHbnSjqbs3bsXxcXFePbZZ3WPWcv70pj2tZuqW/33wtPTs8HzNjY2kEqlDcoEBAQ8cA3tc25ubiapf3tH/Zvl/o742sfxqX+r//pc6eMosGvHFi5ciMTERJw5c6bB4/Pnz9f9/169ekEul2PcuHG4desWAgMDzV3NZk2aNEn3/3v37o0hQ4bAz88Pv/32G+zt7S1Ys7b74YcfMGnSJHh7e+ses5b3hRAusPb+DeBvH0f9m2nRVKwReHh4QCQSPbAbKTc3FzKZzEK1atmiRYuwf/9+nDhxAp07d26x7JAhQwAAN2/eBADIZLIm26p9zlJcXV3RrVs33Lx5EzKZDDU1NSguLm5Qpv57wtV2ZGRk4OjRo/i///u/FstZy/uife2W/j5kMhny8vIaPF9bW4vCwkLOv198R/0bd+4zPvRxfOvf6r8+V/o4CuyMwNbWFgMGDMCxY8d0j6nVahw7dgxhYWEWrNmDGIbBokWLsGfPHhw/fvyBYd+mJCQkAADkcjkAICwsDFevXm1wk0ZFRcHZ2RkhISEmqTcbZWVluHXrFuRyOQYMGACxWNzgPUlJSUFmZqbuPeFqO7Zu3QpPT09MmTKlxXLW8r4EBARAJpM1eC8UCgViY2MbvBfFxcWIj4/XlTl+/DjUarWugw8LC8OpU6egVCp1ZaKiotC9e3eahjUh6t+48XcE8KOP41v/BnCwjzN8Pwhpyq5duxiJRMJs27aNSUpKYubPn8+4uro22MHDBQsWLGBcXFyYv//+u8G28oqKCoZhGObmzZvM6tWrmQsXLjBpaWnMn3/+yXTp0oUZNWqU7hrabecTJkxgEhISmMjISKZjx45m30L/5ptvMn///TeTlpbGnD17lgkPD2c8PDyYvLw8hmE0qQB8fX2Z48ePMxcuXGDCwsKYsLAwzrWjPpVKxfj6+jJLly5t8DjX35fS0lLm0qVLzKVLlxgAzOeff85cunSJycjIYBhGkwrA1dWV+fPPP5krV64w06dPbzIVQL9+/ZjY2FjmzJkzTNeuXRukAiguLma8vLyYp59+mklMTGR27drFODg4ULoTM6D+zTL9At/6OGvt3xjGuvo4CuyM6KuvvmJ8fX0ZW1tbZvDgwUxMTIylq/QAAE3+t3XrVoZhGCYzM5MZNWoUI5VKGYlEwgQFBTFvv/12g3xCDMMw6enpzKRJkxh7e3vGw8ODefPNNxmlUmnWtjzxxBOMXC5nbG1tmU6dOjFPPPEEc/PmTd3zlZWVzCuvvMK4ubkxDg4OzIwZM5js7GzOtaO+w4cPMwCYlJSUBo9z/X05ceJEk/fVvHnzGIbRpANYvnw54+XlxUgkEmbcuHEPtLGgoIB58sknmQ4dOjDOzs7Mc889x5SWljYoc/nyZWbEiBGMRCJhOnXqxKxbt87kbSMa1L+Zv1/gWx9nrf0bw1hXHydgGIZhP75HCCGEEEK4itbYEUIIIYTwBAV2hBBCCCE8QYEdIYQQQghPUGBHCCGEEMITFNgRQgghhPAEBXaEEEIIITxBgR0hhBBCCE9QYEcIIYQQwhMU2BFe8ff3h0AggEAgeOBwbEONHj1ady3tuYWEEGJJ1McRfSiwI5yjUqkwbNgwzJw5s8HjJSUl8PHxwb/+9a8Wf3716tXIzs6Gi4tLm+qxe/duxMXFtekahBDSGPVxxJQosCOcIxKJsG3bNkRGRmLHjh26x1999VVIpVKsWLGixZ93cnKCTCaDQCBoUz2kUik6duzYpmsQQkhj1McRU6LAjnBSt27dsG7dOrz66qvIzs7Gn3/+iV27duGnn36Cra2tQdfatm0bXF1dsX//fnTv3h0ODg549NFHUVFRge3bt8Pf3x9ubm547bXXoFKpTNQiQgj5B/VxxFRsLF0BQprz6quvYs+ePXj66adx9epVfPDBB+jTp0+rrlVRUYGNGzdi165dKC0txcyZMzFjxgy4urri4MGDuH37NmbNmoXhw4fjiSeeMHJLCCHkQdTHEVOgwI5wlkAgwJYtWxAcHIxevXrh3XffbfW1lEoltmzZgsDAQADAo48+iv/+97/Izc1Fhw4dEBISgjFjxuDEiRPU6RFCzIL6OGIKNBVLOO3HH3+Eg4MD0tLScPfu3VZfx8HBQdfhAYCXlxf8/f3RoUOHBo/l5eW1qb6EEGII6uOIsVFgRzjr3Llz2LBhA/bv34/BgwfjhRdeAMMwrbqWWCxu8G+BQNDkY2q1utX1JYQQQ1AfR0yBAjvCSRUVFXj22WexYMECjBkzBj/88APi4uLwzTffWLpqhBDSZtTHEVOhwI5w0rJly8AwDNatWwdAk5Tzs88+wzvvvIP09HTLVo4QQtqI+jhiKhTYEc45efIkNm/ejK1bt8LBwUH3+EsvvYRhw4a1abqCEEIsjfo4YkoChu4ewiP+/v5YvHgxFi9ebJTrpaenIyAgAJcuXULfvn2Nck1CCGkt6uOIPjRiR3hn6dKl6NChA0pKStp0nUmTJqFnz55GqhUhhBgH9XGkJTRiR3glIyMDSqUSANClSxcIha3/7pKVlYXKykoAgK+vr8HZ4AkhxNiojyP6UGBHCCGEEMITNBVLCCGEEMITFNgRQgghhPAEBXaEEEIIITxBgR0hhBBCCE9QYEcIIYQQwhMU2BFCCCGE8AQFdoQQQgghPEGBHSGEEEIIT/w/gdelKgWXs64AAAAASUVORK5CYII=", "text/plain": [ "
" ] @@ -720,10 +724,16 @@ "fig, (ax1,ax2) = plt.subplots(1,2)\n", "\n", "gdf.plot(ax=ax1, aspect='equal')\n", + "ax1.set_xlabel('X [m]')\n", + "ax1.set_ylabel('Y [m]')\n", "ax1.grid()\n", "\n", "gdf_xy.plot(ax=ax2, aspect='equal')\n", - "ax2.grid()" + "ax2.set_xlabel('X [m]')\n", + "ax2.set_ylabel('Y [m]')\n", + "ax2.grid()\n", + "\n", + "plt.tight_layout()" ] }, { @@ -732,7 +742,7 @@ "source": [ "### Extracting the Coordinates to list of X and Y coordinates in separate cells\n", "\n", - "The coordinates of LineStrings in a GeoDataFrame can also be extracted and are stored as lists in respective X and Y columns using ``extract_xy_linestring(..)``." + "The coordinates of LineStrings in a GeoDataFrame can also be extracted and are stored as lists in respective X and Y columns using ``extract_xy_linestring(..)``. This function is being accessed by ``extract_xy`` to extract the coordinates from vertices in LineStrings. " ] }, { @@ -776,7 +786,7 @@ " \n", " \n", " 0\n", - " None\n", + " NaN\n", " Sand1\n", " LINESTRING (0.25633 264.86215, 10.59347 276.73...\n", " [0.256327195431048, 10.59346813871597, 17.1349...\n", @@ -784,7 +794,7 @@ " \n", " \n", " 1\n", - " None\n", + " NaN\n", " Ton\n", " LINESTRING (0.18819 495.78721, 8.84067 504.141...\n", " [0.1881868620686138, 8.840672956663411, 41.092...\n", @@ -792,7 +802,7 @@ " \n", " \n", " 2\n", - " None\n", + " NaN\n", " Ton\n", " LINESTRING (970.67663 833.05262, 959.37243 800...\n", " [970.6766251230017, 959.3724321757514, 941.291...\n", @@ -803,10 +813,10 @@ "" ], "text/plain": [ - " id formation geometry \\\n", - "0 None Sand1 LINESTRING (0.25633 264.86215, 10.59347 276.73... \n", - "1 None Ton LINESTRING (0.18819 495.78721, 8.84067 504.141... \n", - "2 None Ton LINESTRING (970.67663 833.05262, 959.37243 800... \n", + " id formation geometry \\\n", + "0 NaN Sand1 LINESTRING (0.25633 264.86215, 10.59347 276.73... \n", + "1 NaN Ton LINESTRING (0.18819 495.78721, 8.84067 504.141... \n", + "2 NaN Ton LINESTRING (970.67663 833.05262, 959.37243 800... \n", "\n", " X \\\n", "0 [0.256327195431048, 10.59346813871597, 17.1349... \n", @@ -877,25 +887,25 @@ " \n", " \n", " 0\n", - " None\n", + " NaN\n", " Sand1\n", " POLYGON ((0.25633 264.86215, 10.59347 276.7337...\n", " \n", " \n", " 1\n", - " None\n", + " NaN\n", " Ton\n", " POLYGON ((0.25633 264.86215, 0.18819 495.78721...\n", " \n", " \n", " 2\n", - " None\n", + " NaN\n", " Sand2\n", " POLYGON ((0.18819 495.78721, 0.24897 1068.7595...\n", " \n", " \n", " 3\n", - " None\n", + " NaN\n", " Sand2\n", " POLYGON ((511.67477 1068.85246, 971.69794 1068...\n", " \n", @@ -904,11 +914,11 @@ "" ], "text/plain": [ - " id formation geometry\n", - "0 None Sand1 POLYGON ((0.25633 264.86215, 10.59347 276.7337...\n", - "1 None Ton POLYGON ((0.25633 264.86215, 0.18819 495.78721...\n", - "2 None Sand2 POLYGON ((0.18819 495.78721, 0.24897 1068.7595...\n", - "3 None Sand2 POLYGON ((511.67477 1068.85246, 971.69794 1068..." + " id formation geometry\n", + "0 NaN Sand1 POLYGON ((0.25633 264.86215, 10.59347 276.7337...\n", + "1 NaN Ton POLYGON ((0.25633 264.86215, 0.18819 495.78721...\n", + "2 NaN Sand2 POLYGON ((0.18819 495.78721, 0.24897 1068.7595...\n", + "3 NaN Sand2 POLYGON ((511.67477 1068.85246, 971.69794 1068..." ] }, "execution_count": 16, @@ -1016,9 +1026,11 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "### Extracting the Coordinates\n", + "### Extracting the Coordinates to Point Objects\n", + "\n", + "To make the coordinates easier accessible, we use the GemGIS function ``extract_xy`` to append the stored ``X`` and ``Y`` coordinate information to the GeoDataFrame. \n", "\n", - "The resulting GeoDataFrame has now an additional ``X`` and ``Y`` column. These values represent the single vertices of each Polygon. The geometry types of the shapely objects in the GeoDataFrame were converted from Polygons to Points to match the X and Y column data. The ``id`` column was dropped by default. The index of the new GeoDataFrame was reset.\n" + "The resulting GeoDataFrame has now an additional ``X`` and ``Y`` column. These values represent the single vertices of each LineString. The geometry types of the Shapely objects in the GeoDataFrame were converted from Polygons to Points to match the X and Y column data. The ``id`` column was dropped by default but can be kept if needed (``drop_id=False``). Information stored in additional columns, here the ``formation``, are populated to the respective points extracted from each LineString. The index of the new GeoDataFrame was reset. If the original index is needed, you can set ``reset_index=False``.\n" ] }, { @@ -1139,7 +1151,7 @@ "outputs": [ { "data": { - "image/png": "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\n", + "image/png": "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", "text/plain": [ "
" ] @@ -1154,10 +1166,16 @@ "fig, (ax1,ax2) = plt.subplots(1,2)\n", "\n", "gdf.plot(ax=ax1, column='formation', aspect='equal')\n", + "ax1.set_xlabel('X [m]')\n", + "ax1.set_ylabel('Y [m]')\n", "ax1.grid()\n", "\n", "gdf_xy.plot(ax=ax2, aspect='equal')\n", - "ax2.grid()" + "ax2.set_xlabel('X [m]')\n", + "ax2.set_ylabel('Y [m]')\n", + "ax2.grid()\n", + "\n", + "plt.tight_layout()" ] }, { @@ -1358,7 +1376,7 @@ "outputs": [ { "data": { - "image/png": "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\n", + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAnUAAAFJCAYAAAD9p9sfAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjkuMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8hTgPZAAAACXBIWXMAAA9hAAAPYQGoP6dpAABgpUlEQVR4nO3deXhU1f0/8PdMyEIgCwHJIoQEqoaAirIOm1rDFlRU2kpBQKVAFWoVa21a2dUItdSi/HApiwhU2q+oVcOSghWQEBZBlgRQyaJCEjVkwUC2Ob8/0hkz2ebcmTsz9955v57Hp2XmzMw5uXc+c/ZjEkIIEBEREZGumX2dASIiIiJyHyt1RERERAbASh0RERGRAbBSR0RERGQArNQRERERGQArdUREREQGwEodERERkQGwUkdERERkAO18nQEiIlLGarXi/PnzCAsLg8lk8nV2iEgBIQQqKysRFxcHs1ndvjVW6iQwgBLplycDqK+cP38e3bt393U2iMgNX331Fbp166bqe7JSJ4EBlEj/PBFAfSUsLAxAQ5nCw8NbTFNbW4udO3di9OjRCAwM9Gb2vMLo5QNYRiNoqXwVFRXo3r27/XusJlbqJMgEUBt/vEGNxOjlA4xfxqbl82QA9RXbiEF4eHiblbrQ0FCEh4cb9jobuXwAy2gEbZXPEyN/rNRJkAmgNv58gxqB0csHGL+MrZWPUyeIyOiMMcGEiIiIyM+xUkdERERkAKzUERERERkAK3VEREREBsCFEiqqtwoczCsFABzMK8WQn3RFgNk/JmfX1FnxZlY+Ckqr0CMqFFMtCQhqp982w5Waeiz58DTyv69CQudQ/DE1Ge2DAnydLcKP37OSyivoGhaCQYlRfvM9IyJqCyt1Ktl+8gIWv5+D0kuXsXwQ8NAbhxDVsT0W3pmMsX1jfZ09j0rPyMHre/NgFT8+9mxGLmaOSERaarLvMuaGAc/9B9X1DRWFvZ8Dbx4oxKjkrnh92kAf58y/2b5nF8qv2B+LjQjxi+8ZEZEz+u1K0ZDtJy/g4Y2fOvzQAEBR+RU8vPFTbD95wUc587z0jBy8usexQgcAVgG8uicP6Rk5vsmYix79x6etPpeZU4KZGw55MTfUmD9/z4iIZLBS56Z6q8Di93MgWnjO9tji93NQ37TWYwA1dVa8vjevzTSv781DTZ3VSzlyz+Waeuw+822baTJzSnC5pt5LOSIbf/6eERHJYqXOTQfzSpv1HDQmAFwov2Kfa2ckb2blN+uha8oqGtLpwXOSvYqy6Ug9/vw9I+fqrQJZX36P9459g6wvvzd05b6mzoo1e89hwXsnsWbvOd00msk7OKfOTSWVrf/QuJJOT/K/r5JKV1Aql87XZMtz7ttLHs4JNaXse9b2qS9kLP/JLcaSD8/4xTxLI85fJnWxp85NXcNCVE2nF//JLca7x76WStsjKtTDuVFHQme5fH7yZanu5grqnb9+z8i5x7cc84t5lkabv0yewUqdmwYlRiE2IgStbahgQkOrcVBilDez5XGPbzmGyivO55aZTcBUS4LnM6SCPypo6TKIepe/fs+odbYhVn+YZ2m0+cvkOazUuSnAbMLCOxsqA01/cGz/XnhnsmH20WorkLZk5ohE3exX1z4oAD+97irp9Ayi3uNv3zNy7kjBxTafN9I8yy2HCg01f5k8Rx+/tho3tm8sVt9/M2IiHId+YiJCsPr+mw01r8NZILUxm4DZI/U3z2PlL2+WTssg6l3+9D0j54raWDjTmBHmMxdevCyVTi/zl8lzuFBCJWP7xmJUcgwOfFGC73IPYO30gYY8UaKo/IrUTfPniTdg4oDuHs+Pp/y8fzdsPPiN03QMot5l+57xRAn/lp6Rgzf2n8Myib3AjTDPMr5Te6l0epm/TJ7DnjoVBZhN9jk9RvyhSc/IwZ/eOyGVNq6TvoNLzy4dpNIxiHpfgNkES6/OmNDvalh6dTbc94zaZlswIJwMRxppnuV9A+Ph7DbX0/xl8hxW6kiKvwVSBlEi7ZFZMNCYUeZZBrUzY+aIxDbT6Gn+MnkO7wByyh8DKYMokfbIbHgOAOEh7Qw3zzItNRmzRyY2a2zqdf4yeQbn1JFTSgLp8p/dYJhAaguSTTf7NJvAzT6JfEB2DuuEfnGGiUONpaUm44nRSXgzKx8FpVXoERWKqZYENi7JjpU6csqfAymDKJF2yM5hTegsNydWj4LamTFjRE9fZ4M0yqe/THv27MGdd96JuLg4mEwmvPvuuw7PCyGwYMECxMbGon379khJScHnn3/ukKa0tBRTpkxBeHg4IiMjMWPGDFy65HiM0/HjxzFixAiEhISge/fuWL58uaeLZij+HkhtQXTJhL6YMaInK3REPjLVksC5rk7wbFj/5tNfpx9++AE33ngjVq1a1eLzy5cvx8qVK/HKK68gOzsbHTp0wJgxY3Dlyo/7Dk2ZMgWnTp1CZmYmPvjgA+zZswezZs2yP19RUYHRo0ejR48eOHLkCP785z9j0aJFeO211zxePqNgIG0bgyiR9zhbhOXPc13TM3KQNH8bln6Yiw1ZBVj6YS6S5m/j6Td+xKfDr+PGjcO4ceNafE4IgRdffBFPP/00JkyYAADYsGEDoqOj8e6772LSpEnIzc3F9u3bcejQIQwYMAAA8NJLLyE1NRUvvPAC4uLisGnTJtTU1GDt2rUICgpCnz59cOzYMaxYscKh8kdtG5QYhQPnWt+Z3V8DKQ/YJm+orq5GdXW1/d8VFRUAgNraWtTW1rb4GtvjrT2vNyt2nsb6rAJYBRAcAASbG750tv81m4AHLD0wb9Q1himzkmu4YudprN9fgMAWwvD6T87BLOoxb3SS2ll0m9Hu06ZaKp8ny6rZOXV5eXkoKipCSkqK/bGIiAgMHjwYWVlZmDRpErKyshAZGWmv0AFASkoKzGYzsrOzcc899yArKwsjR45EUFCQPc2YMWOwbNkyXLx4EZ06dWr22a4EUBuj3aD+FkgZRPWvafmMUM709HQsXry42eM7d+5EaGjb0yMyMzM9lS2vSgLwfAubDS8d0KhnvO4cMjLOeS1P3iJzDZMALB/URgKN/22Mcp+2pnH5qqo8t2m9Zit1RUVFAIDo6GiHx6Ojo+3PFRUVoWvXrg7Pt2vXDlFRUQ5pEhMTm72H7bmWKnXuBFAbo9yg/hpIGUT1z1Y+TwZQb0lLS8O8efPs/66oqED37t0xevRohIeHt/ia2tpaZGZmYtSoUQgMDPRWVlVXU2fFgGczm63ADzYLLB1gxfzDZtQKEw7/aZThRgtkr+GbWflYtuOM0/d7asx1mpsmY5T7tDUtlc/WUeQJmq3U+ZIrAdTGKDeovwZSBlH9a1o+TwZQbwkODkZwcHCzxwMDA51eQ5k0WrbhwDlcrmt9Um+11YTqehPeOvyNYVeFOruG+RerUV3vfG/Q/IvVmr0X9H6fOtO4fJ4sp2YrdTExMQCA4uJixMb+uE1GcXEx+vXrZ09TUlLi8Lq6ujqUlpbaXx8TE4Pi4mKHNLZ/29I05U4AdSWtFvl7IGUQ1T9b+YxcRn8gu6WSP5/DLLtDAY81ND7NdrEkJiYiJiYGu3btsj9WUVGB7OxsWCwWAIDFYkFZWRmOHDliT7N7925YrVYMHjzYnmbPnj0O82oyMzNx3XXXtTj0Sg0YSNvGIErkHfyuOccdCsjGp5W6S5cu4dixYzh27BiAhsURx44dQ2FhIUwmEx577DE888wz+Pe//40TJ05g2rRpiIuLw9133w0A6N27N8aOHYuZM2fi4MGD+OSTTzB37lxMmjQJcXFxAIDJkycjKCgIM2bMwKlTp7Blyxb87W9/cxhepeYYSNvGIErkHVr7rtm2MPrT1uOY+cYh/OmdEz7fyojHGpKNT4dfDx8+jNtuu83+b1tFa/r06Vi/fj1+//vf44cffsCsWbNQVlaG4cOHY/v27QgJCbG/ZtOmTZg7dy5uv/12mM1mTJw4EStXrrQ/HxERgZ07d2LOnDno378/unTpggULFnA7EyemWhLwbEZum8eDeTuQvpmVj3PfXkJJZTW6hoegZ5cOPjvdwRZEX93T+pm4DKJE7rF975Niw5BzvrLVdJ78rtnyUFBahbPFlcg+V4qWwqKvtzLisYYE+LhSd+utt0KI1msNJpMJS5YswZIlS1pNExUVhc2bN7f5OTfccAP27t3rcj79ja8DaeMg2iMqFOfLL2PdJy2fP/vMh7lIjg3DvTd383oFj0GUyHNa2gOyJQ8N7YGnVP6u2WLQ20e/bjMGNmYVwKt78nD8q3K8MWOwTxp0PNaQNLtQgnzD14F02trsNjc5bkoAOHWhEqc+zPVJS5lBlEh96Rk5bfaC94kNw8SbYoGLOarvBSkbA1uTlVeK657ehiGJUbgmJszrMYFnw/o3VurIzteB9LU9eS0Oa8jyVUuZQZRIPTV1Vry+t/U4BAC5RZW4b+Bg/GenesdfudKobI1AQ+UuK6/hvXw9NEv+g90JBEBJII1X/XMnvZaFV92s0DVmaynzvEMi/Xkzq+WpFo1ZBbDlUKFqn2k7M1WNCl1LbA1OxiTyNPbUEQBlgbSzSp/p7jBHWwRg73Vk65hIP2S3SSq8eFmVWORshEJNr+3JQ1SHYDw4jIuoyDN4VxEAZYHUHbbtAFJX7sGrezxToWvstT15ePXjL3263QARyZPdJim+U3u3PqemzorX/vul1yp0QENjM33baSTN50gCeQZ76giAwkB60bXP8GTPXGtsQXTZ9tOc00KkA7LbKd03MN7lOXW+iEWN2YZjAY4kkLrYU0cA5Df4dHVOnW2Iw9dBlK1jIm3z5Ea6jefw+ioWNfbanjxculLn62yQgbBSRwA8G0gvXanz6hBHWzgcS6R9aanJmD0ysVlD02wCZo90rcddrcUQZhNgSYzClEHd0cPNIWABoO+iHT5rbNqmwyx476TPT8UgdXD4lexkNtNtfIauDNtWJe761bAExEa2t58o8XVZFXIvXFL8Pr4ejm26sTL3tCNqmZp7QKqxGKJPK5ucqzGU64uh2Jbyza1X9I+VOnKgtUBqSYxqdc85d/aV8sWcFgZRImXU2ANSZrumtrQVg4DmMbOto8Ta8vrePDwxOskrjbzWYjPn+ukfK3XUjB4CKdCQz7dmWdxqKXtriwEGUSLvu1xTj/v/fsCl2KDkuL+mMbOmzor1+/Lw3PbT0p9nFcCcTUewakp/j8YimdjszQomqYtXjFR3uaYev3wty+VAOntkIv4x2yIdUNJSk3F66Tj8cazyUy68scWAbBDlfBYi9czccAi9F2zHkcIyRa8zAUgbl4TTS8e53NAKamfGrFt7YfbItucpN5WZW+Lx7U5k9yR9MyvfY3kgz2FPHalq5oZDyMwpUfy61uaryLIF0e+rql0a8rX1mJlFPdQ9AE1ZEOVxY0TuczUOAcCskYmYfUsvVfJhqxQqOQKxce/970Zdo0o+GpPdk1Q2HWkLe+pINa4EUhMaeuY+/O1IzBjR0+3u/tZWzclan1Xg1ue3hEGUyHsu19S7VKGzxSK1p0GkpSbjxKIxil/nqd572T1JZdORtrBSR6pwJZCaAJxYNMYjQdTV4VhP7F3FIErkPc+5MHRpSYzCmWdcH251pmNIO8VDsVYB/O6fx1TPi+yepFMtCap/NnkeK3WkClcC6ayRiegY4pkZAK7OafEEBlEi78n/Xr7H25U5vK5yZRRh99lvAQArdsovuHDGk3uSku/xqpFbbJtX/idXvpfOnQ1ElXJ1OJZBlEh/auqsqKqWO6Ghf3ykW4shXGEbRRjVu6ui163dX6Dq4glPbO5M2sCFEuQyV7YS6R8fiX/M8nyruDHbPlJKthhYu78AVlOAasFNZmNnInKd0ni08VdDfNKQCmpnxqop/ZE0f5ui2Kn2NiNq7klK2sFKHblkxc7TeHVvoeLX+TKQKl0dyyBKpA+L3z+FdZ/kS6cfldwV7YMCPJchJ2y990pW6ntihbwae5KStrBSRy5pWCWqbEzT14EUULbFgFUA6/flYdat6mxvADCIEqlt7qYj2HH6e+n0o5K74vVpAz2YIzmt9d635dy3yo9GdAWPM9QvVurIJUpXiWolkAINwfRSdT02ZTvvaXxu+2l8X1Xt8eFRBlEi1/z38+8g08DsHx+Jjb8a4vOGZWO23vs5m44gU2Je8qaDX6FjSDuPxiMeZ6hvrNSRx8RGhCCld1f8MTVZU4EUAHp26SCd1tPHeDGIEil3paZeUfo+V0doLg4ByufYeTIe8ThD/WNXgJ+xrVZd8N5JrNl7TvHmlkoC6a+GJ2Lp3ddrMpDKbDPS2Gt78nDpityqOiVsQbRpMLcFUU8eF0TkS+7GohcyzyhKr+V9IGVWyDfmiY2JeZyhMbBS50fSM3KQNH8bln6Yiw1ZBVj6Ya7icwaVBFIt77umNIgKAH0X7VC1ksUgSv5KjVi05397uMnQwz6Qtm1GZNqaVgFMW5ut6ufzTFhjYKXOT6jVIyQbSK+ObK/5OWFKgqiNmr1nDKLkj9SIRekZOThffkX6M/WyD2RaajImD46XSnvgXKmqjUweZ2gM2r/LyW1q9QgpCaQ/TbpKOn++lJaajD+MU3acmFq9Zwyi5G/UiEUy79HYg8MSdDUPTMl8XzV78nmcoevcnUqgJlbq/IAaPUJKA+kfdRREHxym7MQJtXrPGERdo6UASsqoEYtk3sPm9qSrsPDOPvIZ1AAl833V7MnncYauUWMqgZpYqfMDavQIKQmkWtiPTgml8+sAdXrPGESV01oAJWXcjUU1dVZsO1kk9R7dOrXHmgcGSedNK5TGI7V68nmcoXJaXOjGq+MH3O0RUhpItbIfnRJK59d1i2jv9mcyiCqjxQBKyrgTi2wV+sMFF6Xe48GhCUqypilpqcmwJEZJpb1Qdlm13mqeCStPqwvd+GvhB9zpEfK3QHog7XaptOnbT6tSiWAQlaPVAErKuBqLWqvQK3kPvXljxmCpRmZmbomqvdVpqck4vXQc5o/vjWmWHpg/vjdOLx3HWNSE7FSCLYeUH6fpDm4+7AdkzhlsqUeotY0oW2OEQNohWO4rIaDeZpw8E9Y5JXOxpg3p7p1MkWKuxCKl83lbeg89CmpnxoNDewB155ymVXtzYB5n6JzssHfhxcvo7OG8NKbvu56kKe0R8tdAavPQ0B5Sk5XV6h2yBdElE/pixoiehvk7qoUrhY1DaSxSMp/XaD3c80Y3rMyXXTjB3mrvkZ1KEN/J/ak6SrCnzo8o6RFSGkiNdqTVvNFJiOwYivRtp9tMZ+sdYqvWs7hS2FiUxCLZivqAHpHYPNNiyAbR4T+NwmP/PO70fFjGI++5b2A8ln6Y22Yas6kh3X92em++Lyt1fka2W/3cdz9IvZ+RA+k3ZZel0m07eYHDpR4mG0CnWhIAoexMUPIN2Vh0daRcT8e4vrGG/Q4GtTMjVvLvcO7bSx7ODdnO63bGF6NXmv4G1NfXY/78+UhMTET79u3Rq1cvLF26FEL82IUkhMCCBQsQGxuL9u3bIyUlBZ9//rnD+5SWlmLKlCkIDw9HZGQkZsyYgUuXeOO3Jj0jB5uz5SZ3GjmQyvb6HC4o47YaHpSekYMbFu9wms5Iw//UID0jB8u2t91bDhhjPq8zsvFo08GvGIs8SGbRji+nAWg6Ai5btgyrV6/Gyy+/jNzcXCxbtgzLly/HSy+9ZE+zfPlyrFy5Eq+88gqys7PRoUMHjBkzBleu/HjywZQpU3Dq1ClkZmbigw8+wJ49ezBr1ixfFEnzbDeszMir0QOp0k1Aua2G+rQeQMlzlKx49YcKvZJ4xFjkGTJzzU0Aji8c47N4pOnh1/3792PChAkYP348ACAhIQH/+Mc/cPDgQQANvXQvvvginn76aUyYMAEAsGHDBkRHR+Pdd9/FpEmTkJubi+3bt+PQoUMYMGAAAOCll15CamoqXnjhBcTFxfmmcBqkdHGE0QOpzEq9pl7fm4cnRicZ+u/iLUoCaMcQTYcyt1VXV6O6utr+74qKCgBAbW0tamtrW3yN7fHWnteymjorNuw/h+A29jAPNjfU9n5l6Y7HRl2jy3I60/gaBgYCs4fHY+3+AqnXbth/Do/e1stjsaimzoothwpRePEy4ju1x30D4136LD3dpxuz8hFodt7K+OfBPHuHR0vl82RZNR0Jhw4ditdeew1nz57Ftddei88++wz79u3DihUrAAB5eXkoKipCSkqK/TUREREYPHgwsrKyMGnSJGRlZSEyMtJeoQOAlJQUmM1mZGdn45577mn2ua4EUBs93aBNydywRg+kTa/f70ZdA7Oox/qsAumFIxv3f+mRHkx/C6KuBFCgefm0Xk4Z6enpWLx4cbPHd+7cidDQtoflMjMzPZUtj3pecg/za+vzkZGR79G8+JrtGiYBWK7gkIz/7NzumQz9T+f//YeLcHsxgB7u086Q/PtfzEFGk57SxuWrqvLcKn2TaDxBTWOsViv++Mc/Yvny5QgICEB9fT2effZZpKWlAWjoyRs2bBjOnz+P2NhY++t+8YtfwGQyYcuWLXjuuefwxhtv4MyZMw7v3bVrVyxevBgPP/xws89dtGhRiwF08+bNTgMoEWlLVVUVJk+ejPLycoSHh/s6Oy5pqaHZvXt3fPfdd62Wqba2FpmZmRg1ahQCAwO9lVVVLPkgB/88/FWbaYLNAksHWHVZPlmtXUOZvw8A3Nw9An+fPkjV3roVO0+32Vv40NAe9q1YZOjpPn0zKx/Ldpxxmu6pMdc59NQ1LV9FRQW6dOnikZik6Z66f/7zn9i0aRM2b96MPn364NixY3jssccQFxeH6dOne+xz09LSMG/ePPu/bQF09OjRTi+Anm7QptZ9koe/ZJ5tM43RA2lb10/2Cw0oD2xt8dcg6koABZqXz9bTrmfBwcEIDg5u9nhgYKDTayiTRkvSM3KwMftrCMlD+/RWPlc0LWNClzBU1zv/+2TlV+D6Jf9RbcupmjorXt1XCKto/bNf3VeIx8f0UVyR1MN1vH9oLzyz7azTOb73D+2FwCblb1w+T5ZT05W6J598En/4wx8wadIkAMD111+PgoICpKenY/r06YiJiQEAFBcXO/TUFRcXo1+/fgCAmJgYlJQ47u1TV1eH0tJS++ubcieAupJWC2xLtNv6sgI/boKpt/Ip1VL5ZL7QNqv3FsJqCnA7kPpzEHUngAI/lk/LZSRHSk6xkV00YERTLQl4NiNXKhapedqEkpNdjLhXnqunM3mTpmdzV1VVwWx2zGJAQACs1oYdsxMTExETE4Ndu3bZn6+oqEB2djYsFgsAwGKxoKysDEeOHLGn2b17N6xWKwYPHuyFUmifklVmD1h6eD5DGmX7QstSY3d3JUHUaGT+3r4OoKQepQu1GIvkYxGgTjziyS7aP69b0z11d955J5599lnEx8ejT58+OHr0KFasWIGHHnoIAGAymfDYY4/hmWeewTXXXIPExETMnz8fcXFxuPvuuwEAvXv3xtixYzFz5ky88sorqK2txdy5czFp0iSufIV8IDUBmDUyEfNGXYOMDOdnERqV7Qv7msS2L2q0WP09iNr+3g29yD8+bsRTTPyd7Ck2jEUNWvtutEaNeMSTXRpo+bxuTVfqXnrpJcyfPx+PPPIISkpKEBcXh9mzZ2PBggX2NL///e/xww8/YNasWSgrK8Pw4cOxfft2hISE2NNs2rQJc+fOxe233w6z2YyJEydi5cqVviiS5sgG0rSxSZh1ay9DrCR0V1pqMi5V12OTxAbN7u7u7q9BtKbO6hAwjy8cgy2HCjUXQKllTa+fzPWSbZhMHtQdaanJjEX4sXIx+fUDOFxw0Wl6d+ORzLCvUfcvbeme1uIQs6YrdWFhYXjxxRfx4osvtprGZDJhyZIlWLJkSatpoqKisHnzZg/kUFtcCaSyx4F9XS53ZJa/6Nmlg1S6TQe/QseQdi73KPljEP1xfuePjz2bkYuZIxKxZEJf32WMpLR1/dr6HsgeB9bzqo7uZtFQgtqZMa5vjFSlzt14pIc5ZZ7g6j3tC8b6y/ux9IwcJM3fhqUf5mJDVgGWfpjr9OgqJceBGa0nyF3e2t3d3+aVtTa/kyd26IOr14/HgbnHm6dNaH1Omdr0FpOM8Uvg51y56XgcmHu8uWjCX4KozPxONSZ7k2e4ev14HJj7vL2IKy01GaeXjsP88b0xzdID88f3xuml4/DE6CSs2XsOC947iTV7z+n+u6rHmKTp4VdyTvama3x0FY8DU4c3F020NjEXANbsPWeIuWb+vl2C3rly/ZQu1DJKA8YTvL2IK6id2eH1ehqilKXHmMRKnc65ctMpXWWm1y+kNyhZNOHuClWjB1F/X+mrd65cP6ULtaht3oxHjbW2v6Cae+T5gh5jkj6b9GTnyk2ndJUZtU120YSa8xL1Ns9Dhr+u9DUKV66fbCziQi153o5HehyilKXHmMRKnc65ctPJvoarzOTITFI2Abg2Ogz1Mt0SThg1iMr8HTm/U7tcuX56/NHUOm/HIyNvkK7HmMRKnc65ctNNtSQ4PVVRazeqlslMUhYApq49iOHLdmP7yQtufZ5Rg6i/rfQ1GleuH2OR+rwdj/Q4RClLjzFJOzkhl7hy0/1lp/OtA7R2o2pdaytUmyoqv4KHN37qViA1chD1l5W+RqX0+jEWeYY345HRe1v1FpO4UMIAlBylJHNgthZvVD2wrVB9Y38+/pp5FlW19c3SCDQMfSx+PwejkmMQ4MKp5EYNorbNsy/XWvGHMUkQJuCbssu6X9Xrb2SPUGIs8ixvxSOjbpCu11NtWKkzCJlAKjMXy2wCnhid5OnsGlZQOzP6Xh3RYgC1EQAulF/BwbxSWHp1VvwZRgyiLa3ktTVKtLJVAMlrulK7KcYi71ASj/Z//h1GXHeVS59htFMm9HyqjX7+yuSULZAumdAXM0b0bPYlMupcLK0pqbwile78RdeGR/U4z6MtRlzJS21jLPIe2Xg0bd1Bl79rehuibIve4xF76vyIkediaUnXsBCpdL97+zjOllS6FPCUDLlrmSubZ5P+MRZ5j2w8EnBvTznZYXctM0I8YqXOjxh1LpbWDEqMQmxECIrKr7S5s7twc2POtoJo0/kgWg2uetyxndzHWOQ9svHIxp1KS2vD7oxH3sNKnQ65+gWZaknAMx/mtvnF1ttcLC0KMJuw8M5kPLzxU6n0agdRPZ00wR4bfXM1Ft03MB5LP8xtMw1jkTqUxiO1Ky2MR97FSp3OuPMF4fYB3jO2byxW338znvy/z1B5pfVJyoC6QVTmuJ7fjbrG7c9RC3ts9MvVWGR7nTOMReqxxaN5Wz5rc9GEjVqVFsYj7+M3RkfcmcBpe21bvXR6m9CqdWP7xuLuft2k0p779pLbn6fHkyb0uGM7uR6LWntdY3qcXK8HY/vG4vFR10ql7RbR3u3PYzzyDVbqdMKdLwi3D/CdhM5yLbpNB79ye1WV7HyQLYecH/btDbahu6TYsDbTscdGW1yNRTKvMwE4vnAMK3QeMn2o80oLAKRvP8141AqtxyPt5owcuLMFALcP8B2Zlp+Nu8vlZYdMCi/6/nD09IwcJM3fhqUf5iLnfGWLadhjo02uxhOZ1wlo50feiGS2QwJ+XAnLePQjvcQjzqnTCXcmcBph8qdeyWzM2Zg7iyZk53nEd2oPXFT89qpxdpJAn9gw3HtzN82ukPN3rsYTxiFtaG07pJYwHukvHmk/hwTAvQmcRpj8qWe2jTllOuzc6TGVnQ9y38B4l95fDTJDcLlFlboJoP7I1XjCOKQdaanJeGqs8+k2jEf6i0f6yCW5NYFT5kuj9cmfepeWmozJg+WCl6s9FTJDK4MTolx6b7VwKoD+uRqLploSnDZsGIe855syuWHPt49+7dL7Mx75Bit1OuHq0VDpGTm4YfEOp++v9cmfRtCzSwepdGeLW57TIaO143pssvJKMeDZTJff310cgtM/V2MRt1TSFtke0ZzzlaofH2bDeKQ+fnt0ROn5etw+QFtkF00cOFfq1gTltNRknF46DkN6ttwKtt0PKyR+ZNXGIThjcDUWcUsl7VCyiMudrUcYj7yLCyV0RvZ8PSXbB3QM4W3gDUoWTahxvuDBvNI2n1+fVYDHx/Txas/IBYkhHw7B6YOasYhbKnmfknikxgbpjEfewZ46HbIdDbVkQl/MGNGzxS8Btw/QprTUZFgSnc8jsQpg/T65FbMt0eJckfSMHPz9E+efxyE4/VArFult3pJRpKUmo4+TfdlsXJ1bB2jzHjBqPNJPTkkRI84VMIprYuSC6HNubACqtesv01sDADOGJ3AIzmC0di+So3tvljv1xp25dVq7B4wcj1ipMygjzhUwCiV/c1c3ANXa9ZdpqQNAnArHE5G2aO1eJEfemFuntXvAyPGIlToNq6mzYs3ec1jw3kms2XtO0ZfJCGfYGZWSIAq4FkhlPsMEoM4qvHL2otZa6qQMY5FxyZ4yAbg+RMp45D2s1GlU42NLNmQVYOmHuUiav02618bVbQfI85QEUcC1QCrzGQJA+rbTiu4rV9TUWaUmJAPsrdEixiLjUzK3zpWKjpbiEQBcHSnXA6fHeMRvkQa1thWJVSgbjntidFKLy8i5jYnvKTllAnAtkDrbI8pG6X2lhK1CkJlb4jQte2u0R41YVFNnRdewECTHNa80MBZph+zcOlcrOlqIR0DDPb1su/PtU/Qaj1ip0xiZCZwyw3G2H9MD535cRm4CYEmMwuml4xhENSAtNRl/GCe3jYM7gfT00nH4o8SRQO7sRdUSmX0SG2NvjbaoEYtaOijdhIbzNOeP781YpCGyQ6TuHOvly3gEKItJeo1H+suxwamx9Lu1G1egYQdvmZ3dyTseHOa85QoA58vlhi9bEtTOjIAA5x+i5pYCsqvLAPbWaJW7saitOHTqQiVKKq/o8kfTqGSHSG9YvMOtXjRfxCNAPiaZoO94xG+UhtTUWbHtZJFU2taG49Tq6SPvkJ1ft2ZfvluBVHb4dtvJC6rcG7Kry0b17sreGg1yNxYxDumTzBCpGsOj3o5HgHxMShubpOt4xEqdRtiGKQ4XXJRK39pwnBY3eaS2paUm41fDEpymc+dHUHb49nBBmSoTlWWDdmxke/bWaIwasYhxSL/SUpNxfOEYp+n0FI8A+Zj0tRujIlqg+Wj6zTff4P7770fnzp3Rvn17XH/99Th8+LD9eSEEFixYgNjYWLRv3x4pKSn4/PPPHd6jtLQUU6ZMQXh4OCIjIzFjxgxcunTJ20VpldK5R21N4Nz6qdyu33pcqm1ksRKrsdz5EVSyjYoaLXEjry4zMrVikZG3jPAHMicN6SkeAf4TkzRdqbt48SKGDRuGwMBAbNu2DTk5OfjLX/6CTp062dMsX74cK1euxCuvvILs7Gx06NABY8aMwZUrV+xppkyZglOnTiEzMxMffPAB9uzZg1mzZvmiSM0omXtk09oEzpo6K3IuVEq9h95vXKOR/XFz9agepduoAK63xI2+usyo1IxFsj+gsunIu4wUjwD/iklSJ7lXVFQofuPw8HDFr2lq2bJl6N69O9atW2d/LDHxxxtBCIEXX3wRTz/9NCZMmAAA2LBhA6Kjo/Huu+9i0qRJyM3Nxfbt23Ho0CEMGDAAAPDSSy8hNTUVL7zwAuLi4tzOpztkx/mBhhtu5ojWJ3C+mZUPmbcyQf83rtHIVrJtR/W4MufD9poN+89JpXflEG9bT48Mva4uc8ZX8dJdasYik+T7yKYj79JqPJqz6QhWTemvKG74W0ySqtRFRkbCZJLfAt9kMuHs2bPo2VP+x6Al//73vzFmzBj8/Oc/x8cff4yrr74ajzzyCGbOnAkAyMvLQ1FREVJSUuyviYiIwODBg5GVlYVJkyYhKysLkZGR9godAKSkpMBsNiM7Oxv33HNPs8+trq5GdXW1/d+2IF1bW4va2to282x73lk6m69KLyE4wHlku7l7BP4+fRCC2plbfW/Z90qKCYNJ1KO2tl4qj40pLZ/e+Kp8kwZcjRd25Ej9qG7Yfw6P3tbLpeDzu1HX4JERPfDf3f9BsNn5h/3n1DeYNOBqqc+qqbNiw/5zCA5oO50JwINDe2DeqGs88ndueg29fS09ES9diUm+jEXny3+Qeq/z5T+4fH2MHosAxqOm9pwtxo2LMvCApQfmjXa+LYoWYlJL19CT19MkhHD6lzSbzXj77bcRFdV8I9umhBBITU3FyZMn3a7UhYSEAADmzZuHn//85zh06BB++9vf4pVXXsH06dOxf/9+DBs2DOfPn0dsbKz9db/4xS9gMpmwZcsWPPfcc3jjjTdw5swZh/fu2rUrFi9ejIcffrjZ5y5atAiLFy9u9vjmzZsRGsphSyI9qaqqwuTJk1FeXu6VHjFPxEvGJCLj8GRMkuqp69GjB0aOHInOnTtLvWnPnj0RGBjoVsYAwGq1YsCAAXjuuecAADfddBNOnjxpr9R5SlpaGubNm2f/d0VFBbp3747Ro0c7vQC1tbXIzMzEqFGjpP4GNXVWDHg2s80WkdkEHP7TKKctoT9vO403sgvaTCP7Xq1RWj698XX5fr56P3KLnc+LTIoJw//9eqhLn2Er48IjZlyul+9R6h0dhrv6xeG+gfEO98+KnaexPqtAeujul4Pi8afU3kqzLa3pNXRlONQdnoiXrsQkxiL983UZtRyPTADmjboWUwb3aHYPaSkmtXQNPRmTpCp1eXnKJs+ePHnSpcw0FRsbi+Rkx7H63r174+233wYAxMTEAACKi4sdeuqKi4vRr18/e5qSEscjiurq6lBaWmp/fVPBwcEIDg5u9nhgYKD0F0s2bWAgMG1ozzbH/GePTESH9s3z01h6Rg5e218IODl4Sua9ZCj5W+iRr8p3503dcezDXKfpPvvmEl7I/Nyt/ZR+OTgBq/c6X+Vmc+z8JRw7fxbPbDuLh4YmIDayPd4++vX/TgqQD8bdozp65W9ru4bevo6eiJfuxCTGIv1jPGrZc9s/x/M7PrfHo4LSKpwtrvzfSUraikmNr6EnP0vTMwKHDRvWbNj07Nmz6NGjB4CGRRMxMTHYtWuX/fmKigpkZ2fDYrEAACwWC8rKynDkyBF7mt27d8NqtWLw4MFeKIVzrW34KLvTvuyqtRnDE3S9qaI/ULLU393NW+eNTpI6i7EpqwD+/km+w9FPsoywuszIGIuoMb3Fow1ZBQ5HY8owWkyS6qlr6tChQ/joo49QUlICq9XxIq5YsUKVjAHA448/jqFDh+K5557DL37xCxw8eBCvvfYaXnvtNQANE4wfe+wxPPPMM7jmmmuQmJiI+fPnIy4uDnfffTeAhp69sWPHYubMmXjllVdQW1uLuXPnYtKkST5f+dpYWmoynhidhDez8lFQWoUeUaGYakmQGpqQXbUWF8HtA7TOttRfZrWWVQDT1mbjrVkWlz/Pdt9Nfv2A9Gaz7jDC6jKlvBUv1cJYRDa+ikdzNh1BZm6J8xeowGgxSXGl7rnnnsPTTz+N6667DtHR0Q6rvJSs+JIxcOBAvPPOO0hLS8OSJUuQmJiIF198EVOmTLGn+f3vf48ffvgBs2bNQllZGYYPH47t27fbF1kAwKZNmzB37lzcfvvtMJvNmDhxIlauXKlqXtUQ1M6saPsIG270aSxpqcnY9/l3OCWx5+CBc6UubylgE9TOjHF9YzxaqXO2BYZReTNeqomxiGx8EY9WTemPpPnbpOfEucKoMUlxpe5vf/sb1q5diwceeMAD2WnujjvuwB133NHq8yaTCUuWLMGSJUtaTRMVFYXNmzd7InuaILunEDcc1o97b+6GUxJzWYCGYY8nRie51dqcaknAsxm5Hgmio3p3Vby3lFF4O176GmORMXk7HinpIXSFkWOS4hKZzWYMGzbME3nxCzV1VqzZew4L3juJNXvPqXJYscy8B6PNGzA6pcfouHuGpis7vMswm2DY4ClDy/GSsYhkeTseAa3P73SX0WOS4lI9/vjjWLVqlSfyYni2g7JtEzqXfpir2mHFgxLb3hPLaPMGjE5pJUuN4SxPBFF/v++0Gi8Zi0gJX8QjoCEmnV46Dn8c63yjYVlGv/8UD7/+7ne/w/jx49GrVy8kJyc3W5q7detW1TJnJK0dVWI7rBiAS2P76Rk5eH1v6wdwG3XegD9IS03G8a/KkZXnfDXXWYm9pGQ/0zZJ/sftSpTjfddAi/GSsYhc4Yt4BDRUKGfd2gvfV1W7NRzrL/ef4urqo48+io8++gjXXnstOnfujIiICIf/qDmZZf6uLAe3BefWgqglMQqnl44z/E1sZG/MGCy125JtgrIabJPkMx4dqbjnrk9sGOaP78377n+0Fi8Zi8gdvohHNq6MJFgSozDN0sOvYpLinro33ngDb7/9NsaPH++J/BiSzDJ/pYenywTn7Hxl+/WQ9gS1M2PWSLkJw2pMUG6qpe0tzpdfxrpPHO9pb7aCa+qsLm234Qtai5eMReQOxiPtU1ypi4qKQq9evTyRF8PyxDJ/TwRn0ibZYQ9PXe+Wtrd4amxvn1SsWhriezYjV7MBXGvxkrGI3MV4pG2KK3WLFi3CwoULsW7dOh4kLckTy/y5H5R/uSYmTGoui7eut6v7mLnDU3PBPElr8ZKxiNTAeKRdiit1K1euxJdffono6GgkJCQ0m/j76aefqpY5o5DZA0zpMn/uB+VfZK9jN4Pu1C87F+yJ0UkKTnz0PK3FS8YiUoO/xyMtU1ypsx2/RfJkNlJUusx6qiUBz3yYi7ZGPbgflHHIbg6cvv00vq+q1lyPlbuUDPFNG9LdO5mSoLV46YlYdN/AeCx1sjEtY5Gx+Hs8stHi/F7FlbqFCxd6Ih+G82ZWPvIvVtsvtO2mbjofyNUJnX/ZedppGqPvx+NPZHdYF9DuUKQ79DrEp4V46clYZJvj6AxjkbH4ezwCtDu/V3Gljtq2YudpJAFYtuMMqusbBoIaX2hXD8purLW5RY3NHqnNiePkutZ+jFviiZVnvvR5kdy+Vxzi+5GnY5FMHPLnVYhG58/xSMvze6X+wlFRUfjuu++k3zQ+Ph4FBQUuZ0qvlvz7FNbub15u24VOz8ixT+hcMqEvZozoqfgml5lbZDYBT4xWbwdu0o601GQ8JbG7ulpH9WhBTZ1VaksME7QxxKeFeJmekePRWCQTh0wAji8cwwqdgflrPPLEXo9qkeqpKysrw7Zt26Q3y/z+++9RX1/vVsb0Zsb6g9h1+lsEB7SeRo3WCrcPoG/KLkul09pQpKtk7nkAGNwzCkHtzKit9W3s8XW8tP3oBLYRZtyNRTLXRADYcqiQccjgGI+a8+VvsPTw6/Tp0z2ZD12bueEQdp3+1mk6NS60XucWkXr8beXZ1k+/lkp3bXSYh3Miz5fx0hs/OoxDZONv8Ujr975UM81qtSr+r2dP/2idXa6pR2ZOiXR6dy80tw+gqZYEqaNy0refVv2oHm+rqbMi54K+5tP5Ol5640eHcYhs/CkeAcDVkXKVU9l0ajPGrEUfek7hTepukLtvYLzTNNw+wNhsK8+csa0803MgfTMrv81te2y0Mp9OC7xR4ZpqSXC6HyDjkH/wp3gEACaZgKQgndpYqXNT/vfyrV13g1x6Rg5uWLzDaTpuH2B8Sg639uWkXXfJ9ib1jgvjPf8/Mj0n7sYibqlEjflLPAKAr8vl5hDKplMbv3FuSugs39p1J8jZllA72wmeW5n4D39YeSbbmzTxpm4ezol+yPScqBGL2uqIYBzyP/4QjwDtb68kvVDi/PnziIuL82RedOmPqcl480Ch03QPDU1wOcjV1FnxmpP9oGzbB3QM4daD/sToK88uSJRPi8N8vo6XaanJMIt6oO6cw+Pu7hsnE4u4pZL/Mno8Ss/IkTrz1pcxSbqp1qdPH2zevNmTedGl9kEBGJXctc00tyddhQV39XH5M6avyXY6r8i2fQD5FyNPWK+ps2Lt/nyn6R4c5vujeZrSQryc97+K1VNjrsM0Sw/MH98bp5eOc6sHTSYW6b0nhlxn9Hgkc3oK4NupB9Kf+uyzz2L27Nn4+c9/jtJS5zVVf/L6tIGtVuxGJXfFmgcGufzesi0DQL+tH3Kd7Mqz8z6a3+EO2f3p4jS4VYKW4uVUS4LLm503xlhEzjAeAUN6Rvl06oH0N/yRRx7B8ePH8f333yM5ORnvv/++J/OlO69PG4jcJWMxaWDDYeKTBnZH7pKxeH3aQJffU0nLANBn64fcI7vybM2+fN2tOtP6flBtMVq8ZCwiGYxHvt8vU9EErMTEROzevRsvv/wy7r33XvTu3Rvt2jm+xaeffqpqBvWkfVAAnh6fjIyMfDw9PhmBgW0cLyFBtmUAaHNeEXlHWmoy6usF/v5JfpvpbKcISDSkNUHrE5KdMVK8ZCwiWUaNR3oZWlY8q76goABbt25Fp06dMGHChGZBitSjpAeC2wf4t1iJjS5tc52mDenuhRy5Rw8TkmUYJV4yFpESRotHgH4WbSmKMK+//jqeeOIJpKSk4NSpU7jqqqs8lS+C/I7Uvh7DJ9/T81BlU3qZkOyMkeIlYxEpYaR4BOhr0ZZ0pW7s2LE4ePAgXn75ZUybNs2TeSI09FTI/LCZTcCGhwZ7IUekZbJd/meL5YY0fUkvE5LbYqR4yVhEShkpHgH6WrQlXaWsr6/H8ePHdR+g9EBmo2EbLfdUkPfIrjrLPleq+d3ct376tVQ6X09IbotR4iVjEbnCSPEIkI9JWuh5lP4GZmZmols37truabJDTyZw13b6UVA7MwYnRDlNp/X9DGvqrMi5oO8FEoAx4iVjEbnKKPEI0F9MYrNKY2S7edPGJjGIkoNrYuR6rt777LyHc+K6N7PynW5uCzRUJHw9IdnoGIvIHUaIR4D+YhIrdRoj233rq8OCSbtkW4mnJbcK8QXZ+793XBiH+jyMsYjcYYR4BOgvJvk+B+RAL3vhkPbIzmOx0eJcFtn7euJN+h7a1APGInKHEeIRoL+YxEqdxsh8EbSwFw5pj+xu7jZanMsy1ZLgdDNS3v/ewVhE7jBCPAL0F5NYqdOgQYltTzDlKjNqTVpqMvrEys1lKbyovWGzv+w87TQN73/vYSwid+g9HgH6i0nayAUBaNg+IGn+Nhw41/JO+mYTV5mRc/feLDcMEN/J93sqNWbbPqOtScm8/72DsYjUotd4BOgzJumqUvf888/DZDLhsccesz925coVzJkzB507d0bHjh0xceJEFBcXO7yusLAQ48ePR2hoKLp27Yonn3wSdXV1Xs5925ztB2VJjMLppeM0dfOQNsnOZblvYLznMyNJZvsMswl4YnSSl3LkvxiLSE16jEeAfmOSbip1hw4dwquvvoobbrjB4fHHH38c77//Pv71r3/h448/xvnz53Hvvffan6+vr8f48eNRU1OD/fv344033sD69euxYMECbxehVTI3T3a+83MwiQDlc1m0QGb7DNtZkeQ5jEWkNj3GI0C/MUkXlbpLly5hypQpeP3119GpUyf74+Xl5VizZg1WrFiBn/70p+jfvz/WrVuH/fv348CBAwCAnTt3IicnBxs3bkS/fv0wbtw4LF26FKtWrUJNTY2viuRArzcPaVdaajJmj0xss4U84NlMpGfkeC9TbTDaWZF6xVhEnqC3eAToNyZJn/3qS3PmzMH48eORkpKCZ555xv74kSNHUFtbi5SUFPtjSUlJiI+PR1ZWFoYMGYKsrCxcf/31iI6OtqcZM2YMHn74YZw6dQo33XRTs8+rrq5GdXW1/d8VFRUAgNraWtTW1raZV9vzztI1lv9dJYIDnG9v+FXpJUXv6wmulE9PjFS+3426Bo/e1guzNx7GofyL9seDzQ33WqBJYP0n52AW9Zjn4yGEhE7BUt+BhE7Bir+DRriWrsQkxiL9M1IZ9RSPAPViUkvX0JPX0ySEkNks2WfeeustPPvsszh06BBCQkJw6623ol+/fnjxxRexefNmPPjggw7BDgAGDRqE2267DcuWLcOsWbNQUFCAHTt22J+vqqpChw4dkJGRgXHjxjX7zEWLFmHx4sXNHt+8eTNCQ7knE5GeVFVVYfLkySgvL0d4eLivs+MSxiQi4/BkTNJ0T91XX32F3/72t8jMzERISIjXPjctLQ3z5s2z/7uiogLdu3fH6NGjnV6A2tpaZGZmYtSoUQgMDGwz7Yqdp7F2f4FUnswm4PCfRvl82bSS8umR0cr3ZlY+lu044/BYsFlg6QAr5h82o9raMB7y1JjrfLrPksx34aGhPaRa8E2voa1XS89ciUmMRfpntDIaKR4BcjGppWvoyZik6UrdkSNHUFJSgptvvtn+WH19Pfbs2YOXX34ZO3bsQE1NDcrKyhAZGWlPU1xcjJiYGABATEwMDh486PC+ttWxtjRNBQcHIzg4uNnjgYGB0l8sZ2lr6qxYvbcQcLqtYYPZIxPRoX3zPPmKkr+FHhmlfPkXq1Fd3/I9Vm012Z/Lv1jts/LW1Fnx6r5CWEXr3wUTgMfH9EGggoqE7Roa4Tq6E5MYi/TPKGU0SjwCgBnDE/DU+D7S79v4GnqybJpeKHH77bfjxIkTOHbsmP2/AQMGYMqUKfb/HxgYiF27dtlfc+bMGRQWFsJisQAALBYLTpw4gZKSEnuazMxMhIeHIznZd0vy133S9gqzxrS2Dw7phx6OepKZnC/AyfmewlhE3mKUeAQAcRHa21cP0HhPXVhYGPr27evwWIcOHdC5c2f74zNmzMC8efMQFRWF8PBw/OY3v4HFYsGQIUMAAKNHj0ZycjKmTp2K5cuXo6ioCE8//TTmzJnTYsvXWzJzip0nAnBzfCSDKLlsqiUBz2bkthmkTPDtHlF6XWVmFGv3yVXqGIvIXYxHnqfpnjoZf/3rX3HHHXdg4sSJGDlyJGJiYrB161b78wEBAfjggw8QEBAAi8WC+++/H9OmTcOSJUt8mGug8PsfpNIpORCZqCmZPaIEgBsW7/DZdgJ6aL0bVXpGDoorq50nBGMRuY/xyPM03VPXkv/+978O/w4JCcGqVauwatWqVl/To0cPZGRkeDhn8p55/xRKLsntkTe6d8vz/ohk2XpXXt/b+ikBVgG8uifPIb23XChzfuajlg7MNgqZjYYbYywiNTAeeZbue+r0Jj0jB3//JF86/QPD9bcTN2lPWmoyji8c4zTd63vzUFNn9UKOGsh+H7R0YLZRyM4dAhqGxBiLSC2MR56jzVwZlNKWcXJcmGZvHNKfLYcKnabx5mkBst+HGcMTOJfLA7Z++rV02lkjtfsjRvrEeOQZ/JZ6kZKWMQBMvKmb5zJDfkdrE4D1vspMz2rqrDh1oVIq7ZCeUZr+ESN9ko0zbx+Vb3y4wyjxiJU6L1LSMtbymD3pk+zE3rPFcj/27tJaJdOfTF+TLZ12w0ODPZgT8ley8SjnfKVXFk0YJR6xUucl6Rk50i1jQNtj9qRPUy0JUisYs8+VemUey+dFct8Hra4y06v0jBxk5ZVKpeUUEPIU2XgEeGdunVHiEb+tXqB0Lp3Wx+xJn4LamTE4IcppOm9s9FtTZ0V2vvOKhQnssVaT0ljEKSDkKTLbm9h4em6dkeIRK3VeoGQu3a+GJWD+HfJHjxApcU1MmFQ6T89jkf1ODO4ZxZ4iFSmJRZwCQp6WlpqM3tFyMcmTw55Gikfazp1ByM6lS44Lw9N3skJHniM7dJB7vtKjwx2y34lrJQM+yVEyr5dTQMgb7uoXJ5XOk8OeshVGPcQjfmM9TMkqMw51kKdNtSRIHdvuySFYJfNLtT5/RU+44pW06N6b5X73PHl0mFHm0wGs1Hmc7CozPYzVk/4FtTMjyYfDHUrmdHH4T11c8UpaJNt7LLOvnSuMNJ8OYKXOo5SsMuvNVWbkJbLDHZ7Y2kTJnC4O/6mHK15JqwovOj+WC/DcnDojzacDWKnzGK4yI62SHcY4cK5U9f2hZANzclwYh/9UwlhEWhbfSW4zX0/tn2mk+XQAK3Ues+VQIVeZkSYpaW2qvT+U7JwUVizUw1hEWubLRiYgH5P0MJ8OYKXOY2S7lAEOM5FvDOrRyWkatfeHulDm/HvBioW6/n3svHRaxiLyNl82MgHjxSR+ez3ko9PFUum4yox8pZeXF0zU1FmxZn++03QPDktgxUJFuZLDVoxF5Eu+aGSmZ+Tg7584fz89NXb0kUsdKqqodprGBK4yI9+Rncui1rDDH7cehzDAgdl6oaRHw2xiLCLf8kUjU2auqd5OeGKlTmVKAqleVtOQMd03MN7p2YtqLeOvtwpknCiSSqv1A7P1YlN2gXRaPfVEkDF5e8GE7KpXvTUy+S1W2a/fPCydVi+raciYZM5eFAD+svO02591MK8UVbX1Umn1MiFZ697MkqvURYcF66ongoxJppEJqLdgIv97ucajbDqtYKVORaWXanCw4KJ0ev54ka89MTrJaRo1JieXVF6RSmfS0YRkLUvPyEHJJedTQACge5S+eiLImGQamTbqLJiQXBIunU4bWKlTyV0v78XNz2RKp9fL7tRkbDKTjtWYnJyZI7dwaPz1sRwGdJPSfelG947xYG6I5KWlJsOSGOU0nRox6SvJaR79ukW69Tnexuipgrte3ovjX1coes1Dw7nCj3xPdv6aO/PcauqsyDhxwWm69oFm/G3STS5/DjVQcmqHCcADw+V6R4i84ZoYzy+YSM/IwX/PfieVNq6TvkbUWKtw06UrdYordLERwZh/Rx8P5YhInjc23pStZIy/Pg4BMpNqqE1KfuxmjeQCCdIWT8ckJT3ZsREhGCTRc6gl/Da76fEtRxW/5uMnf+qBnBApN9WSILUCVnbX95bIVjJCgwNc/gz6keyPHfelIy2SiUnubAaspCd74Z3JumtoslLnJiUnRwDAbLaMSUNkV8DesHiHyyvOjHYMj9bJ/ihyXzrSIpmY9NBQ16cvyTYyb7m2C8b2jXXpM3yJtQs3ye6tAzRU6NgyJq1JS03G7JGJbVYErAJ4dU+eSxW7qZYEOGvr6ukYHq2T+VHkvnSkZc5i0tr9+R5vZI685iqX3t/X+K1201/vk5vY/enTo1ihI81KS03G8YVjnKZzZSsBmX3uWMlQV1pqMh4a2qPZ42YTG5ekD2mpyXhwWEKLz7nbyHRGz41MRlE3dQxphxu6hbeZ5oZu4YjqGOSlHBG5ZsuhQqdplG4lkJ6Rg1f35LW50xMrGZ4x7397ED415jpMs/TA/PG9cXrpOP6tSRdq6qxY5+RcVjYym9NnrjXm33NHtFqxu6FbOP49d4SXc0SknNrbm8isMjOb5DZAJtdNtSRgyYS+mDGip25/qMj/yCxoUNrIlIlJJug7JvEbrpJ/zx2Bk4vG4KfXNozD//Taq3By0RhW6Eg31F7Q4ImgTET+wRN7aMrEJAF9xyRW6lTUMaQdVk6+GQCwcvLN6BjSzsc5IpKn9lYC3tjYmIiMyROr5v0hJrFSR0QA5FZNWoXcnBSAW5kQkes8sV+dP8QkVuqIyM62lUBbsVR21ZmnNxElIuNSu5EJ+EdMYqWOiBzITBKWXXXm7IgdPa8yIyLPUrORCfjHHo76zTkRecSbWfltbkECOF/gkJ6Rg6T523DgXGmLz3O/NCKSoWYjs6bOiq5hIUiOC2v2nFFikqYrdenp6Rg4cCDCwsLQtWtX3H333Thz5oxDmitXrmDOnDno3LkzOnbsiIkTJ6K4uNghTWFhIcaPH4/Q0FB07doVTz75JOrq6rxZFCLdkJ0k/PbRr1t83LY3XWurzCyJUdwvjYikqNHIBH5saC79MBc55ysBNGxf0ic2zFB7OGq6Uvfxxx9jzpw5OHDgADIzM1FbW4vRo0fjhx9+sKd5/PHH8f777+Nf//oXPv74Y5w/fx733nuv/fn6+nqMHz8eNTU12L9/P9544w2sX78eCxYs8EWRiDRPdpJwzvnKZsMeMvtAZee33HtHRNSUu41MoPWGpgBw6kIlSiqv6HrItTFNl2L79u144IEH0KdPH9x4441Yv349CgsLceTIEQBAeXk51qxZgxUrVuCnP/0p+vfvj3Xr1mH//v04cOAAAGDnzp3IycnBxo0b0a9fP4wbNw5Lly7FqlWrUFNT48viEWmSzGRim6bDHtybjojU5E4jE2hoaL62p+2GpisnU2iVrjZSKy8vBwBERTVMvj5y5Ahqa2uRkpJiT5OUlIT4+HhkZWVhyJAhyMrKwvXXX4/o6Gh7mjFjxuDhhx/GqVOncNNNzc9ura6uRnV1tf3fFRUVAIDa2lrU1ta2mUfb887S6RXLp3/OymgCMHt4PNbuL5B6v437v7SvFnv/6FcIDnA2WAJ8VXrJY3/jpuUzwrV0JSYZqfwtMXr5AJYRACYNuBov7Mhx2lgEgA37z+HR23o59LrNWHcQQRIxqXEcU1NL5fPk9TQJIST+VL5ntVpx1113oaysDPv27QMAbN68GQ8++KBDsAOAQYMG4bbbbsOyZcswa9YsFBQUYMeOHfbnq6qq0KFDB2RkZGDcuHHNPmvRokVYvHhxs8c3b96M0FD97l9D5I+qqqowefJklJeXIzy87XOatYoxicg4PBmTdNNTN2fOHJw8edJeofOktLQ0zJs3z/7viooKdO/eHaNHj3Z6AWpra5GZmYlRo0YhMDDQ01n1OpZP/2TL+GZWPpbtONPq841NG9IDsREhUulNAI48Pcpjc1ials/Wq6VnrsQko9/LRi8fwDI29vPV+5FbXOn0/a7rGoa3HxmKmjor+j+T6XSRhc1TY67zWE9d0/J5MibpolI3d+5cfPDBB9izZw+6detmfzwmJgY1NTUoKytDZGSk/fHi4mLExMTY0xw8eNDh/WyrY21pmgoODkZwcHCzxwMDA6W/WErS6hHLp3/Oynj/0F5YmnFWKii+/kkhQgPNqK53PhkvOS4MHdo3/36pzVY+I1xHd2KSUf4GrTF6+QCWEQDuvKk7jn2Y6/R9jl+4hOd3nEXONxW4IhGPgIbtTO4f2guBHlws0bh8nryWml4oIYTA3Llz8c4772D37t1ITHTcNLB///4IDAzErl277I+dOXMGhYWFsFgsAACLxYITJ06gpKTEniYzMxPh4eFITtb/8mUiTwlqZ0ZybPP9nFpTVSs30XjiTd2cJyIiamSqJaHNTYgbW7MvH1l58qvs9b7hcGOaLsWcOXOwceNGbN68GWFhYSgqKkJRUREuX74MAIiIiMCMGTMwb948fPTRRzhy5AgefPBBWCwWDBkyBAAwevRoJCcnY+rUqfjss8+wY8cOPP3005gzZ06LLV8i+tG9N6tbAdP7ETxE5BtKG5myhvSMMsT+dDaartStXr0a5eXluPXWWxEbG2v/b8uWLfY0f/3rX3HHHXdg4sSJGDlyJGJiYrB161b78wEBAfjggw8QEBAAi8WC+++/H9OmTcOSJUt8USQiXVGyvYkMI7WIici7PNHI3PDQYFXf09c0PadOZmFuSEgIVq1ahVWrVrWapkePHsjIyFAza0R+wXZW4qtO9nmSkRwXZqgWMRF511RLAp7NyJXa3kSGERuZxioNEakuLTUZvxqW4Pb7cC4dEbnD1shUg9GGXW1YqSMip56+sw9mDE9w+fWcS0dEalCjkWnEYVcbVuqISMr8O/pg9shEl+bYGXGYg4h8w91GppHjkTFLRUQekZaajNNLx2FIzyip9GYTMHtkoiGHOYjId1xpZPpDPNL0Qgki0p6gdma8NcuC9IwcvL43z2HSstkEDE6IwjUxYegRFYqplgTDtoiJyLfSUpPxxOgkTFubjQPnWt+Xrk9sGO69uZtfxCNW6ojIJbaA+mZWPgpKq1iJIyKvc9bInDnC2D1zTbFSR0QuC2pnxowRPX2dDSLyc2xkNmCljoiIiHSPjUwulCAiIiIyBFbqiIiIiAyAlToiIiIiA2CljoiIiMgAWKkjIiIiMgBW6oiIiIgMgJU6IiIiIgNgpY6IiIjIAFipIyIiIjIAVuqIiIiIDICVOiIiIiIDYKWOiIiIyABYqSMiIiIyAFbqiIiIiAyAlToiIiIiA2CljoiIiMgAWKkjIiIiMgBW6oiIiIgMgJU6IiIiIgNgpY6IiIjIAFipIyIiIjIAVuqIiIiIDICVOiIiIiIDYKWOiIiIyABYqVNRvVXgYF4pAOBgXinqrcLHOSIiIiJ/4VeVulWrViEhIQEhISEYPHgwDh48qNp7bz95AcOX7cZDbxwCADz0xiEMX7Yb209eUO0ziKih8ZT15fd479g3yPryezaeiIj+p52vM+AtW7Zswbx58/DKK69g8ODBePHFFzFmzBicOXMGXbt2deu9t5+8gIc3fgoBIDjgx8eLyq/g4Y2fYvX9N2Ns31j3CkBE2H7yAha/n4ML5Vfsj8VGhGDhncn8jhGR3/ObnroVK1Zg5syZePDBB5GcnIxXXnkFoaGhWLt2rVvvW28VWPx+DlrqK7A9tvj9HPYmELnJ1nhqXKEDfmw8sVeciPydX/TU1dTU4MiRI0hLS7M/ZjabkZKSgqysrGbpq6urUV1dbf93RUUFAKC2tha1tbUOaQ/mlaL00mV7D12wWTj8LwCUXrqMA1+UYFBilGpl8hVb+Zv+HYzC6OUD9FnGeqtA+oenEBTQcuPIBCD9w1O49ZrOsNbXAdBnOYmI3OEXlbrvvvsO9fX1iI6Odng8Ojoap0+fbpY+PT0dixcvbvb4zp07ERoa2uzx5YOaf+bSAVbHPOQeQEauwoxrWGZmpq+z4FFGLx+gvzLOS3KW4gfs2L7N/i9b+aqqqjyXKSIiDfGLSp1SaWlpmDdvnv3fFRUV6N69O0aPHo3w8HCHtAfzSu2LI4CGHrqlA6yYf9iMaqvJ/vja6QMN01OXmZmJUaNGITAw0NfZUZ3Rywfos4wZJy7g928fd5pu+cQbMCqpi0P5bD3teqZk9MDG6D2VRi8fwDIaQUvl82RZ/aJS16VLFwQEBKC4uNjh8eLiYsTExDRLHxwcjODg4GaPBwYGNvsRHPKTrojq2B5F5Vcc5tVVW02orjfBBCAmIgRDftIVAWYTjKKlv4WRGL18gL7K2DWiA6rrnX9/ukZ0sJfJVj69lLEtSkcPGtNbj6xSRi8fwDIaQePyeXL0wC8qdUFBQejfvz927dqFu+++GwBgtVqxa9cuzJ071633DjCbsPDOZDy88VM0/cmx/XvhncmGqtARedugxCjERoQ0azzZ2BpPgxKj7HPqjETJ6IGNHntklTB6+QCW0QhaKp8nRw/8olIHAPPmzcP06dMxYMAADBo0CC+++CJ++OEHPPjgg26/99i+sVh9/81Y/H4OSi9dtj8ew60WiFTRtPHUuGLXtPFkrfdBBj1MyeiBK2n0zOjlA1hGI2hcPk+W028qdffddx++/fZbLFiwAEVFRejXrx+2b9/ebPGEq8b2jcWo5Bgc+KIE3+UewNrpAw035ErkS40bT423NWHjiYiogd9U6gBg7ty5bg+3tiXAbMKgxChk5DYMF7FCR6QuW+PpYF4pSiqvoGtYCL9rRET/41eVOiLSvwCzCZZenX2dDSIizfGbEyWIiIiIjIyVOiIiIiIDYKWOiIiIyAA4p06CEA0bKMjsLVNbW4uqqipUVFQYcnk2y6d/Ri9j0/LZvre27zERkVGxUiehsrISANC9e3cf54SIXFVZWYmIiAhfZ4OIyGNYqZMQFxeHr776CmFhYTCZ2t46wbbT+1dffdXqTu96xvLpn9HL2LR8QghUVlYiLi7O11lTjczogb/1yBoRy6h/LZXPk6MHrNRJMJvN6Natm6LXhIeHG/IH04bl0z+jl7Fx+YzWQ8fRAyL988ToASt1REQ6IzN64G89skbEMupfS+Xz5OgBK3VERDqjZPTAn3pkjYpl1L+m5fPU6AG3NFFZcHAwFi5c2OLh20bA8umf0cto9PIREbXGJLjOn4jIcCoqKhAREYHy8nJD9oAYvXwAy2gE3i4fe+qIiAzI6D2WRi8fwDIagbfLx546IiIiIgNgTx0RERGRAbBSR0RERGQArNQRERERGQArdSpbtWoVEhISEBISgsGDB+PgwYO+zpJT6enpGDhwIMLCwtC1a1fcfffdOHPmjEOaW2+9FSaTyeG/X//61w5pCgsLMX78eISGhqJr16548sknUVdX582itGjRokXN8p6UlGR//sqVK5gzZw46d+6Mjh07YuLEiSguLnZ4D62WzSYhIaFZGU0mE+bMmQNAf9dvz549uPPOOxEXFweTyYR3333X4XkhBBYsWIDY2Fi0b98eKSkp+Pzzzx3SlJaWYsqUKQgPD0dkZCRmzJiBS5cuOaQ5fvw4RowYgZCQEHTv3h3Lly/3dNGIiDxHkGreeustERQUJNauXStOnTolZs6cKSIjI0VxcbGvs9amMWPGiHXr1omTJ0+KY8eOidTUVBEfHy8uXbpkT3PLLbeImTNnigsXLtj/Ky8vtz9fV1cn+vbtK1JSUsTRo0dFRkaG6NKli0hLS/NFkRwsXLhQ9OnTxyHv3377rf35X//616J79+5i165d4vDhw2LIkCFi6NCh9ue1XDabkpISh/JlZmYKAOKjjz4SQujv+mVkZIg//elPYuvWrQKAeOeddxyef/7550VERIR49913xWeffSbuuusukZiYKC5fvmxPM3bsWHHjjTeKAwcOiL1794qf/OQn4pe//KX9+fLychEdHS2mTJkiTp48Kf7xj3+I9u3bi1dffdVbxfSYl19+WfTo0UMEBweLQYMGiezsbF9nScpzzz0nBgwYIDp27CiuuuoqMWHCBHH69GmHNLfccosA4PDf7NmzHdIUFBSI1NRU0b59e3HVVVeJ3/3ud6K2ttabRWnVwoULm+X/uuuusz9/+fJl8cgjj4ioqCjRoUMHce+994qioiKH99By+YQQokePHs3KCEA88sgjQgj9XcOPP/5Y3HHHHSI2NrbFeGS1WsX8+fNFTEyMCAkJEbfffrs4e/asQ5rvv/9eTJ48WYSFhYmIiAjx0EMPicrKSoc0n332mRg+fLgIDg4W3bp1E8uWLVOcV1bqVDRo0CAxZ84c+7/r6+tFXFycSE9P92GulCspKREAxMcff2x/7JZbbhG//e1vW31NRkaGMJvNDsFn9erVIjw8XFRXV3syu04tXLhQ3HjjjS0+V1ZWJgIDA8W//vUv+2O5ubkCgMjKyhJCaLtsrfntb38revXqJaxWqxBC39evaRC1Wq0iJiZG/PnPf7Y/VlZWJoKDg8U//vEPIYQQOTk5AoA4dOiQPc22bduEyWQS33zzjRBCiP/3//6f6NSpk0P5nnrqKYcfWD3Sa+NSCOM3MIVgI1MI/V1DPTUyWalTSXV1tQgICGh2sadNmybuuusu32TKRZ9//rkAIE6cOGF/7JZbbhFdunQRnTt3Fn369BF/+MMfxA8//GB/fv78+c0qTufOnRMAxKeffuqtrLdo4cKFIjQ0VMTGxorExEQxefJkUVBQIIQQYteuXQKAuHjxosNr4uPjxYoVK4QQ2i5bS6qrq0Xnzp3Fs88+a39Mz9evaRD98ssvBQBx9OhRh3QjR44Ujz76qBBCiDVr1ojIyEiH52tra0VAQIDYunWrEEKIqVOnigkTJjik2b17twAgSktLVS+HtxilcSmE8RqYQrCRKYS+r6HWG5mcU6eS7777DvX19YiOjnZ4PDo6GkVFRT7KlXJWqxWPPfYYhg0bhr59+9ofnzx5MjZu3IiPPvoIaWlpePPNN3H//ffbny8qKmqx7LbnfGnw4MFYv349tm/fjtWrVyMvLw8jRoxAZWUlioqKEBQUhMjISIfXNL5uWi5bS959912UlZXhgQcesD+m5+vXlC0/bX3XioqK0LVrV4fn27Vrh6ioKN1eVxk1NTU4cuQIUlJS7I+ZzWakpKQgKyvLhzlzTXl5OQAgKirK4fFNmzahS5cu6Nu3L9LS0lBVVWV/LisrC9dff73DtR0zZgwqKipw6tQp72Tcic8//xxxcXHo2bMnpkyZgsLCQgDAkSNHUFtb63D9kpKSEB8fb79+eihfYzU1Ndi4cSMeeughmEwm++N6v4Y2eXl5KCoqcrhmERERGDx4sMM1i4yMxIABA+xpUlJSYDabkZ2dbU8zcuRIBAUF2dOMGTMGZ86cwcWLF6Xz087dApGxzJkzBydPnsS+ffscHp81a5b9/19//fWIjY3F7bffji+//BK9evXydjYVGTdunP3/33DDDRg8eDB69OiBf/7zn2jfvr0Pc+YZa9aswbhx4xAXF2d/TM/Xj+S11bg8ffq0j3LlmrYamD169EBcXByOHz+Op556CmfOnMHWrVsBaL+ybmtkXnfddbhw4QIWL16MESNG4OTJk37VyNTzNWxMzUZmYmJis/ewPdepUyep/LBSp5IuXbogICCg2arJ4uJixMTE+ChXysydOxcffPAB9uzZg27durWZdvDgwQCAL774Ar169UJMTEyzlb62v4XWyh8ZGYlrr70WX3zxBUaNGoWamhqUlZU5BNLG101PZSsoKMB//vMfe3BsjZ6vny0/xcXFiI2NtT9eXFyMfv362dOUlJQ4vK6urg6lpaUO17Wl72vjzyDfMWIDE2AjE9D/NdQyDr+qJCgoCP3798euXbvsj1mtVuzatQsWi8WHOXNOCIG5c+finXfewe7du5u1Flpy7NgxALD/qFosFpw4ccLhhzQzMxPh4eFITk72SL5ddenSJXz55ZeIjY1F//79ERgY6HDdzpw5g8LCQvt101PZ1q1bh65du2L8+PFtptPz9UtMTERMTIzDNauoqEB2drbDNSsrK8ORI0fsaXbv3g2r1Wqv0FosFuzZswe1tbX2NJmZmbjuuuukW8VaY4TGJfBjA/Ojjz5S1MAE9FdZb9zIjImJsTcyG2vayNRL+WyNzF/96ldtptPzNWzcyGys6TXzWiPThXmC1Iq33npLBAcHi/Xr14ucnBwxa9YsERkZ2Ww5utY8/PDDIiIiQvz3v/91WI1UVVUlhBDiiy++EEuWLBGHDx8WeXl54r333hM9e/YUI0eOtL+HbbXS6NGjxbFjx8T27dvFVVddpYkVWU888YT473//K/Ly8sQnn3wiUlJSRJcuXURJSYkQomG1WXx8vNi9e7c4fPiwsFgswmKx2F+v5bI1Vl9fL+Lj48VTTz3l8Lger19lZaU4evSoOHr0qAAgVqxYIY4ePWpf4PL888+LyMhI8d5774njx4+LCRMmtLja7KabbhLZ2dli37594pprrnFYbVZWViaio6PF1KlTxcmTJ8Vbb70lQkNDdb+lyaBBg8TcuXPt/66vrxdXX321LhZKWK1WMWfOHBEXF9dsS4jW7Nu3TwAQn332mRDix0n2jVf7vvrqqyI8PFxcuXLFI/l2R2VlpejUqZP429/+Zl8o8X//93/250+fPt3iQgk9lG/hwoUiJibG6VYkerqGaGWhxAsvvGB/rLy8vMWFEocPH7an2bFjR4sLJWpqauxp0tLSFC+UYKVOZS+99JKIj48XQUFBYtCgQeLAgQO+zpJTaGE/IQBi3bp1QgghCgsLxciRI0VUVJQIDg4WP/nJT8STTz7psARdCCHy8/PFuHHjRPv27UWXLl3EE088oYm9k+677z4RGxsrgoKCxNVXXy3uu+8+8cUXX9ift+0L1alTJxEaGiruuececeHCBYf30GrZGtuxY4cAIM6cOePwuB6v30cffdTiPTl9+nQhxI/7QkVHR4vg4GBx++23Nyv3999/L375y1+Kjh07ivDwcPHggw+2uS/U1VdfLZ5//nlvFdFj9Nq4FML4DUwh2MjU4zXUUyOTlToiIoPRY+NSCOM3MIVgI1OP11BPjUyTEELID9YSERERkRZxoQQRERGRAbBSR0RERGQArNQRERERGQArdUREREQGwEodERERkQGwUkdERERkAKzUkV9KSEiAyWSCyWRqdiSPUrfeeqv9vWzHbxEREXkbK3WkW/X19Rg6dCjuvfdeh8fLy8vRvXt3/OlPf2rz9UuWLMGFCxcQERHhVj62bt2KgwcPuvUeRERE7mKljnQrICAA69evx/bt27Fp0yb747/5zW8QFRWFhQsXtvn6sLAwxMTEwGQyuZWPqKgoXHXVVW69BxH5N44ekBpYqSNdu/baa/H888/jN7/5DS5cuID33nsPb731FjZs2ICgoCBF77V+/XpERkbigw8+wHXXXYfQ0FD87Gc/Q1VVFd544w0kJCSgU6dOePTRR1FfX++hEhGRXnH0gHytna8zQOSu3/zmN3jnnXcwdepUnDhxAgsWLMCNN97o0ntVVVVh5cqVeOutt1BZWYl7770X99xzDyIjI5GRkYFz585h4sSJGDZsGO677z6VS0JEemYbPejXrx82bdqEKVOmAFA+euCuqKgoVFRUuP0+pD/sqSPdM5lMWL16NXbt2oXo6Gj84Q9/cPm9amtrsXr1atx0000YOXIkfvazn2Hfvn1Ys2YNkpOTcccdd+C2227DRx99pGIJiMgoOHpAvsSeOjKEtWvXIjQ0FHl5efj666+RkJDg0vuEhoaiV69e9n9HR0cjISEBHTt2dHispKTE3SwTkUFx9IB8hT11pHv79+/HX//6V3zwwQcYNGgQZsyYASGES+8VGBjo8G+TydTiY1ar1eX8EpGxcfSAfIWVOtK1qqoqPPDAA3j44Ydx2223Yc2aNTh48CBeeeUVX2eNiPxY09EDV3H0gJRgpY50LS0tDUIIPP/88wAatgV44YUX8Pvf/x75+fm+zRwR+SWOHpCvsFJHuvXxxx9j1apVWLduHUJDQ+2Pz549G0OHDnUrkBIRuYKjB+RLrNSRbt1yyy2oq6vD8OHDmz23Y8cO7Nq1S9HGwg888ECzTT8XLVrUbPPO9evX491333Uhx0RkdBw9IF9ipY781lNPPYWOHTuivLzcrfcZN24c+vTpo1KuiEivOHpAvmYSvMPIDxUUFKC2thYA0LNnT5jNrrdvvvnmG1y+fBkAEB8fr3gvKiKihIQEPPbYY3jsscdUeb/8/HwkJibi6NGj6NevnyrvSdrHSh0REZGPJSQk4MKFCwgMDMQ333zj1lFh48aNw549e1BVVcVKnZ9hpY6IiMjHOHpAamCljoiIiMgAuFCCiIiIyABYqSMiIiIyAFbqiIiIiAyAlToiIiIiA2CljoiIiMgAWKkjIiIiMgBW6oiIiIgMgJU6IiIiIgP4/0DOA9+RoDTXAAAAAElFTkSuQmCC", "text/plain": [ "
" ] @@ -1370,13 +1388,19 @@ "source": [ "import matplotlib.pyplot as plt\n", "\n", - "fig, (ax1,ax2) = plt.subplots(1,2)\n", + "fig, (ax1,ax2) = plt.subplots(1,2, sharey=True)\n", "\n", "gdf_xy.plot(ax=ax1, aspect='equal')\n", + "ax1.set_xlabel('X [m]')\n", + "ax1.set_ylabel('Y [m]')\n", "ax1.grid()\n", "\n", "gdf_xy_without_bounds.plot(ax=ax2, aspect='equal')\n", - "ax2.grid()" + "ax2.set_xlabel('X [m]')\n", + "ax2.set_ylabel('Y [m]')\n", + "ax2.grid()\n", + "\n", + "plt.tight_layout()" ] }, { @@ -1477,15 +1501,15 @@ " \n", " \n", " 0\n", - " GEOMETRYCOLLECTION (LINESTRING (0.00000 0.0000...\n", + " GEOMETRYCOLLECTION (LINESTRING (0 0, 1 1), POI...\n", " \n", " \n", " 1\n", - " LINESTRING (0.00000 0.00000, 1.00000 1.00000, ...\n", + " LINESTRING (0 0, 1 1, 1 2, 2 2)\n", " \n", " \n", " 2\n", - " LINESTRING (0.00000 0.00000, 1.00000 1.00000, ...\n", + " LINESTRING (0 0, 1 1, 2 1, 2 2)\n", " \n", " \n", "\n", @@ -1493,9 +1517,9 @@ ], "text/plain": [ " geometry\n", - "0 GEOMETRYCOLLECTION (LINESTRING (0.00000 0.0000...\n", - "1 LINESTRING (0.00000 0.00000, 1.00000 1.00000, ...\n", - "2 LINESTRING (0.00000 0.00000, 1.00000 1.00000, ..." + "0 GEOMETRYCOLLECTION (LINESTRING (0 0, 1 1), POI...\n", + "1 LINESTRING (0 0, 1 1, 1 2, 2 2)\n", + "2 LINESTRING (0 0, 1 1, 2 1, 2 2)" ] }, "execution_count": 28, @@ -1558,61 +1582,61 @@ " \n", " \n", " 0\n", - " POINT (0.00000 0.00000)\n", + " POINT (0 0)\n", " 0.00\n", " 0.00\n", " \n", " \n", " 1\n", - " POINT (1.00000 1.00000)\n", + " POINT (1 1)\n", " 1.00\n", " 1.00\n", " \n", " \n", " 2\n", - " POINT (0.00000 0.00000)\n", + " POINT (0 0)\n", " 0.00\n", " 0.00\n", " \n", " \n", " 3\n", - " POINT (1.00000 1.00000)\n", + " POINT (1 1)\n", " 1.00\n", " 1.00\n", " \n", " \n", " 4\n", - " POINT (1.00000 2.00000)\n", + " POINT (1 2)\n", " 1.00\n", " 2.00\n", " \n", " \n", " 5\n", - " POINT (2.00000 2.00000)\n", + " POINT (2 2)\n", " 2.00\n", " 2.00\n", " \n", " \n", " 6\n", - " POINT (0.00000 0.00000)\n", + " POINT (0 0)\n", " 0.00\n", " 0.00\n", " \n", " \n", " 7\n", - " POINT (1.00000 1.00000)\n", + " POINT (1 1)\n", " 1.00\n", " 1.00\n", " \n", " \n", " 8\n", - " POINT (2.00000 1.00000)\n", + " POINT (2 1)\n", " 2.00\n", " 1.00\n", " \n", " \n", " 9\n", - " POINT (2.00000 2.00000)\n", + " POINT (2 2)\n", " 2.00\n", " 2.00\n", " \n", @@ -1621,17 +1645,17 @@ "" ], "text/plain": [ - " geometry X Y\n", - "0 POINT (0.00000 0.00000) 0.00 0.00\n", - "1 POINT (1.00000 1.00000) 1.00 1.00\n", - "2 POINT (0.00000 0.00000) 0.00 0.00\n", - "3 POINT (1.00000 1.00000) 1.00 1.00\n", - "4 POINT (1.00000 2.00000) 1.00 2.00\n", - "5 POINT (2.00000 2.00000) 2.00 2.00\n", - "6 POINT (0.00000 0.00000) 0.00 0.00\n", - "7 POINT (1.00000 1.00000) 1.00 1.00\n", - "8 POINT (2.00000 1.00000) 2.00 1.00\n", - "9 POINT (2.00000 2.00000) 2.00 2.00" + " geometry X Y\n", + "0 POINT (0 0) 0.00 0.00\n", + "1 POINT (1 1) 1.00 1.00\n", + "2 POINT (0 0) 0.00 0.00\n", + "3 POINT (1 1) 1.00 1.00\n", + "4 POINT (1 2) 1.00 2.00\n", + "5 POINT (2 2) 2.00 2.00\n", + "6 POINT (0 0) 0.00 0.00\n", + "7 POINT (1 1) 1.00 1.00\n", + "8 POINT (2 1) 2.00 1.00\n", + "9 POINT (2 2) 2.00 2.00" ] }, "execution_count": 29, @@ -1666,7 +1690,7 @@ "outputs": [ { "data": { - "image/png": "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wQcfaPTo0Zo+fbp27tzZH1OOK7uuSyycsi6GYai8vFx33nmnxo0b1+04N62N5K6+IdE77Lw2ZjllXfqzdzjir/1e6eo/I24YRo9/WjzS+Ej7rWCmltGjR2v06NGh7ZKSEp08eVKrV6/W1KlTEzrPRLDzupjhlHVZsGCBvvjiC3322We9jnXL2lzJTX1DondcyW5rEy2nrEt/9g7HnCEZPny4UlNTu7wKaG5u7pLKgnJzcyOOHzBggIYNG5awufYmlloimTRpkr788st4Ty/h7Lou8WK3dXn66af18ccfa/v27bruuut6HOu2tXFT35DoHXZem3iw27r0d+9wTCBJT09XcXGxfD5f2H6fz6fJkydHPKakpKTL+Orqak2YMEFpaWkJm2tvYqklktraWuXl5cV7egln13WJF7usi2EYWrBggT744AN9+umnKiws7PUYt62Nm/qGRO+w89rEg13WxbLeYeJCW8u99957RlpamlFRUWHU1dUZixYtMn70ox8Zx48fNwzDMJYtW2Y8+OCDofF///vfjUGDBhmLFy826urqjIqKCiMtLc3405/+ZFUJIWZr+cMf/mB8+OGHxt/+9jfj8OHDxrJlywxJRmVlpVUlhJw9e9aora01amtrDUnGK6+8YtTW1honTpwwDMNZ62K2Fjuvy5NPPmkMGTLEqKmpMRobG0M/Fy5cCI1x0trEyk19wzDoHXZdG3pH39fGUYHEMAzj9ddfN66//nojPT3duO2228I+hjR37lxj2rRpYeNramqM8ePHG+np6cbIkSONdevW9fOMu2emlpdfftm44YYbjMzMTOOaa64x7rzzTmPr1q0WzLqr4MfXrv6ZO3euYRjOWheztdh5XSLVIcl46623QmOctDZ94aa+YRj0DjuuDb2j72vj+cedAwAAWMYx15AAAAD3IpAAAADLEUgAAIDlCCQAAMByBBIAAGA5AgkAALAcgQQAAFiOQAIAACxHIAEAAJYjkAAAAMsRSAAAgOUIJAAAwHL/H3GqCEIhU/HWAAAAAElFTkSuQmCC\n", + "image/png": "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+eSTaJQcca0dv7q6Op07d86mqsKjT58+Wrp0qV577TW99tpryszM1NixY7V37167S2uXz+fTnDlzNGbMGA0bNqzVdU66/8UCZlxzJs84k+eb5NwZF+351q1DVcJIOTk5ysnJ8V/Ozc3VkCFD9Nxzz2nBggU2Vob2DB48WIMHD/Zfzs3N1UcffaSFCxfqhRdesLGy9s2aNUsVFRV677337C4FhmPGOZdTZ1y051uXPVN35ZVXKj4+XrW1tQHba2trlZGR0eI+GRkZIa23U0f6u1RCQoK+973v6fDhw5EoMepaO36pqanq3r27TVVFzujRo2P+2M2ePVtvvfWWtmzZon79+rW51kn3v1jAjGufSTOuq803KfZnnB3zrcuGusTERF1//fXatGmTf5vP59OmTZsCHsldLCcnJ2C9JLnd7lbX26kj/V3K6/XqwIED6tOnT6TKjConHb9w2L9/f8weO8uyNHv2bK1du1abN29WVlZWu/t0tePXWcy49pk045x07MIlVmecrfOtA2/kMMaaNWuspKQkq7S01KqsrLT++Z//2erVq5dVU1NjWZZl3XXXXdYjjzziX//nP//Z6tatm/XUU09Zhw4dsoqLi62EhATrwIEDdrXQplD7mz9/vvX2229bH330kbVnzx7rjjvusJKTk62DBw/a1UKbTp8+be3bt8/at2+fJcl65plnrH379llHjx61LMuyHnnkEeuuu+7yr//rX/9qpaSkWA8//LB16NAha/HixVZ8fLy1YcMGu1poVai9LVy40Fq3bp31f//3f9aBAwesBx980IqLi7Peeecdu1po08yZM62ePXtaW7dutaqrq/1f9fX1/jVOv//FAmacc2ecyfPNssyecXbOty4d6izLsv7zP//T6t+/v5WYmGiNHj3a2rFjh/97t9xyizV9+vSA9S+//LI1aNAgKzEx0Ro6dKi1fv36KFccmlD6mzNnjn9tenq6NXnyZGvv3r02VB2cpre4X/rV1NP06dOtW265pdk+I0aMsBITE62rr77aev7556NedzBC7e3JJ5+0Bg4caCUnJ1tpaWnW2LFjrc2bN9tTfBBa6k1SwPEw4f4XC5hx0/2XnTTjTJ5vlmX2jLNzvrn+VgAAAAAcrMu+pg4AAMAkhDoAAAADEOoAAAAMQKgDAAAwAKEOAADAAIQ6AAAAAxDqAAAADECoAwAAMAChDkYZMGCAXC6XXC6Xvvrqq05d19ixY/3XtX///rDUBwAdxXxDewh1iDler1e5ubm67bbbArafOnVKmZmZ+uUvf9nm/r/+9a9VXV2tnj17dqqO119/XTt37uzUdQDAxZhviCRCHWJOfHy8SktLtWHDBv3xj3/0b3/ggQeUlpam4uLiNvfv0aOHMjIy5HK5OlVHWlqavvWtb3XqOgDgYsw3RBKhDjFp0KBBeuKJJ/TAAw+ourpab7zxhtasWaNVq1YpMTExpOsqLS1Vr1699NZbb2nw4MFKSUnRD3/4Q9XX12vlypUaMGCALr/8cv30pz+V1+uNUEcAcB7zDZHSze4CgNY88MADWrt2re666y4dOHBAjz76qIYPH96h66qvr9fvf/97rVmzRqdPn9Ztt92mf/zHf1SvXr1UVlamv/71r/rBD36gMWPG6Pbbbw9zJwAQiPmGSCDUIWa5XC4tWbJEQ4YM0XXXXadHHnmkw9fl8Xi0ZMkSDRw4UJL0wx/+UC+88IJqa2t12WWXKTs7W+PGjdOWLVsYegAijvmGSODpV8S0FStWKCUlRVVVVfrkk086fD0pKSn+gSdJ6enpGjBggC677LKAbcePH+9UvQAQLOYbwo1Qh5i1fft2LVy4UG+99ZZGjx6te++9V5Zldei6EhISAi67XK4Wt/l8vg7XCwDBYr4hEgh1iEn19fX6yU9+opkzZ2rcuHH6r//6L+3cuVNLly61uzQA6BTmGyKFUIeYVFRUJMuy9MQTT0g6/6GbTz31lObOnasjR47YWxwAdALzDZFCqEPM2bZtmxYvXqznn39eKSkp/u3333+/cnNzO/U0BQDYifmGSHJZ/PTAIAMGDNCcOXM0Z86csFzfkSNHlJWVpX379mnEiBFhuU4A6AjmG9rDmToY5+c//7kuu+wynTp1qlPXM2nSJA0dOjRMVQFA5zHf0BbO1MEoR48elcfjkSRdffXViovr+OOWTz/9VOfOnZMk9e/fP+RPegeAcGK+oT2EOgAAAAPw9CsAAIABCHUAAAAGINQBAAAYgFAHAABgAEIdAACAAQh1AAAABiDUAQAAGIBQBwAAYID/D9Bj89dMXCsvAAAAAElFTkSuQmCC", "text/plain": [ "
" ] @@ -1681,10 +1705,16 @@ "fig, (ax1,ax2) = plt.subplots(1,2)\n", "\n", "gdf.plot(ax=ax1, aspect='equal')\n", + "ax1.set_xlabel('X [m]')\n", + "ax1.set_ylabel('Y [m]')\n", "ax1.grid()\n", "\n", "gdf_xy.plot(ax=ax2, aspect='equal')\n", - "ax2.grid()" + "ax2.set_xlabel('X [m]')\n", + "ax2.set_ylabel('Y [m]')\n", + "ax2.grid()\n", + "\n", + "plt.tight_layout()" ] }, { @@ -1693,7 +1723,7 @@ "source": [ "## Additional Arguments\n", "\n", - "Several additional arguments can be passed to adapt the functionality of the function. For further reference, see the [API Reference for extract_xy](https://gemgis.readthedocs.io/en/latest/api_reference/vector_data.html).\n", + "Several additional arguments can be passed to adapt the functionality of the function. For further reference, see the [API Reference for extract_xy](https://gemgis.readthedocs.io/en/latest/getting_started/reference/vector_api/gemgis.vector.extract_xy.html#gemgis.vector.extract_xy).\n", "\n", "* reset_index (bool)\n", "* drop_id (bool)\n", @@ -1751,31 +1781,31 @@ " \n", " \n", " 0\n", - " POINT (0.00000 0.00000)\n", + " POINT (0 0)\n", " 0.00\n", " 0.00\n", " \n", " \n", " 1\n", - " POINT (1.00000 1.00000)\n", + " POINT (1 1)\n", " 1.00\n", " 1.00\n", " \n", " \n", " 2\n", - " POINT (0.00000 0.00000)\n", + " POINT (0 0)\n", " 0.00\n", " 0.00\n", " \n", " \n", " 3\n", - " POINT (1.00000 1.00000)\n", + " POINT (1 1)\n", " 1.00\n", " 1.00\n", " \n", " \n", " 4\n", - " POINT (1.00000 2.00000)\n", + " POINT (1 2)\n", " 1.00\n", " 2.00\n", " \n", @@ -1784,12 +1814,12 @@ "" ], "text/plain": [ - " geometry X Y\n", - "0 POINT (0.00000 0.00000) 0.00 0.00\n", - "1 POINT (1.00000 1.00000) 1.00 1.00\n", - "2 POINT (0.00000 0.00000) 0.00 0.00\n", - "3 POINT (1.00000 1.00000) 1.00 1.00\n", - "4 POINT (1.00000 2.00000) 1.00 2.00" + " geometry X Y\n", + "0 POINT (0 0) 0.00 0.00\n", + "1 POINT (1 1) 1.00 1.00\n", + "2 POINT (0 0) 0.00 0.00\n", + "3 POINT (1 1) 1.00 1.00\n", + "4 POINT (1 2) 1.00 2.00" ] }, "execution_count": 31, @@ -1852,7 +1882,8 @@ " \n", " \n", " \n", - " \n", + " level_0\n", + " level_1\n", " geometry\n", " points\n", " X\n", @@ -1861,38 +1892,46 @@ " \n", " \n", " \n", - " 0\n", " 0\n", - " POINT (0.00000 0.00000)\n", + " 0\n", + " 0\n", + " POINT (0 0)\n", " [0.0, 0.0]\n", " 0.00\n", " 0.00\n", " \n", " \n", " 0\n", - " POINT (1.00000 1.00000)\n", + " 0\n", + " 0\n", + " POINT (1 1)\n", " [1.0, 1.0]\n", " 1.00\n", " 1.00\n", " \n", " \n", - " 1\n", - " 0\n", - " POINT (0.00000 0.00000)\n", + " 1\n", + " 1\n", + " 0\n", + " POINT (0 0)\n", " [0.0, 0.0]\n", " 0.00\n", " 0.00\n", " \n", " \n", - " 0\n", - " POINT (1.00000 1.00000)\n", + " 1\n", + " 1\n", + " 0\n", + " POINT (1 1)\n", " [1.0, 1.0]\n", " 1.00\n", " 1.00\n", " \n", " \n", - " 0\n", - " POINT (1.00000 2.00000)\n", + " 1\n", + " 1\n", + " 0\n", + " POINT (1 2)\n", " [1.0, 2.0]\n", " 1.00\n", " 2.00\n", @@ -1902,12 +1941,12 @@ "" ], "text/plain": [ - " geometry points X Y\n", - "0 0 POINT (0.00000 0.00000) [0.0, 0.0] 0.00 0.00\n", - " 0 POINT (1.00000 1.00000) [1.0, 1.0] 1.00 1.00\n", - "1 0 POINT (0.00000 0.00000) [0.0, 0.0] 0.00 0.00\n", - " 0 POINT (1.00000 1.00000) [1.0, 1.0] 1.00 1.00\n", - " 0 POINT (1.00000 2.00000) [1.0, 2.0] 1.00 2.00" + " level_0 level_1 geometry points X Y\n", + "0 0 0 POINT (0 0) [0.0, 0.0] 0.00 0.00\n", + "0 0 0 POINT (1 1) [1.0, 1.0] 1.00 1.00\n", + "1 1 0 POINT (0 0) [0.0, 0.0] 0.00 0.00\n", + "1 1 0 POINT (1 1) [1.0, 1.0] 1.00 1.00\n", + "1 1 0 POINT (1 2) [1.0, 2.0] 1.00 2.00" ] }, "execution_count": 32, @@ -1970,6 +2009,7 @@ " \n", " \n", " \n", + " index\n", " level_0\n", " level_1\n", " geometry\n", @@ -1983,7 +2023,8 @@ " 0\n", " 0\n", " 0\n", - " POINT (0.00000 0.00000)\n", + " 0\n", + " POINT (0 0)\n", " [0.0, 0.0]\n", " 0.00\n", " 0.00\n", @@ -1992,7 +2033,8 @@ " 1\n", " 0\n", " 0\n", - " POINT (1.00000 1.00000)\n", + " 0\n", + " POINT (1 1)\n", " [1.0, 1.0]\n", " 1.00\n", " 1.00\n", @@ -2000,8 +2042,9 @@ " \n", " 2\n", " 1\n", + " 1\n", " 0\n", - " POINT (0.00000 0.00000)\n", + " POINT (0 0)\n", " [0.0, 0.0]\n", " 0.00\n", " 0.00\n", @@ -2009,8 +2052,9 @@ " \n", " 3\n", " 1\n", + " 1\n", " 0\n", - " POINT (1.00000 1.00000)\n", + " POINT (1 1)\n", " [1.0, 1.0]\n", " 1.00\n", " 1.00\n", @@ -2018,8 +2062,9 @@ " \n", " 4\n", " 1\n", + " 1\n", " 0\n", - " POINT (1.00000 2.00000)\n", + " POINT (1 2)\n", " [1.0, 2.0]\n", " 1.00\n", " 2.00\n", @@ -2029,12 +2074,12 @@ "" ], "text/plain": [ - " level_0 level_1 geometry points X Y\n", - "0 0 0 POINT (0.00000 0.00000) [0.0, 0.0] 0.00 0.00\n", - "1 0 0 POINT (1.00000 1.00000) [1.0, 1.0] 1.00 1.00\n", - "2 1 0 POINT (0.00000 0.00000) [0.0, 0.0] 0.00 0.00\n", - "3 1 0 POINT (1.00000 1.00000) [1.0, 1.0] 1.00 1.00\n", - "4 1 0 POINT (1.00000 2.00000) [1.0, 2.0] 1.00 2.00" + " index level_0 level_1 geometry points X Y\n", + "0 0 0 0 POINT (0 0) [0.0, 0.0] 0.00 0.00\n", + "1 0 0 0 POINT (1 1) [1.0, 1.0] 1.00 1.00\n", + "2 1 1 0 POINT (0 0) [0.0, 0.0] 0.00 0.00\n", + "3 1 1 0 POINT (1 1) [1.0, 1.0] 1.00 1.00\n", + "4 1 1 0 POINT (1 2) [1.0, 2.0] 1.00 2.00" ] }, "execution_count": 33, @@ -2072,7 +2117,7 @@ "- `explode_polygons`\n", "- `set_dtype`\n", "\n", - "For more information of these functions see the [API Reference](https://gemgis.readthedocs.io/en/latest/api_reference/vector_data.html)." + "For more information of these functions see the [API Reference](https://gemgis.readthedocs.io/en/latest/getting_started/reference/vector_api/gemgis.vector.extract_xy.html#gemgis.vector.extract_xy)." ] } ], @@ -2093,7 +2138,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.0" + "version": "3.11.0" }, "varInspector": { "cols": { From 2d171e47317bafb6e5263b57448aac301b8130e9 Mon Sep 17 00:00:00 2001 From: AlexanderJuestel Date: Thu, 2 Jan 2025 11:11:23 +0100 Subject: [PATCH 59/63] Update remove_interfaces_within_fault_buffers --- gemgis/vector.py | 110 +++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 92 insertions(+), 18 deletions(-) diff --git a/gemgis/vector.py b/gemgis/vector.py index a45a731b..783c704a 100644 --- a/gemgis/vector.py +++ b/gemgis/vector.py @@ -5072,38 +5072,86 @@ def remove_interfaces_within_fault_buffers( extract_coordinates: bool = True, ) -> Tuple[gpd.geodataframe.GeoDataFrame, gpd.geodataframe.GeoDataFrame]: """Function to create a buffer around a GeoDataFrame containing fault data and removing interface points - that are located within this buffer + that are located within this buffer. Parameters __________ fault_gdf : gpd.geodataframe.GeoDataFrame - GeoDataFrame containing the fault data + GeoDataFrame containing the fault data. + + +----+------+-----------+------------------------+ + | ID | id | formation | geometry | + +----+------+-----------+------------------------+ + | 0 | None | Fault1 | POINT (19.150 293.313) | + +----+------+-----------+------------------------+ + | 1 | None | Fault1 | POINT (61.934 381.459) | + +----+------+-----------+------------------------+ + | 2 | None | Fault1 | POINT (109.358 480.946)| + +----+------+-----------+------------------------+ + | 3 | None | Fault1 | POINT (157.812 615.999)| + +----+------+-----------+------------------------+ + | 4 | None | Fault1 | POINT (191.318 719.094)| + +----+------+-----------+------------------------+ interfaces_gdf : gpd.geodataframe.GeoDataFrame - GeoDataFrame containing the interface point data + GeoDataFrame containing the interface point data. + + +----+------+-----------+------------------------+ + | ID | id | formation | geometry | + +----+------+-----------+------------------------+ + | 0 | None | Ton | POINT (19.000 293.000) | + +----+------+-----------+------------------------+ + | 1 | None | Ton | POINT (62.000 381.500) | + +----+------+-----------+------------------------+ + | 2 | None | Ton | POINT (109.000 481.000)| + +----+------+-----------+------------------------+ + | 3 | None | Ton | POINT (150.000 610.000)| + +----+------+-----------+------------------------+ + | 4 | None | Ton | POINT (190.000 710.000)| + +----+------+-----------+------------------------+ distance : float, int - Distance of the buffer around the geometry object, e.g. ``distance=10`` + Distance of the buffer around the geometry object, e.g. ``distance=10``. remove_empty_geometries : bool - Variable to remove empty geometries, Options include: ``True`` or ``False`` default ``True`` + Variable to remove empty geometries, Options include: ``True`` or ``False`` default set to ``True``. extract_coordinates : bool Variable to extract X and Y coordinates from resulting Shapely Objects, Options include: ``True`` or - ``False`` default ``True`` + ``False`` default set to ``True``. Returns _______ gdf_out : gpd.geodataframe.GeoDataFrame - GeoDataFrame containing the vertices located outside the fault buffer + GeoDataFrame containing the vertices located outside the fault buffer. + + +----+------+-----------+------------------------+ + | ID | id | formation | geometry | + +----+------+-----------+------------------------+ + | 0 | None | Ton | POINT (150.000 610.000)| + +----+------+-----------+------------------------+ + | 1 | None | Ton | POINT (190.000 710.000)| + +----+------+-----------+------------------------+ gdf_in : gpd.geodataframe.GeoDataFrame - GeoDataFrame containing the vertices located inside the fault buffer + GeoDataFrame containing the vertices located inside the fault buffer. + + +----+------+-----------+------------------------+ + | ID | id | formation | geometry | + +----+------+-----------+------------------------+ + | 0 | None | Ton | POINT (19.000 293.000) | + +----+------+-----------+------------------------+ + | 1 | None | Ton | POINT (62.000 381.500) | + +----+------+-----------+------------------------+ + | 2 | None | Ton | POINT (109.000 481.000)| + +----+------+-----------+------------------------+ .. versionadded:: 1.0.x + .. versionchanged:: 1.2 + Example _______ @@ -5124,6 +5172,15 @@ def remove_interfaces_within_fault_buffers( >>> # Creating GeoDataFrame from Points >>> fault_gdf = gpd.GeoDataFrame(geometry=[point1, point2]) + >>> fault_gdf + + +----+-------------+ + | ID | geometry | + +====+=============+ + | 0 | POINT (0 0) | + +----+-------------+ + | 1 | POINT (5 0) | + +----+-------------| >>> # Creating first LineString >>> linestring1 = LineString([(0, 0), (10, 10), (20, 20)]) @@ -5137,25 +5194,42 @@ def remove_interfaces_within_fault_buffers( >>> # Creating GeoDataFrame from LineStrings >>> buffer_objects_gdf = gpd.GeoDataFrame(geometry=[linestring1, linestring2]) + >>> buffer_objects_gdf + + +----+-------------+ + | ID | geometry | + +====+=============+ + | 0 | POINT (0 0) | + +----+-------------+ + | 1 | POINT (5 0) | + +----+-------------| >>> # Removing interfaces within fault buffers >>> result_out, result_in = gg.vector.remove_interfaces_within_fault_buffers(fault_gdf=fault_gdf, interfaces_gdf=buffer_objects_gdf, distance=10) >>> # Inspecting the Base Geometries that remain outside >>> result_out - geometry X Y - 0 POINT (7.07107 7.07107) 7.07 7.07 - 1 POINT (10.00000 10.00000) 10.00 10.00 - 2 POINT (20.00000 20.00000) 20.00 20.00 - 3 POINT (10.00000 0.00000) 10.00 0.00 - 4 POINT (20.00000 10.00000) 20.00 10.00 - 5 POINT (30.00000 20.00000) 30.00 20.00 + + +----+---------------------------+-------+-------+ + | | geometry | X | Y | + +----+---------------------------+-------+-------+ + | 0 | POINT (7.07107 7.07107) | 7.07 | 7.07 | + | 1 | POINT (10.00000 10.00000) | 10.00 | 10.00 | + | 2 | POINT (20.00000 20.00000) | 20.00 | 20.00 | + | 3 | POINT (10.00000 0.00000) | 10.00 | 0.00 | + | 4 | POINT (20.00000 10.00000) | 20.00 | 10.00 | + | 5 | POINT (30.00000 20.00000) | 30.00 | 20.00 | + +----+---------------------------+-------+-------+ >>> # Inspecting the Base Geometries that remain inside >>> result_in - geometry X Y - 0 POINT (0.00000 0.00000) 0.00 0.00 - 1 POINT (7.07107 7.07107) 7.07 7.07 + + +----+---------------------------+-------+-------+ + | | geometry | X | Y | + +----+---------------------------+-------+-------+ + | 0 | POINT (0.00000 0.00000) | 0.00 | 0.00 | + | 1 | POINT (7.07107 7.07107) | 7.07 | 7.07 | + +----+---------------------------+-------+-------+ See Also From 01ddde2839dadaf40f02b245cdea8aeea82668d3 Mon Sep 17 00:00:00 2001 From: AlexanderJuestel Date: Fri, 3 Jan 2025 09:43:15 +0100 Subject: [PATCH 60/63] Update calculate_angle --- gemgis/vector.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/gemgis/vector.py b/gemgis/vector.py index 783c704a..739e8168 100644 --- a/gemgis/vector.py +++ b/gemgis/vector.py @@ -5308,23 +5308,25 @@ def remove_interfaces_within_fault_buffers( def calculate_angle(linestring: shapely.geometry.linestring.LineString) -> float: - """Calculating the angle of a LineString to the vertical + """Calculating the angle of a LineString to the vertical. Parameters __________ linestring : shapely.geometry.linestring.LineString Shapely LineString consisting of two vertices, - e.g. ``linestring = LineString([(0, 0), (10, 10), (20, 20)])`` + e.g. ``linestring = LineString([(0, 0), (10, 10), (20, 20)])``. Returns _______ angle : float - Angle of a LineString to the vertical + Angle of a LineString to the vertical. .. versionadded:: 1.0.x + .. versionupdated:: 1.2 + Example _______ @@ -5351,7 +5353,7 @@ def calculate_angle(linestring: shapely.geometry.linestring.LineString) -> float Note ____ - The LineString must only consist of two points (start and end point) + The LineString must only consist of two points (start and end point). """ From a054beef774ffcadb2967fe79f569a376f9cb906 Mon Sep 17 00:00:00 2001 From: AlexanderJuestel Date: Fri, 3 Jan 2025 11:01:53 +0100 Subject: [PATCH 61/63] Linting File --- gemgis/vector.py | 1074 ++++++++++++++++------------------------------ 1 file changed, 377 insertions(+), 697 deletions(-) diff --git a/gemgis/vector.py b/gemgis/vector.py index 739e8168..27d88c3a 100644 --- a/gemgis/vector.py +++ b/gemgis/vector.py @@ -1,5 +1,5 @@ """ -Contributors: Alexander Jüstel, Arthur Endlein Correia, Florian Wellmann, Marius Pischke +Contributors: Alexander Jüstel, Arthur Endlein Correia, Florian Wellmann, Marius Pischke. GemGIS is a Python-based, open-source spatial data processing library. It is capable of preprocessing spatial data such as vector data @@ -53,12 +53,10 @@ def extract_xy_points( target_crs: Union[str, pyproj.crs.crs.CRS] = None, bbox: Optional[Sequence[float]] = None, ) -> gpd.geodataframe.GeoDataFrame: - """Extract X and Y coordinates from a GeoDataFrame (Points) and returning a GeoDataFrame with X and Y - coordinates as additional columns. + """Extract X and Y coordinates from a GeoDataFrame (Points) and return GeoDataFrame with X and Y columns. Parameters ---------- - gdf : gpd.geodataframe.GeoDataFrame GeoDataFrame created from vector data containing exclusively elements of `geom_type` `'Point'`. @@ -100,7 +98,6 @@ def extract_xy_points( Returns ------- - gdf : gpd.geodataframe.GeoDataFrame GeoDataFrame with appended X and Y coordinates as new columns and optional columns. @@ -123,50 +120,49 @@ def extract_xy_points( .. versionchanged:: 1.2 Example - _______ - - >>> # Loading Libraries and File - >>> import gemgis as gg - >>> import geopandas as gpd - >>> gdf = gpd.read_file(filename='file.shp') - >>> gdf - - +----+------+-----------+------------------------+ - | ID | id | formation | geometry | - +----+------+-----------+------------------------+ - | 0 | None | Ton | POINT (19.150 293.313) | - +----+------+-----------+------------------------+ - | 1 | None | Ton | POINT (61.934 381.459) | - +----+------+-----------+------------------------+ - | 2 | None | Ton | POINT (109.358 480.946)| - +----+------+-----------+------------------------+ - | 3 | None | Ton | POINT (157.812 615.999)| - +----+------+-----------+------------------------+ - | 4 | None | Ton | POINT (191.318 719.094)| - +----+------+-----------+------------------------+ - - - >>> # Extracting X and Y Coordinates from Point GeoDataFrame - >>> gdf_xy = gg.vector.extract_xy_points(gdf=gdf, reset_index=False) - >>> gdf_xy - - +----+-----------+-------------------------+-----------+-----------+ - | ID | formation | geometry | X | Y | - +====+===========+=========================+===========+===========+ - | 0 | Ton | POINT (19.150 293.313) | 19.150 | 293.313 | - +----+-----------+-------------------------+-----------+-----------+ - | 1 | Ton | POINT (61.934 381.459) | 61.934 | 381.459 | - +----+-----------+-------------------------+-----------+-----------+ - | 2 | Ton | POINT (109.358 480.946) | 109.358 | 480.946 | - +----+-----------+-------------------------+-----------+-----------+ - | 3 | Ton | POINT (157.812 615.999) | 157.812 | 615.999 | - +----+-----------+-------------------------+-----------+-----------+ - | 4 | Ton | POINT (191.318 719.094) | 191.318 | 719.094 | - +----+-----------+-------------------------+-----------+-----------+ + ------- - See Also - ________ + >>> # Loading Libraries and File + >>> import gemgis as gg + >>> import geopandas as gpd + >>> gdf = gpd.read_file(filename='file.shp') + >>> gdf + + +----+------+-----------+------------------------+ + | ID | id | formation | geometry | + +----+------+-----------+------------------------+ + | 0 | None | Ton | POINT (19.150 293.313) | + +----+------+-----------+------------------------+ + | 1 | None | Ton | POINT (61.934 381.459) | + +----+------+-----------+------------------------+ + | 2 | None | Ton | POINT (109.358 480.946)| + +----+------+-----------+------------------------+ + | 3 | None | Ton | POINT (157.812 615.999)| + +----+------+-----------+------------------------+ + | 4 | None | Ton | POINT (191.318 719.094)| + +----+------+-----------+------------------------+ + + + >>> # Extracting X and Y Coordinates from Point GeoDataFrame + >>> gdf_xy = gg.vector.extract_xy_points(gdf=gdf, reset_index=False) + >>> gdf_xy + + +----+-----------+-------------------------+-----------+-----------+ + | ID | formation | geometry | X | Y | + +====+===========+=========================+===========+===========+ + | 0 | Ton | POINT (19.150 293.313) | 19.150 | 293.313 | + +----+-----------+-------------------------+-----------+-----------+ + | 1 | Ton | POINT (61.934 381.459) | 61.934 | 381.459 | + +----+-----------+-------------------------+-----------+-----------+ + | 2 | Ton | POINT (109.358 480.946) | 109.358 | 480.946 | + +----+-----------+-------------------------+-----------+-----------+ + | 3 | Ton | POINT (157.812 615.999) | 157.812 | 615.999 | + +----+-----------+-------------------------+-----------+-----------+ + | 4 | Ton | POINT (191.318 719.094) | 191.318 | 719.094 | + +----+-----------+-------------------------+-----------+-----------+ + See Also + -------- extract_xy : Extract X and Y coordinates from Vector Data extract_xy_linestring : Extract X and Y coordinates from a GeoDataFrame containing Shapely LineStrings and saving the X and Y coordinates as lists for each LineString @@ -274,12 +270,10 @@ def extract_xy_linestring( target_crs: Union[str, pyproj.crs.crs.CRS] = None, bbox: Optional[Sequence[float]] = None, ) -> gpd.geodataframe.GeoDataFrame: - """Extract the coordinates of Shapely LineStrings within a GeoDataFrame - and storing the X and Y coordinates in lists per LineString. + """Extract the coordinates of Shapely LineStrings within a GeoDataFrame. Parameters - __________ - + ---------- gdf : gpd.geodataframe.GeoDataFrame GeoDataFrame created from vector data containing elements of geom_type LineString. @@ -299,8 +293,7 @@ def extract_xy_linestring( Values (minx, maxx, miny, maxy) to limit the extent of the data, e.g. ``bbox=[0, 972, 0, 1069]``. Returns - _______ - + ------- gdf : gpd.geodataframe.GeoDataFrame GeoDataFrame containing the additional X and Y columns with lists of X and Y coordinates. @@ -320,8 +313,7 @@ def extract_xy_linestring( .. versionchanged:: 1.2 Example - _______ - + ------- >>> # Loading Libraries and File >>> import gemgis as gg >>> import geopandas as gpd @@ -353,8 +345,7 @@ def extract_xy_linestring( +----+-----------+-----------+-------------------------------------------+--------+-----------------------------------------------------+-------------------------------------------------------------+ See Also - ________ - + -------- extract_xy : Extract X and Y coordinates from Vector Data extract_xy_points : Extract X and Y coordinates from a GeoDataFrame containing Shapely Points extract_xy_linestrings : Extract X and Y coordinates from a GeoDataFrame containing Shapely LineStrings @@ -435,11 +426,10 @@ def extract_xy_linestrings( target_crs: Union[str, pyproj.crs.crs.CRS] = None, bbox: Optional[Sequence[float]] = None, ) -> gpd.geodataframe.GeoDataFrame: - """Extract X and Y coordinates from a GeoDataFrame (LineStrings) and returning a GeoDataFrame with X and Y - coordinates as additional columns. + """Extract X and Y coordinates from a GeoDataFrame (LineStrings) and return GeoDataFrame with X and Y columns. Parameters - __________ + ---------- gdf : gpd.geodataframe.GeoDataFrame GeoDataFrame created from vector data containing elements of geom_type LineString. @@ -503,7 +493,7 @@ def extract_xy_linestrings( .. versionchanged:: 1.2 Example - _______ + ------- >>> # Loading Libraries and File >>> import gemgis as gg >>> import geopandas as gpd @@ -539,20 +529,17 @@ def extract_xy_linestrings( +----+-----------+------------------------+-------+--------+ See Also - ________ - + -------- extract_xy : Extract X and Y coordinates from Vector Data extract_xy_points : Extract X and Y coordinates from a GeoDataFrame containing Shapely Points extract_xy_linestring : Extract X and Y coordinates from a GeoDataFrame containing Shapely LineStrings and saving the X and Y coordinates as lists for each LineString - Note - ____ + ---- The function was adapted to also extract Z coordinates from LineStrings """ - # Checking that gdf is of type GepDataFrame if not isinstance(gdf, gpd.geodataframe.GeoDataFrame): raise TypeError("Loaded object is not a GeoDataFrame") @@ -706,12 +693,10 @@ def extract_xy( remove_total_bounds: bool = False, threshold_bounds: Union[float, int] = 0.1, ) -> gpd.geodataframe.GeoDataFrame: - """Extract X and Y coordinates from a GeoDataFrame (Points, LineStrings, MultiLineStrings, Polygons, Geometry - Collections) and returning a GeoDataFrame with X and Y coordinates as additional columns + """Extract X and Y coordinates from a GeoDataFrame (Points, LineStrings, MultiLineStrings, Polygons, etc.). Parameters - __________ - + ---------- gdf : gpd.geodataframe.GeoDataFrame GeoDataFrame created from vector data such as Shapely Points, LineStrings, MultiLineStrings or Polygons or data loaded from disc with GeoPandas (i.e. Shape File). @@ -774,8 +759,7 @@ def extract_xy( e.g. ``threshold_bounds=10``, default set to ``0.1``. Returns - _______ - + ------- gdf : gpd.geodataframe.GeoDataFrame GeoDataFrame with appended x, y columns and Point geometry features. @@ -799,8 +783,7 @@ def extract_xy( .. versionchanged:: 1.2 Example - _______ - + ------- >>> # Loading Libraries and File >>> import gemgis as gg >>> import geopandas as gpd @@ -842,15 +825,14 @@ def extract_xy( See Also - ________ - + -------- extract_xy_points : Extract X and Y coordinates from a GeoDataFrame containing Shapely Points extract_xy_linestring : Extract X and Y coordinates from a GeoDataFrame containing Shapely LineStrings and saving the X and Y coordinates as lists for each LineString extract_xy_linestrings : Extract X and Y coordinates from a GeoDataFrame containing Shapely LineStrings Note - ____ + ---- GeoDataFrames that contain multiple types of geometries are currently not supported. Please use ``gdf = gdf.explode().reset_index(drop=True)`` to create a GeoDataFrame with only one type of geometries. @@ -1046,8 +1028,7 @@ def extract_xyz_points( """Extract X, Y, and Z coordinates from a GeoDataFrame containing Shapely Points with Z components. Parameters - __________ - + ---------- gdf : gpd.geodataframe.GeoDataFrame GeoDataFrame containing Shapely Points with X, Y, and Z components. @@ -1060,8 +1041,7 @@ def extract_xyz_points( +----+-----------------------------------+ Returns - _______ - + ------- gdf : gpd.geodataframe.GeoDataFrame GeoDataFrame containing Shapely Points with appended X, Y, and Z columns. @@ -1078,8 +1058,7 @@ def extract_xyz_points( .. versionchanged:: 1.2 Example - _______ - + ------- >>> # Loading Libraries and creating Shapely Point >>> import gemgis as gg >>> from shapely.geometry import Point @@ -1114,8 +1093,7 @@ def extract_xyz_points( See Also - ________ - + -------- extract_xyz_linestrings: Extract X and Y coordinates from a GeoDataFrame containing Shapely LineStrings with Z components. extract_xyz_polygons: Extract X and Y coordinates from a GeoDataFrame containing Shapely Polygons with Z @@ -1158,8 +1136,7 @@ def extract_xyz_linestrings( """Extract X, Y, and Z coordinates from a GeoDataFrame containing Shapely LineStrings with Z components. Parameters - __________ - + ---------- gdf : gpd.geodataframe.GeoDataFrame GeoDataFrame containing Shapely LineStrings with X, Y, and Z components. @@ -1180,8 +1157,7 @@ def extract_xyz_linestrings( Options include: ``True`` or ``False``, default set to ``True``. Returns - _______ - + ------- gdf : gpd.geodataframe.GeoDataFrame GeoDataFrame containing Shapely Points with appended X, Y, and Z columns. @@ -1202,8 +1178,7 @@ def extract_xyz_linestrings( .. versionchanged:: 1.2 Example - _______ - + ------- >>> # Loading Libraries and creating Shapely LineString >>> import gemgis as gg >>> from shapely.geometry import LineString @@ -1243,8 +1218,7 @@ def extract_xyz_linestrings( See Also - ________ - + -------- extract_xyz_points: Extract X and Y coordinates from a GeoDataFrame containing Shapely Points with Z components extract_xyz_polygons: Extract X and Y coordinates from a GeoDataFrame containing Shapely Polygons with Z @@ -1313,8 +1287,7 @@ def extract_xyz_polygons( """Extract X, Y, and Z coordinates from a GeoDataFrame containing Shapely Polygons with Z components. Parameters - __________ - + ---------- gdf : gpd.geodataframe.GeoDataFrame GeoDataFrame containing Shapely Polygons with X, Y, and Z components. @@ -1335,8 +1308,7 @@ def extract_xyz_polygons( Options include: ``True`` or ``False``, default set to ``True``. Returns - _______ - + ------- gdf : gpd.geodataframe.GeoDataFrame GeoDataFrame containing Shapely Points with appended X, Y, and Z columns. @@ -1357,8 +1329,7 @@ def extract_xyz_polygons( .. versionchanged:: 1.2 Example - _______ - + ------- >>> # Loading Libraries and creating Shapely Polygon >>> import gemgis as gg >>> from shapely.geometry import Polygon @@ -1399,8 +1370,7 @@ def extract_xyz_polygons( See Also - ________ - + -------- extract_xyz_points: Extract X and Y coordinates from a GeoDataFrame containing Shapely Points with Z component extract_xyz_linestrings: Extract X and Y coordinates from a GeoDataFrame containing Shapely LineStrings with @@ -1477,12 +1447,10 @@ def extract_xyz_rasterio( remove_total_bounds: bool = False, threshold_bounds: Union[float, int] = 0.1, ) -> gpd.geodataframe.GeoDataFrame: - """Extract X and Y coordinates from a GeoDataFrame (Points, LineStrings, MultiLineStrings Polygons) and z values - from a rasterio object and returning a GeoDataFrame with X, Y, and Z coordinates as additional columns. + """Extract X and Y coordinates from a GeoDataFrame and z values from a rasterio object. Parameters - __________ - + ---------- gdf : gpd.geodataframe.GeoDataFrame GeoDataFrame created from vector data containing Shapely Points, LineStrings, MultiLineStrings or Polygons. @@ -1549,16 +1517,14 @@ def extract_xyz_rasterio( e.g. ``threshold_bounds=10``, default set to ``0.1``. Returns - _______ - + ------- gdf : gpd.geodataframe.GeoDataFrame GeoDataFrame containing the X, Y, and Z coordinates .. versionadded:: 1.0.x Example - _______ - + ------- >>> # Loading Libraries and File >>> import gemgis as gg >>> import geopandas as gpd @@ -1606,8 +1572,7 @@ def extract_xyz_rasterio( See Also - ________ - + -------- extract_xyz : Extract X, Y, and Z coordinates from a GeoDataFrame and Digital Elevation Model extract_xyz_array : Extract X, Y, and Z coordinates from a GeoDataFrame and Digital Elevation Model as array @@ -1835,12 +1800,10 @@ def extract_xyz_array( remove_total_bounds: bool = False, threshold_bounds: Union[float, int] = 0.1, ) -> gpd.geodataframe.GeoDataFrame: - """Extract X and Y coordinates from a GeoDataFrame (Points, LineStrings, MultiLineStrings Polygons) and Z values from - a NumPy nd.array and returning a GeoDataFrame with X, Y, and Z coordinates as additional columns. + """Extract X and Y coordinates from a GeoDataFrame and Z values from a NumPy nd.array. Parameters - __________ - + ---------- gdf : gpd.geodataframe.GeoDataFrame GeoDataFrame created from vector data containing Shapely Points, LineStrings, MultiLineStrings or Polygons. @@ -1910,8 +1873,7 @@ def extract_xyz_array( e.g. ``threshold_bounds=10``, default set to ``0.1`` Returns - _______ - + ------- gdf : gpd.geodataframe.GeoDataFrame GeoDataFrame containing the X, Y, and Z coordinates @@ -1934,8 +1896,7 @@ def extract_xyz_array( .. versionchanged:: 1.2 Example - _______ - + ------- >>> # Loading Libraries and File >>> import gemgis as gg >>> import geopandas as gpd @@ -1984,8 +1945,7 @@ def extract_xyz_array( +----+-----------+------------------------+-------+-------+-------+ See Also - ________ - + -------- extract_xyz : Extract X, Y, and Z coordinates from a GeoDataFrame and Digital Elevation Model extract_xyz_rasterio : Extract X, Y, and Z coordinates from a GeoDataFrame and Digital Elevation Model as rasterio object @@ -2201,12 +2161,10 @@ def extract_xyz( remove_total_bounds: bool = False, threshold_bounds: Union[float, int] = 0.1, ) -> gpd.geodataframe.GeoDataFrame: - """Extract X and Y coordinates from a GeoDataFrame (Points, LineStrings, MultiLineStrings Polygons) and Z values from - a NumPy nd.array or a Rasterio object and returning a GeoDataFrame with X, Y, and Z coordinates as additional columns. + """Extract X and Y coordinates from a GeoDataFrame and Z values from a NumPy nd.array or a Rasterio object. Parameters - __________ - + ---------- gdf : gpd.geodataframe.GeoDataFrame GeoDataFrame created from vector data containing Shapely Points, LineStrings, MultiLineStrings or Polygons @@ -2277,8 +2235,7 @@ def extract_xyz( e.g. ``threshold_bounds=10``, default set to ``0.1``. Returns - _______ - + ------- gdf : gpd.geodataframe.GeoDataFrame GeoDataFrame containing the X, Y, and Z coordinates as additional columns @@ -2301,8 +2258,7 @@ def extract_xyz( .. versionchanged.: 1.2 Example - _______ - + ------- >>> # Loading Libraries and File >>> import gemgis as gg >>> import geopandas as gpd @@ -2348,8 +2304,7 @@ def extract_xyz( +----+-----------+------------------------+-------+-------+-------+ See Also - ________ - + -------- extract_xyz_array : Extract X, Y, and Z coordinates from a GeoDataFrame and Digital Elevation Model as array extract_xyz_rasterio : Extract X, Y, and Z coordinates from a GeoDataFrame and Digital Elevation as rasterio object @@ -2567,15 +2522,13 @@ def explode_linestring( """Explode a LineString to its vertices, also works for LineStrings with Z components. Parameters - __________ - + ---------- linestring : shapely.geometry.linestring.LineString Shapely LineString from which vertices are extracted, e.g. ``linestring = LineString([(0, 0), (10, 10), (20, 20)])``. Returns - _______ - + ------- points_list : List[shapely.geometry.point.Point] List of extracted Shapely Points. @@ -2584,8 +2537,7 @@ def explode_linestring( .. versionchanged:: 1.2 Example - _______ - + ------- >>> # Loading Libraries and creating LineString >>> import gemgis as gg >>> from shapely.geometry import LineString @@ -2613,8 +2565,7 @@ def explode_linestring( 'POINT (20 20)' See Also - ________ - + -------- explode_linestring_to_elements : Explode a LineString with more than two vertices into single LineStrings """ @@ -2639,19 +2590,16 @@ def explode_linestring( def explode_linestring_to_elements( linestring: shapely.geometry.linestring.LineString, ) -> List[shapely.geometry.linestring.LineString]: - """Separate a LineString into its single elements and returning a list of LineStrings representing these elements, - also works for LineStrings with Z components. + """Separate a LineString into its single elements and returning a list of LineStrings representing these elements. Parameters - __________ - + ---------- linestring : shapely.geometry.linestring.LineString Shapely LineString containing more than two vertices, e.g. ``linestring = LineString([(0, 0), (10, 10), (20, 20)])``. Returns - _______ - + ------- splitted_linestrings : List[shapely.geometry.linestring.LineString] List containing the separate elements of the original LineString stored as LineStrings. @@ -2660,8 +2608,7 @@ def explode_linestring_to_elements( .. versionchanged:: 1.2 Example - _______ - + ------- >>> # Loading Libraries and creating LineString >>> import gemgis as gg >>> from shapely.geometry import LineString @@ -2684,8 +2631,7 @@ def explode_linestring_to_elements( 'LINESTRING (10 10, 20 20)' See Also - ________ - + -------- explode_linestring : Explode a LineString into its single vertices """ @@ -2722,15 +2668,13 @@ def explode_multilinestring( """Explode a MultiLineString into a list of LineStrings. Parameters - __________ - + ---------- multilinestring : shapely.geometry.multilinestring.MultiLineString Shapely MultiLineString consisting of multiple LineStrings, e.g. ``multilinestring = MultiLineString([((0, 0), (1, 1)), ((-1, 0), (1, 0))])``. Returns - _______ - + ------- splitted_multilinestring : List[shapely.geometry.linestring.LineString] List of Shapely LineStrings. @@ -2739,8 +2683,7 @@ def explode_multilinestring( .. versionchanged:: 1.2 Example - _______ - + ------- >>> # Loading Libraries and creating MultiLineString >>> import gemgis as gg >>> from shapely.geometry import MultiLineString @@ -2763,8 +2706,7 @@ def explode_multilinestring( 'LINESTRING (-1 0, 1 0)' See Also - ________ - + -------- explode_multilinestrings : Explode a GeoDataFrame containing MultiLineStrings into a GeoDataFrame containing LineStrings only @@ -2803,7 +2745,6 @@ def explode_multilinestrings( Parameters ---------- - gdf : gpd.geodataframe.GeoDataFrame GeoDataFrame created from vector data containing elements of ``geom_type`` MultiLineString. @@ -2829,7 +2770,6 @@ def explode_multilinestrings( Returns ------- - gdf : gpd.geodataframe.GeoDataFrame GeoDataFrame containing LineStrings. @@ -2850,8 +2790,7 @@ def explode_multilinestrings( .. versionchanged:: 1.2 Example - _______ - + ------- >>> # Loading Libraries and File >>> import gemgis as gg >>> import geopandas as gpd @@ -2885,8 +2824,7 @@ def explode_multilinestrings( See Also - ________ - + -------- explode_multilinestring : Explode a MultiLineString into a list of single LineStrings """ @@ -2944,14 +2882,12 @@ def explode_polygon( """Explode Shapely Polygon to list of Points. Parameters - __________ - + ---------- polygon : shapely.geometry.polygon.Polygon Shapely Polygon from which vertices are extracted, e.g. ``polygon = Polygon([(0, 0), (1, 1), (1, 0)])``. Returns - _______ - + ------- point_list : List[shapely.geometry.point.Point] List containing the vertices of a polygon as Shapely Points. @@ -2960,8 +2896,7 @@ def explode_polygon( .. versionchanged:: 1.2 Example - _______ - + ------- >>> # Loading Libraries and creating Polygon >>> import gemgis as gg >>> from shapely.geometry import Polygon @@ -2986,8 +2921,7 @@ def explode_polygon( 'POINT (1 1)' See Also - ________ - + -------- explode_polygons : Explode a GeoDataFrame containing Polygons into a GeoDataFrame containing LineStrings """ @@ -3014,8 +2948,7 @@ def explode_polygons( """Convert a GeoDataFrame containing elements of ``geom_type`` Polygon to a GeoDataFrame with LineStrings. Parameters - ___________ - + ---------- gdf : gpd.geodataframe.GeoDataFrame GeoDataFrame created from vector data containing elements of ``geom_type`` Polygon. @@ -3028,8 +2961,7 @@ def explode_polygons( +----+------------------------------------------------+ Returns - _______ - + ------- gdf_linestrings : gpd.geodataframe.GeoDataFrame GeoDataFrame containing elements of type MultiLineString and LineString. @@ -3046,8 +2978,7 @@ def explode_polygons( .. versionchanged:: 1.2 Example - _______ - + ------- >>> # Loading Libraries and creating Polygon >>> import gemgis as gg >>> import geopandas as gpd @@ -3076,8 +3007,7 @@ def explode_polygons( +----+-------------------------------------------------+ See Also - ________ - + -------- explode_polygon : Explod a Polygon into single Points """ @@ -3111,14 +3041,12 @@ def explode_geometry_collection( """Explode a Shapely Geometry Collection to a List of Base Geometries. Parameters - __________ - + ---------- collection : shapely.geometry.collection.GeometryCollection Shapely Geometry Collection consisting of different Base Geometries. Returns - _______ - + ------- collection_exploded : List[shapely.geometry.base.BaseGeometry] List of Base Geometries from the original Geometry Collection. @@ -3127,8 +3055,7 @@ def explode_geometry_collection( .. versionchanged:: 1.2 Example - _______ - + ------- >>> # Loading Libraries and creating Geometry Collection >>> import gemgis as gg >>> from shapely.geometry import LineString @@ -3153,8 +3080,7 @@ def explode_geometry_collection( 'LINESTRING (0 0, 1 1)' See Also - ________ - + -------- explode_geometry_collections : Explode a GeoDataFrame containing different Base Geometries """ @@ -3187,7 +3113,6 @@ def explode_geometry_collections( Parameters ---------- - gdf : gpd.geodataframe.GeoDataFrame GeoDataFrame created from vector data containing elements of geom_type GeometryCollection. @@ -3221,7 +3146,6 @@ def explode_geometry_collections( Returns ------- - gdf : gpd.geodataframe.GeoDataFrame GeoDataFrame containing different geometry types. @@ -3242,8 +3166,7 @@ def explode_geometry_collections( .. versionchanged:: 1.2 Example - _______ - + ------- >>> # Loading Libraries and creating Geometries >>> import gemgis as gg >>> from shapely.geometry import LineString, Polygon @@ -3287,8 +3210,7 @@ def explode_geometry_collections( +----+----------------------------------------------------+ See Also - ________ - + -------- explode_geometry_collection : Explod a Shapely Geometry Collection Object into a list of Base Geometries """ @@ -3357,7 +3279,7 @@ def create_linestring_from_xyz_points( """Create LineString from an array or GeoDataFrame containing X, Y, and Z coordinates of points. Parameters - __________ + ---------- points : Union[np.ndarray, gpd.geodataframe.GeoDataFrame] NumPy Array or GeoDataFrame containing XYZ points. @@ -3386,8 +3308,7 @@ def create_linestring_from_xyz_points( ``False``, default is ``True``. Returns - _______ - + ------- line : shapely.geometry.linestring.LineString LineString Z constructed from provided point values. @@ -3396,8 +3317,7 @@ def create_linestring_from_xyz_points( .. versionchanged:: 1.2 Example - _______ - + ------- >>> # Loading Libraries and creating points >>> import gemgis as gg >>> import numpy as np @@ -3423,8 +3343,7 @@ def create_linestring_from_xyz_points( 'LINESTRING Z (3.23 5.69 2.03, 3.24 5.68 2.02, 3.25 5.67 1.97, 3.26 5.66 1.95)' See Also - ________ - + -------- create_linestrings_from_xyz_points : Create LineStrings from a GeoDataFrame containing X, Y, and Z coordinates of vertices of multiple LineStrings """ @@ -3498,8 +3417,7 @@ def create_linestrings_from_xyz_points( """Create LineStrings from a GeoDataFrame containing X, Y, and Z coordinates of vertices of multiple LineStrings. Parameters - __________ - + ---------- gdf : gpd.geodataframe.GeoDataFrame GeoDataFrame containing extracted X, Y, and Z coordinates of LineStrings. @@ -3550,8 +3468,7 @@ def create_linestrings_from_xyz_points( Options include ``True`` and ``False``, default is ``True``. Returns - _______ - + ------- linestrings : Union[List[shapely.geometry.linestring.LineString], gpd.geodataframe.GeoDataFrame] List of LineStrings or GeoDataFrame containing the LineStrings with Z component @@ -3560,8 +3477,7 @@ def create_linestrings_from_xyz_points( .. versionchanged:: 1.2 Example - _______ - + ------- >>> # Loading Libraries and File >>> import gemgis as gg >>> import geopandas as gpd @@ -3596,9 +3512,9 @@ def create_linestrings_from_xyz_points( +----+-----------+----------------------------------------------------------------------+ See Also - ________ - + -------- create_linestring_from_xyz_points : Create LineString from an array or GeoDataFrame containing X, Y, and Z coordinates of points. + """ # Checking that the input is a GeoDataFrame if not isinstance(gdf, gpd.geodataframe.GeoDataFrame): @@ -3689,8 +3605,7 @@ def create_linestrings_from_contours( """Create LineStrings from PyVista Contour Lines and save them as list or GeoDataFrame. Parameters - __________ - + ---------- contours : pv.core.pointset.PolyData PyVista PolyData dataset containing contour lines extracted from a mesh. @@ -3722,8 +3637,7 @@ def create_linestrings_from_contours( Name of the CRS provided to reproject coordinates of the GeoDataFrame, e.g. ``crs='EPSG:4647'``. Returns - _______ - + ------- linestrings : Union[List[shapely.geometry.linestring.LineString], gpd.geodataframe.GeoDataFrame] List of LineStrings or GeoDataFrame containing the contours that were converted. @@ -3746,8 +3660,7 @@ def create_linestrings_from_contours( .. versionchanged:: 1.2 Example - _______ - + ------- >>> # Loading Libraries and File >>> import gemgis as gg >>> import pyvista as pv @@ -3790,7 +3703,6 @@ def create_linestrings_from_contours( | 4 | LINESTRING Z (32404738.664 5782672.480 -2350.0...) | -2350.00| +----+----------------------------------------------------+---------+ - """ # Checking that the input data is a PyVista PolyData dataset if not isinstance(contours, pv.core.pointset.PolyData): @@ -3875,8 +3787,7 @@ def interpolate_raster( """Interpolate a raster/digital elevation model from Point or LineString Shape file. Parameters - __________ - + ---------- gdf : gpd.geodataframe.GeoDataFrame GeoDataFrame containing vector data of geom_type Point or Line containing the Z values of an area. @@ -3918,8 +3829,7 @@ def interpolate_raster( For kwargs for rbf and griddata see: https://docs.scipy.org/doc/scipy/reference/interpolate.html. Returns - _______ - + ------- array : np.ndarray Array representing the interpolated raster/digital elevation model. @@ -3928,8 +3838,7 @@ def interpolate_raster( .. versionchanged:: 1.2 Example - _______ - + ------- >>> # Loading Libraries and File >>> import gemgis as gg >>> import geopandas as gpd @@ -4077,8 +3986,7 @@ def clip_by_bbox( """Clip vector data contained in a GeoDataFrame to a provided bounding box/extent. Parameters - __________ - + ---------- gdf : gpd.geodataframe.GeoDataFrame GeoDataFrame containing vector data that will be clipped to a provided bounding box/extent. @@ -4124,8 +4032,7 @@ def clip_by_bbox( Options include: ``True`` or ``False``, default set to ``True``. Returns - _______ - + ------- gdf : gpd.geodataframe.GeoDataFrame GeoDataFrame containing vector data clipped by a bounding box. @@ -4148,8 +4055,7 @@ def clip_by_bbox( .. versionchanged:: 1.2 Example - _______ - + ------- >>> # Loading Libraries and File >>> import gemgis as gg >>> import geopandas as gpd @@ -4201,8 +4107,7 @@ def clip_by_bbox( 25 See Also - ________ - + -------- clip_by_polygon : Clip vector data with a Shapely Polygon """ @@ -4336,8 +4241,7 @@ def clip_by_polygon( """Clip vector data contained in a GeoDataFrame to a provided bounding box/extent. Parameters - __________ - + ---------- gdf : gpd.geodataframe.GeoDataFrame GeoDataFrame containing vector data that will be clipped to a provided bounding box/extent. @@ -4384,8 +4288,7 @@ def clip_by_polygon( Options include: ``True`` or ``False``, default set to ``True``. Returns - _______ - + ------- gdf : gpd.geodataframe.GeoDataFrame GeoDataFrame containing vector data clipped by a bounding box. @@ -4408,8 +4311,7 @@ def clip_by_polygon( .. versionchanged:: 1.2 Example - _______ - + ------- >>> # Loading Libraries and File >>> import gemgis as gg >>> import geopandas as gpd @@ -4464,8 +4366,7 @@ def clip_by_polygon( 25 See Also - ________ - + -------- clip_by_bbox : Clip vector data with a bbox """ @@ -4528,8 +4429,7 @@ def create_buffer( """Create a buffer around a Shapely LineString or a Shapely Point. Parameters - __________ - + ---------- geom_object : shapely.geometry.base.BaseGeometry Shapely LineString or Point, e.g. ``geom_object=Point(0, 0)``. @@ -4537,8 +4437,7 @@ def create_buffer( Distance of the buffer around the geometry object, e.g. ``distance=10``. Returns - _______ - + ------- polygon : shapely.geometry.polygon.Polygon Polygon representing the buffered area around a geometry object. @@ -4547,8 +4446,7 @@ def create_buffer( .. versionchanged:: 1.2 Example - _______ - + ------- >>> # Loading Libraries and creating Point >>> import gemgis as gg >>> from shapely.geometry import Point @@ -4563,8 +4461,7 @@ def create_buffer( -29.02846772544621, 92.38795325112869 -38.26834323650894, 88.19212643483553...))' See Also - ________ - + -------- create_unified_buffer : Create a unified buffer around Shapely LineStrings or Shapely Points """ @@ -4593,8 +4490,7 @@ def create_unified_buffer( """Create a unified buffer around Shapely LineStrings or Shapely Points. Parameters - __________ - + ---------- geom_object : Union[gpd.geodataframe.GeoDataFrame, List[shapely.geometry.base.BaseGeometry]] GeoDataFrame or List of Shapely objects. @@ -4602,8 +4498,7 @@ def create_unified_buffer( Distance of the buffer around the geometry object, e.g. ``distance=10``. Returns - _______ - + ------- polygon : shapely.geometry.multipolygon.MultiPolygon Polygon representing the buffered area around a geometry object. @@ -4612,8 +4507,7 @@ def create_unified_buffer( .. versionchanged:: 1.2 Example - _______ - + ------- >>> # Loading Libraries and creating Point >>> import gemgis as gg >>> from shapely.geometry import Point @@ -4636,8 +4530,7 @@ def create_unified_buffer( -2.902846772544621, 9.23879532511287 -3.826834323650894,...)))' See Also - ________ - + -------- create_buffer : Create a buffer around a Shapely LineString or a Shapely Point """ @@ -4687,8 +4580,7 @@ def subtract_geom_objects( """Subtract Shapely geometry objects from each other and returning the left over object. Parameters - __________ - + ---------- geom_object1 : shapely.geometry.base.BaseGeometry Shapely object from which other object will be subtracted, e.g. ``geom_object1 = Polygon([[0, 0], [10, 0], [10, 10], [0, 10], [0, 0]])``. @@ -4698,8 +4590,7 @@ def subtract_geom_objects( e.g. ``geom_object2 = Polygon([[5, 0], [15, 0], [15, 10], [5, 10], [5, 0]])``. Returns - _______ - + ------- result : shapely.geometry.base.BaseGeometry Shapely object from which the second object was subtracted. @@ -4708,8 +4599,7 @@ def subtract_geom_objects( .. versionchanged:: 1.2 Example - _______ - + ------- >>> # Loading Libraries and creating Polygon >>> import gemgis as gg >>> from shapely.geometry import Polygon @@ -4755,8 +4645,7 @@ def remove_object_within_buffer( """Remove object from a buffered object by providing a distance. Parameters - __________ - + ---------- buffer_object : shapely.geometry.base.BaseGeometry Shapely object for which a buffer will be created, e.g. ``buffer_object=Point(0, 0)``. @@ -4772,8 +4661,7 @@ def remove_object_within_buffer( Options include: ``True`` or ``False``, default set to ``True``. Returns - _______ - + ------- result_out : shapely.geometry.base.BaseGeometry Shapely object that remains after the buffering (outside the buffer). @@ -4785,8 +4673,7 @@ def remove_object_within_buffer( .. versionchanged:: 1.2 Example - _______ - + ------- >>> # Loading Libraries and creating Point >>> import gemgis as gg >>> from shapely.geometry import Point, LineString @@ -4811,8 +4698,7 @@ def remove_object_within_buffer( 'LINESTRING (0 0, 7.071067811865473 7.071067811865473)' See Also - ________ - + -------- remove_objects_within_buffer : Remove several objects from one buffered object remove_interfaces_within_fault_buffers : Remove interfaces of layer boundaries within fault line buffers @@ -4879,8 +4765,7 @@ def remove_objects_within_buffer( """Remove objects from a buffered object by providing a distance. Parameters - __________ - + ---------- buffer_object : shapely.geometry.base.BaseGeometry Shapely object for which a buffer will be created, e.g. ``buffer_object=Point(0, 0)``. @@ -4908,8 +4793,7 @@ def remove_objects_within_buffer( Options include: ``True`` or ``False``, default set to ``True``. Returns - _______ - + ------- result_out : list, gpd.geodataframe.GeoDataFrame List or GeoDataFrame of Shapely objects that remain after the buffering (outside the buffer). @@ -4921,8 +4805,7 @@ def remove_objects_within_buffer( .. versionchanged:: 1.2 Example - _______ - + ------- >>> # Loading Libraries and creating Point >>> import gemgis as gg >>> from shapely.geometry import Point, LineString @@ -4957,13 +4840,11 @@ def remove_objects_within_buffer( ] See Also - ________ - + -------- remove_object_within_buffer : Remove one object from one buffered object remove_interfaces_within_fault_buffers : Remove interfaces of layer boundaries within fault line buffers """ - # Checking that the buffer object is a Shapely Point or LineString if not isinstance(buffer_object, shapely.geometry.base.BaseGeometry): raise TypeError("Buffer object must be a Shapely Point or LineString") @@ -5071,12 +4952,10 @@ def remove_interfaces_within_fault_buffers( remove_empty_geometries: bool = True, extract_coordinates: bool = True, ) -> Tuple[gpd.geodataframe.GeoDataFrame, gpd.geodataframe.GeoDataFrame]: - """Function to create a buffer around a GeoDataFrame containing fault data and removing interface points - that are located within this buffer. + """Create a buffer around a GeoDataFrame containing fault data and remove interface points within this buffer. Parameters - __________ - + ---------- fault_gdf : gpd.geodataframe.GeoDataFrame GeoDataFrame containing the fault data. @@ -5122,8 +5001,7 @@ def remove_interfaces_within_fault_buffers( ``False`` default set to ``True``. Returns - _______ - + ------- gdf_out : gpd.geodataframe.GeoDataFrame GeoDataFrame containing the vertices located outside the fault buffer. @@ -5153,8 +5031,7 @@ def remove_interfaces_within_fault_buffers( .. versionchanged:: 1.2 Example - _______ - + ------- >>> # Loading Libraries >>> import gemgis as gg >>> import geopandas as gpd @@ -5233,13 +5110,11 @@ def remove_interfaces_within_fault_buffers( See Also - ________ - + -------- remove_object_within_buffer : Removing one object from one buffered object remove_objects_within_buffer : Removing several objects from one buffered object """ - # Checking that the buffer object is a Shapely Point or LineString if not isinstance(fault_gdf, gpd.geodataframe.GeoDataFrame): raise TypeError("Buffer object must be a Shapely Point or LineString") @@ -5308,28 +5183,25 @@ def remove_interfaces_within_fault_buffers( def calculate_angle(linestring: shapely.geometry.linestring.LineString) -> float: - """Calculating the angle of a LineString to the vertical. + """Calculate the angle of a LineString to the vertical. Parameters - __________ - + ---------- linestring : shapely.geometry.linestring.LineString Shapely LineString consisting of two vertices, e.g. ``linestring = LineString([(0, 0), (10, 10), (20, 20)])``. Returns - _______ - + ------- angle : float Angle of a LineString to the vertical. .. versionadded:: 1.0.x - .. versionupdated:: 1.2 + .. versionchanged:: 1.2 Example - _______ - + ------- >>> # Loading Libraries and creating LineString >>> import gemgis as gg >>> from shapely.geometry import LineString @@ -5343,20 +5215,17 @@ def calculate_angle(linestring: shapely.geometry.linestring.LineString) -> float 135.0 See Also - ________ - + -------- calculate_strike_direction_straight_linestring : Calculating the strike direction of a straight LineString calculate_strike_direction_bent_linestring : Calculating the strike direction of a bent LineString calculate_dipping_angle_linestring : Calculate the dipping angle of a LineString calculate_dipping_angles_linestrings : Calculate the dipping angles of LineStrings Note - ____ - + ---- The LineString must only consist of two points (start and end point). """ - # Checking that the LineString is a Shapely LineString if not isinstance(linestring, shapely.geometry.linestring.LineString): raise TypeError("Input geometry must be a Shapley LineString") @@ -5386,27 +5255,23 @@ def calculate_angle(linestring: shapely.geometry.linestring.LineString) -> float def calculate_strike_direction_straight_linestring( linestring: shapely.geometry.linestring.LineString, ) -> float: - """Function to calculate the strike direction of a straight Shapely LineString. - The strike will always be calculated from start to end point + """Calculate the strike direction of a straight LineString. Strike calculated from start to end point. Parameters - __________ - + ---------- linestring : shapely.geometry.linestring.LineString Shapely LineString representing the surface trace of a straight geological profile, e.g. ``linestring = LineString([(0, 0), (10, 10), (20, 20)])`` Returns - _______ - + ------- angle: float Strike angle calculated from start to end point for a straight Shapely LineString .. versionadded:: 1.0.x Example - _______ - + ------- >>> # Loading Libraries and creating LineString >>> import gemgis as gg >>> from shapely.geometry import LineString @@ -5420,20 +5285,17 @@ def calculate_strike_direction_straight_linestring( 45.0 See Also - ________ - + -------- calculate_angle : Calculating the angle of a LineString calculate_strike_direction_bent_linestring : Calculating the strike direction of a bent LineString calculate_dipping_angle_linestring : Calculate the dipping angle of a LineString calculate_dipping_angles_linestrings : Calculate the dipping angles of LineStrings Note - ____ - + ---- The LineString must only consist of two points (start and end point) """ - # Checking that the LineString is a Shapely LineString if not isinstance(linestring, shapely.geometry.linestring.LineString): raise TypeError("Input geometry must be a Shapley LineString") @@ -5481,26 +5343,23 @@ def calculate_strike_direction_straight_linestring( def calculate_strike_direction_bent_linestring( linestring: shapely.geometry.linestring.LineString, ) -> List[float]: - """Calculating the strike direction of a LineString with multiple elements + """Calculate the strike direction of a LineString with multiple elements. Parameters - _________ - + ---------- linestring : linestring: shapely.geometry.linestring.LineString Shapely LineString containing more than two vertices, e.g. ``linestring = LineString([(0, 0), (10, 10), (20, 20)])`` Returns - _______ - + ------- angles_splitted_linestrings : List[float] List containing the strike angles of each line segment of the original .. versionadded:: 1.0.x Example - _______ - + ------- >>> # Loading Libraries and creating LineString >>> import gemgis as gg >>> from shapely.geometry import LineString @@ -5514,15 +5373,13 @@ def calculate_strike_direction_bent_linestring( [45.0, 45.0] See Also - ________ - + -------- calculate_angle : Calculating the angle of a LineString calculate_strike_direction_straight_linestring : Calculating the strike direction of a straight LineString calculate_dipping_angle_linestring : Calculate the dipping angle of a LineString calculate_dipping_angles_linestrings : Calculate the dipping angles of LineStrings """ - # Checking that the LineString is a Shapely LineString if not isinstance(linestring, shapely.geometry.linestring.LineString): raise TypeError("Input geometry must be a Shapley LineString") @@ -5554,26 +5411,23 @@ def calculate_strike_direction_bent_linestring( def calculate_dipping_angle_linestring( linestring: shapely.geometry.linestring.LineString, ): - """Calculating the dipping angle of a LineString digitized on a cross section + """Calculate the dipping angle of a LineString digitized on a cross section. Parameters - __________ - + ---------- linestring : shapely.geometry.linestring.LineString Shapely LineString digitized on a cross section, e.g. ``linestring = LineString([(0, 0), (20, 20)])`` Returns - _______ - + ------- dip : float Dipping angle of the LineString .. versionadded:: 1.0.x Example - _______ - + ------- >>> # Loading Libraries and creating LineString >>> import gemgis as gg >>> from shapely.geometry import LineString @@ -5587,20 +5441,17 @@ def calculate_dipping_angle_linestring( 45.0 See Also - ________ - + -------- calculate_angle : Calculating the angle of a LineString calculate_strike_direction_straight_linestring : Calculating the strike direction of a straight LineString calculate_strike_direction_bent_linestring : Calculating the strike direction of a bent LineString calculate_dipping_angles_linestrings : Calculate the dipping angles of LineStrings Note - ____ - + ---- The LineString must only consist of two points (start and end point) """ - # Checking that the LineString is a Shapely LineString if not isinstance(linestring, shapely.geometry.linestring.LineString): raise TypeError("Input geometry must be a Shapley LineString") @@ -5635,25 +5486,22 @@ def calculate_dipping_angles_linestrings( gpd.geodataframe.GeoDataFrame, List[shapely.geometry.linestring.LineString] ] ): - """Calculating the dipping angles of LineStrings digitized on a cross section + """Calculate the dipping angles of LineStrings digitized on a cross section. Parameters - __________ - + ---------- linestring_list : Union[gpd.geodataframe.GeoDataFrame, List[shapely.geometry.linestring.LineString]] GeoDataFrame containing LineStrings or list of LineStrings Returns - _______ - + ------- dipping_angles : List[float] List containing the dipping angles of LineStrings .. versionadded:: 1.0.x Example - _______ - + ------- >>> # Loading Libraries and creating LineString >>> import gemgis as gg >>> from shapely.geometry import LineString @@ -5670,20 +5518,17 @@ def calculate_dipping_angles_linestrings( [45.0, 45.0] See Also - ________ - + -------- calculate_angle : Calculating the angle of a LineString calculate_strike_direction_straight_linestring : Calculating the strike direction of a straight LineString calculate_strike_direction_bent_linestring : Calculating the strike direction of a bent LineString calculate_dipping_angle_linestring : Calculate the dipping angle of a LineString Note - ____ - + ---- The LineString must only consist of two points (start and end point) """ - # Checking that the list of LineStrings is either provided as list or within a GeoDataFrame if not isinstance(linestring_list, (list, gpd.geodataframe.GeoDataFrame)): raise TypeError("LineStrings must be provided as list or within a GeoDataFrame") @@ -5726,11 +5571,10 @@ def calculate_coordinates_for_point_on_cross_section( linestring: shapely.geometry.linestring.LineString, point: Union[shapely.geometry.point.Point, Tuple[float, float]], ): - """Calculating the coordinates for one point digitized on a cross section provided as Shapely LineString + """Calculate the coordinates for one point digitized on a cross section provided as Shapely LineString. Parameters - __________ - + ---------- linestring : shapely.geometry.linestring.LineString Shapely LineString containing the trace of a cross section on a map, e.g. ``linestring = LineString([(0, 0), (20, 20)])`` @@ -5740,16 +5584,14 @@ def calculate_coordinates_for_point_on_cross_section( e.g. ``point = Point(5, 0)`` Returns - _______ - + ------- point : shapely.geometry.point.Point Shapely Point with real world X and Y coordinates extracted from cross section LineString on Map .. versionadded:: 1.0.x Example - _______ - + ------- >>> # Loading Libraries and creating LineString >>> import gemgis as gg >>> from shapely.geometry import Point, LineString @@ -5768,8 +5610,7 @@ def calculate_coordinates_for_point_on_cross_section( 'POINT (3.535533905932737 -3.535533905932737)' See Also - ________ - + -------- calculate_coordinates_for_linestring_on_cross_sections : Calculating the coordinates for a LineString on a cross section calculate_coordinates_for_linestrings_on_cross_sections : Calculating the coordinates for LineStrings on @@ -5778,7 +5619,6 @@ def calculate_coordinates_for_point_on_cross_section( extract_xyz_from_cross_sections: Extracting the X, Y, and Z coordinates of interfaces from cross sections """ - # Checking that the LineString is a Shapely LineString if not isinstance(linestring, shapely.geometry.linestring.LineString): raise TypeError("Input geometry must be a Shapley LineString") @@ -5827,12 +5667,10 @@ def calculate_coordinates_for_linestring_on_cross_sections( linestring: shapely.geometry.linestring.LineString, interfaces: shapely.geometry.linestring.LineString, ): - """Calculating the coordinates of vertices for a LineString on a straight cross section provided as Shapely - LineString + """Calculate the coordinates of vertices for a LineString on a straight cross section provided as LineString. Parameters - __________ - + ---------- linestring : shapely.geometry.linestring.LineString Shapely LineString containing the trace of a cross section on a map, e.g. ``linestring = LineString([(0, 0), (20, 20)])`` @@ -5842,16 +5680,14 @@ def calculate_coordinates_for_linestring_on_cross_sections( e.g. ``interfaces = LineString([(2, -2), (5, -5)])`` Returns - _______ - + ------- points : List[shapely.geometry.point.Point] List of Shapely Points with real world coordinates of digitized points on cross section .. versionadded:: 1.0.x Example - _______ - + ------- >>> # Loading Libraries and creating LineString >>> import gemgis as gg >>> from shapely.geometry import Point, LineString @@ -5879,8 +5715,7 @@ def calculate_coordinates_for_linestring_on_cross_sections( 'POINT (3.535533905932737 -3.535533905932737)' See Also - ________ - + -------- calculate_coordinates_for_point_on_cross_section : Calculating the coordinates for a Point on a cross section calculate_coordinates_for_linestrings_on_cross_sections : Calculating the coordinates for LineStrings on @@ -5889,7 +5724,6 @@ def calculate_coordinates_for_linestring_on_cross_sections( extract_xyz_from_cross_sections: Extracting the X, Y, and Z coordinates of interfaces from cross sections """ - # Checking that the LineString is a Shapely LineString if not isinstance(linestring, shapely.geometry.linestring.LineString): raise TypeError("Input geometry must be a Shapley LineString") @@ -5929,12 +5763,10 @@ def calculate_coordinates_for_linestrings_on_cross_sections( linestring: shapely.geometry.linestring.LineString, linestring_interfaces_list: List[shapely.geometry.linestring.LineString], ) -> List[shapely.geometry.point.Point]: - """Calculating the coordinates of vertices for LineStrings on a straight cross section provided as Shapely - LineString + """Calculate the coordinates of vertices for LineStrings on a straight cross section provided as LineString. Parameters - _________ - + ---------- linestring : shapely.geometry.linestring.LineString Shapely LineString containing the trace of a cross section on a map, e.g. ``linestring = LineString([(0, 0), (10, 10), (20, 20)])`` @@ -5943,16 +5775,14 @@ def calculate_coordinates_for_linestrings_on_cross_sections( List containing Shapely LineStrings representing interfaces on cross sections Returns - _______ - + ------- points : List[shapely.geometry.point.Point] List containing Shapely Points with real world coordinates of the digitized interfaces on the cross section .. versionadded:: 1.0.x Example - _______ - + ------- >>> # Loading Libraries and creating LineString >>> import gemgis as gg >>> from shapely.geometry import Point, LineString @@ -5993,8 +5823,7 @@ def calculate_coordinates_for_linestrings_on_cross_sections( 'POINT (3.535533905932737 -3.535533905932737)' See Also - ________ - + -------- calculate_coordinates_for_point_on_cross_section : Calculating the coordinates for a Point on a cross section calculate_coordinates_for_linestring_on_cross_sections : Calculating the coordinates for one LineString on @@ -6003,7 +5832,6 @@ def calculate_coordinates_for_linestrings_on_cross_sections( extract_xyz_from_cross_sections: Extracting the X, Y, and Z coordinates of interfaces from cross sections """ - # Checking that the LineString is a Shapely LineString if not isinstance(linestring, shapely.geometry.linestring.LineString): raise TypeError("Input geometry must be a Shapley LineString") @@ -6046,11 +5874,10 @@ def extract_interfaces_coordinates_from_cross_section( interfaces_gdf: gpd.geodataframe.GeoDataFrame, extract_coordinates: bool = True, ) -> gpd.geodataframe.GeoDataFrame: - """Extracting coordinates of interfaces digitized on a cross section + """Extract coordinates of interfaces digitized on a cross section. Parameters - __________ - + ---------- linestring : shapely.geometry.linestring.LineString Shapely LineString containing the trace of a cross section on a map, e.g. ``linestring = LineString([(0, 0), (20, 20)])`` @@ -6059,16 +5886,14 @@ def extract_interfaces_coordinates_from_cross_section( GeoDataFrame containing the LineStrings of interfaces digitized on a cross section Returns - _______ - + ------- gdf : gpd.geodataframe.GeoDataFrame GeoDataFrame containing the extracted coordinates, depth/elevation data and additional columns .. versionadded:: 1.0.x Example - _______ - + ------- >>> # Loading Libraries and creating LineString >>> import gemgis as gg >>> from shapely.geometry import Point, LineString @@ -6099,8 +5924,7 @@ def extract_interfaces_coordinates_from_cross_section( 3 POINT (3.53553 -3.53553) 3.54 -3.54 -5.00 See Also - ________ - + -------- calculate_coordinates_for_point_on_cross_section : Calculating the coordinates for a Point on a cross section calculate_coordinates_for_linestring_on_cross_sections : Calculating the coordinates for one LineString on @@ -6110,7 +5934,6 @@ def extract_interfaces_coordinates_from_cross_section( extract_xyz_from_cross_sections: Extracting the X, Y, and Z coordinates of interfaces from cross sections """ - # Checking that the LineString is a Shapely LineString if not isinstance(linestring, shapely.geometry.linestring.LineString): raise TypeError("Input geometry must be a Shapley LineString") @@ -6195,11 +6018,10 @@ def extract_xyz_from_cross_sections( interfaces_gdf: gpd.geodataframe.GeoDataFrame, profile_name_column: str = "name", ) -> gpd.geodataframe.GeoDataFrame: - """Extracting X, Y, and Z coordinates from cross sections and digitized interfaces + """Extract X, Y, and Z coordinates from cross sections and digitized interfaces. Parameters - __________ - + ---------- profile_gdf : gpd.geodataframe.GeoDataFrame GeoDataFrame containing the traces (LineStrings) of cross sections on a map and a profile name @@ -6210,16 +6032,14 @@ def extract_xyz_from_cross_sections( Name of the profile column, default is ``profile_name_column='name'`` Returns - _______ - + ------- gdf : gpd.geodataframe.GeoDataFrame GeoDataFrame containing the X, Y, and Z information of all extracted digitized interfaces on cross sections .. versionadded:: 1.0.x Example - _______ - + ------- >>> # Loading Libraries and creating LineString >>> import gemgis as gg >>> from shapely.geometry import Point, LineString @@ -6259,8 +6079,7 @@ def extract_xyz_from_cross_sections( 3 Profile2 POINT (3.53553 -3.53553) 3.54 -3.54 -5.00 See Also - ________ - + -------- calculate_coordinates_for_point_on_cross_section : Calculating the coordinates for a Point on a cross section calculate_coordinates_for_linestring_on_cross_sections : Calculating the coordinates for one LineString on @@ -6270,7 +6089,6 @@ def extract_xyz_from_cross_sections( extract_interfaces_coordinates_from_cross_section: Extracting the coordinates of interfaces from cross sections """ - # Checking that the profile traces are provided as a GeoDataFrame if not isinstance(profile_gdf, gpd.geodataframe.GeoDataFrame): raise TypeError("Input geometry must be a GeoDataFrame") @@ -6347,26 +6165,23 @@ def extract_xyz_from_cross_sections( def calculate_midpoint_linestring( linestring: shapely.geometry.linestring.LineString, ) -> shapely.geometry.point.Point: - """Calculating the midpoint of a LineString with two vertices + """Calculate the midpoint of a LineString with two vertices. Parameters - __________ - + ---------- linestring : shapely.geometry.linestring.LineString LineString consisting of two vertices from which the midpoint will be extracted, e.g. ``linestring = LineString([(0, 0), (20, 20)])`` Returns - _______ - + ------- point : shapely.geometry.point.Point Shapely Point representing the midpoint of the LineString .. versionadded:: 1.0.x Example - _______ - + ------- >>> # Loading Libraries and creating LineString >>> import gemgis as gg >>> from shapely.geometry import Point, LineString @@ -6380,17 +6195,14 @@ def calculate_midpoint_linestring( 'POINT (10 -10)' See Also - ________ - + -------- calculate_midpoints_linestrings : Calculating the midpoints of LineStrings Note - ____ - + ---- The LineString must only consist of two points (start and end point) """ - # Checking that the LineString is a Shapely LineString if not isinstance(linestring, shapely.geometry.linestring.LineString): raise TypeError("Input geometry must be a Shapley LineString") @@ -6423,25 +6235,22 @@ def calculate_midpoints_linestrings( gpd.geodataframe.GeoDataFrame, List[shapely.geometry.linestring.LineString] ] ) -> List[shapely.geometry.point.Point]: - """Calculating the midpoints of LineStrings with two vertices each + """Calculate the midpoints of LineStrings with two vertices each. Parameters - __________ - + ---------- linestring_gdf: Union[gpd.geodataframe.GeoDataFrame, List[shapely.geometry.linestring.LineString]] GeoDataFrame containing LineStrings or list of LineStrings of which the midpoints will be calculated Returns - _______ - + ------- midpoint_list : List[shapely.geometry.point.Point] List of Shapely Points representing the midpoints of the provided LineStrings .. versionadded:: 1.0.x Example - _______ - + ------- >>> # Loading Libraries and creating LineString >>> import gemgis as gg >>> from shapely.geometry import Point, LineString @@ -6467,12 +6276,10 @@ def calculate_midpoints_linestrings( 'POINT (10 -10)' See Also - ________ - + -------- calculate_midpoint_linestring : Calculating the midpoint of one LineString """ - # Checking that the LineString is a Shapely LineString if not isinstance(linestring_gdf, (gpd.geodataframe.GeoDataFrame, list)): raise TypeError( @@ -6523,11 +6330,10 @@ def calculate_orientation_from_cross_section( profile_linestring: shapely.geometry.linestring.LineString, orientation_linestring: shapely.geometry.linestring.LineString, ) -> list: - """Calculating the orientation for one LineString on one cross sections + """Calculate the orientation for one LineString on one cross sections. Parameters - __________ - + ---------- profile_linestring : shapely.geometry.linestring.LineString Shapely LineString containing the trace of a cross section on a map, e.g. ``profile_linestring = LineString([(0, 0), (20, 20)])`` @@ -6537,16 +6343,14 @@ def calculate_orientation_from_cross_section( e.g. ``orientation_linestring = LineString([(2, -2), (5, -5)])`` Returns - _______ - + ------- orientation : list List containing a Shapely Point with X and Y coordinates, the Z value, dip, azimuth and polarity values .. versionadded:: 1.0.x Example - _______ - + ------- >>> # Loading Libraries and creating LineString >>> import gemgis as gg >>> from shapely.geometry import LineString @@ -6569,15 +6373,13 @@ def calculate_orientation_from_cross_section( 'POINT (2.474873734152916 2.474873734152916)' See Also - ________ - + -------- calculate_orientation_from_bent_cross_section : Calculating the orientation of a LineString on a bent cross section calculate_orientations_from_cross_section : Calculating orientations for LineStrings on a cross section extract_orientations_from_cross_sections : Calculating the orientations for LineStrings on cross sections """ - # Checking that the LineString is a Shapely LineString if not isinstance(profile_linestring, shapely.geometry.linestring.LineString): raise TypeError("Input geometry must be a Shapley LineString") @@ -6670,11 +6472,10 @@ def calculate_orientation_from_bent_cross_section( profile_linestring: shapely.geometry.linestring.LineString, orientation_linestring: shapely.geometry.linestring.LineString, ) -> list: - """Calculating the orientation of a LineString on a bent cross section provided as Shapely LineString + """Calculate the orientation of a LineString on a bent cross section provided as Shapely LineString. Parameters - __________ - + ---------- profile_linestring : shapely.geometry.linestring.LineString Shapely LineString containing the trace of a cross section on a map e.g. ``profile_linestring = LineString([(0, 0), (5, 10), (20, 20)])`` @@ -6684,16 +6485,14 @@ def calculate_orientation_from_bent_cross_section( e.g. ``orientation_linestring = LineString([(2, -2), (5, -5)])`` Returns - _______ - + ------- orientation : list List containing a Shapely Point with X and Y coordinates, the Z value, dip, azimuth and polarity values .. versionadded:: 1.0.x Example - _______ - + ------- >>> # Loading Libraries and creating LineString >>> import gemgis as gg >>> from shapely.geometry import LineString @@ -6716,14 +6515,12 @@ def calculate_orientation_from_bent_cross_section( 'POINT (1.565247584249853 3.130495168499706)' See Also - ________ - + -------- calculate_orientation_from_cross_section : Calculating the orientation of a LineString on a cross section calculate_orientations_from_cross_section : Calculating orientations for LineStrings on a cross section extract_orientations_from_cross_sections : Calculating the orientations for LineStrings on cross sections """ - # Checking that the LineString is a Shapely LineString if not isinstance(profile_linestring, shapely.geometry.linestring.LineString): raise TypeError("Input geometry must be a Shapley LineString") @@ -6815,11 +6612,10 @@ def calculate_orientations_from_cross_section( ], extract_coordinates: bool = True, ) -> gpd.geodataframe.GeoDataFrame: - """Calculating orientations from a cross sections using multiple LineStrings + """Calculate orientations from a cross sections using multiple LineStrings. Parameters - __________ - + ---------- profile_linestring : shapely.geometry.linestring.LineString Shapely LineString containing the trace of a cross section on a map, e.g. ``profile_linestring = LineString([(0, 0), (5, 10), (20, 20)])`` @@ -6832,16 +6628,14 @@ def calculate_orientations_from_cross_section( Options include: ``True`` or ``False``, default set to ``True`` Returns - _______ - + ------- gdf : gpd.geodataframe.GeoDataFrame GeoDataFrame containing the Shapely Points with X, Y coordinates, the Z value, dips, azimuths and polarities .. versionadded:: 1.0.x Example - _______ - + ------- >>> # Loading Libraries and creating LineString >>> import gemgis as gg >>> from shapely.geometry import LineString @@ -6865,15 +6659,13 @@ def calculate_orientations_from_cross_section( 1 1.57 3.13 -3.50 45.00 26.57 1.00 POINT (1.56525 3.13050) See Also - ________ - + -------- calculate_orientation_from_cross_section : Calculating the orientation of a LineString on a cross section calculate_orientation_from_bent_cross_section : Calculating orientations of a LineStrings on a bent cross section extract_orientations_from_cross_sections : Calculating the orientations for LineStrings on cross sections """ - # Checking that the LineString is a Shapely LineString if not isinstance(profile_linestring, shapely.geometry.linestring.LineString): raise TypeError("Input geometry must be a Shapley LineString") @@ -6958,11 +6750,10 @@ def extract_orientations_from_cross_sections( orientations_gdf: gpd.geodataframe.GeoDataFrame, profile_name_column: str = "name", ) -> gpd.geodataframe.GeoDataFrame: - """Calculating orientations digitized from cross sections + """Calculate orientations digitized from cross sections. Parameters - __________ - + ---------- profile_gdf : gpd.geodataframe.GeoDataFrame GeoDataFrame containing the different profile traces as LineStrings @@ -6973,16 +6764,14 @@ def extract_orientations_from_cross_sections( Name of the profile column, e.g. ``profile_name_column='name'``, default is ``'name'`` Returns - _______ - + ------- gdf : gpd.geodataframe.GeoDataFrame GeoDataFrame containing the orientation and location data for orientations digitized on cross sections .. versionadded:: 1.0.x Example - _______ - + ------- >>> # Loading Libraries and creating LineString >>> import gemgis as gg >>> from shapely.geometry import Point, LineString @@ -7019,7 +6808,6 @@ def extract_orientations_from_cross_sections( 1 2.47 -2.47 -3.50 45.00 135.00 1.00 POINT (2.47487 -2.47487) Profile1 """ - # Checking that the profile traces are provided as GeoDataFrame if not isinstance(profile_gdf, gpd.geodataframe.GeoDataFrame): raise TypeError("Profile traces must be provided as GeoDataFrame") @@ -7105,25 +6893,22 @@ def extract_orientations_from_cross_sections( def calculate_orientation_for_three_point_problem( gdf: gpd.geodataframe.GeoDataFrame, ) -> gpd.geodataframe.GeoDataFrame: - """Calculating the orientation for a three point problem + """Calculate the orientation for a three point problem. Parameters - __________ - + ---------- gdf : gpd.geodataframe.GeoDataFrame GeoDataFrame containing the three points and respective altitudes Returns - _______ - + ------- orientation : gpd.geodataframe.GeoDataFrame GeoDataFrame containing the calculated orientation value .. versionadded:: 1.0.x Example - _______ - + ------- >>> # Loading Libraries >>> import gemgis as gg >>> import geopandas as gpd @@ -7141,7 +6926,6 @@ def calculate_orientation_for_three_point_problem( 0 400.0 Coal 140.84 11.29 1 1214.43 1382.63 POINT (1214.432 1382.628) """ - # Checking that the points are provided as GeoDataFrame if not isinstance(gdf, gpd.geodataframe.GeoDataFrame): raise TypeError("Profile traces must be provided as GeoDataFrame") @@ -7228,11 +7012,10 @@ def intersect_polygon_polygon( polygon1: shapely.geometry.polygon.Polygon, polygon2: shapely.geometry.polygon.Polygon, ) -> Union[shapely.geometry.linestring.LineString, shapely.geometry.polygon.Polygon]: - """Calculating the intersection between to Shapely Polygons + """Calculate the intersection between to Shapely Polygons. Parameters - __________ - + ---------- polygon1 : shapely.geometry.polygon.Polygon First polygon used for intersecting, e.g. ``polygon1=Polygon([[0, 0], [10, 0], [10, 10], [0, 10], [0, 0]])`` @@ -7242,8 +7025,7 @@ def intersect_polygon_polygon( e.g. ``polygon2=Polygon([[0, 0], [10, 0], [10, 10], [0, 10], [0, 0]])`` Returns - _______ - + ------- intersection : Union[shapely.geometry.linestring.LineString, shapely.geometry.polygon.Polygon] Intersected geometry as Shapely Object @@ -7252,8 +7034,7 @@ def intersect_polygon_polygon( .. versionchanged:: 1.2 Example - _______ - + ------- >>> # Loading Libraries >>> import gemgis as gg >>> from shapely.geometry import Polygon @@ -7272,14 +7053,12 @@ def intersect_polygon_polygon( 'LINESTRING (10 0, 10 10)' See Also - ________ - + -------- intersect_polygon_polygons : Intersecting a polygon with mutiple polygons intersect_polygons_polygons : Intersecting multiple polygons with multiple polygons extract_xy_from_polygon_intersections : Extracting intersections between multiple polygons """ - # Checking that the input polygon is a Shapely Polygon if not isinstance(polygon1, shapely.geometry.polygon.Polygon): raise TypeError("Input Polygon1 must a be Shapely Polygon") @@ -7316,11 +7095,10 @@ def intersect_polygon_polygons( gpd.geodataframe.GeoDataFrame, List[shapely.geometry.polygon.Polygon] ], ) -> List[shapely.geometry.base.BaseGeometry]: - """Calculating the intersections between one polygon and a list of polygons + """Calculate the intersections between one polygon and a list of polygons. Parameters - __________ - + ---------- polygon1 : shapely.geometry.polygon.Polygon First polygon used for intersecting, e.g. ``polygon1=Polygon([[0, 0], [10, 0], [10, 10], [0, 10], [0, 0]])`` @@ -7329,8 +7107,7 @@ def intersect_polygon_polygons( List of polygons as list or GeoDataFrame to get intersected Returns - _______ - + ------- intersections : List[shapely.geometry.base.BaseGeometry] List of intersected geometries @@ -7339,8 +7116,7 @@ def intersect_polygon_polygons( .. versionchanged:: 1.2 Example - _______ - + ------- >>> # Loading Libraries and creating Polygon >>> import gemgis as gg >>> from shapely.geometry import Polygon @@ -7371,14 +7147,12 @@ def intersect_polygon_polygons( 'LINESTRING (10 0, 10 10)' See Also - ________ - + -------- intersect_polygon_polygon : Intersecting a polygon with a polygon intersect_polygons_polygons : Intersecting multiple polygons with multiple polygons extract_xy_from_polygon_intersections : Extracting intersections between multiple polygons """ - # Checking that the input polygon is a Shapely Polygon if not isinstance(polygon1, shapely.geometry.polygon.Polygon): raise TypeError("Input Polygon1 must a be Shapely Polygon") @@ -7429,11 +7203,10 @@ def intersect_polygons_polygons( gpd.geodataframe.GeoDataFrame, List[shapely.geometry.polygon.Polygon] ], ) -> List[shapely.geometry.base.BaseGeometry]: - """Calculating the intersections between a list of Polygons + """Calculate the intersections between a list of Polygons. Parameters - __________ - + ---------- polygons1 : Union[gpd.geodataframe.GeoDataFrame, List[shapely.geometry.polygon.Polygon]] List of Polygons or GeoDataFrame containing Polygons to be intersected @@ -7441,8 +7214,7 @@ def intersect_polygons_polygons( List of Polygons or GeoDataFrame containing Polygons to be intersected Returns - _______ - + ------- intersections : List[shapely.geometry.base.BaseGeometry] List of intersected geometries @@ -7451,8 +7223,7 @@ def intersect_polygons_polygons( .. versionchanged:: 1.2 Example - _______ - + ------- >>> # Loading Libraries and creating Polygon >>> import gemgis as gg >>> from shapely.geometry import Polygon @@ -7496,14 +7267,12 @@ def intersect_polygons_polygons( 'LINESTRING (10 0, 10 10)' See Also - ________ - + -------- intersect_polygon_polygon : Intersecting a polygon with a polygon intersect_polygon_polygons : Intersecting a polygons with multiple polygons extract_xy_from_polygon_intersections : Extracting intersections between multiple polygons """ - # Checking that the input polygon is a list or a GeoDataFrame if not isinstance(polygons1, (gpd.geodataframe.GeoDataFrame, list)): raise TypeError("Input Polygon2 must a be Shapely Polygon") @@ -7569,11 +7338,10 @@ def extract_xy_from_polygon_intersections( extract_coordinates: bool = False, drop_index: bool = True, ) -> gpd.geodataframe.GeoDataFrame: - """Calculating the intersections between Polygons; the table must be sorted by stratigraphic age + """Calculate the intersections between Polygons; the table must be sorted by stratigraphic age. Parameters - __________ - + ---------- gdf : gpd.geodataframe.GeoDataFrame GeoDataFrame containing Polygons of a geological map ordered by their stratigraphic age @@ -7586,16 +7354,14 @@ def extract_xy_from_polygon_intersections( Options include: ``True`` or ``False``, default set to ``True`` Returns - _______ - + ------- intersections : gpd.geodataframe.GeoDataFrame GeoDataFrame containing the intersections of the polygons of a geological map .. versionadded:: 1.0.x Example - _______ - + ------- >>> # Loading Libraries and creating Polygon >>> import gemgis as gg >>> from shapely.geometry import Polygon @@ -7624,14 +7390,12 @@ def extract_xy_from_polygon_intersections( 0 Formation1 LINESTRING (10.0 0.0, 10.0 10.0) See Also - ________ - + -------- intersection_polygon_polygon: Intersecting a polygon with a polygon intersections_polygon_polygons: Intersecting a polygons with multiple polygons intersections_polygons_polygons: Intersecting multiple polygons with multiple polygons """ - # Checking that the polygons of the geological map are provided as GeoDataFrame if not isinstance(gdf, gpd.geodataframe.GeoDataFrame): raise TypeError("Input Geometries must be stored as GeoDataFrame") @@ -7714,25 +7478,22 @@ def calculate_azimuth( gpd.geodataframe.GeoDataFrame, List[shapely.geometry.linestring.LineString] ] ) -> List[Union[float, int]]: - """Calculating the azimuth for an orientation Geodataframe represented by LineStrings + """Calculate the azimuth for an orientation Geodataframe represented by LineStrings. Parameters - __________ - + ---------- gdf : Union[gpd.geodataframe.GeoDataFrame, List[shapely.geometry.linestring.LineString] GeoDataFrame or list containing the LineStrings of orientations Returns - _______ - + ------- azimuth_list: List[Union[float, int]] List containing the azimuth values of the orientation LineString .. versionadded:: 1.0.x Example - _______ - + ------- >>> # Loading Libraries and creating LineString >>> import gemgis as gg >>> from shapely.geometry import LineString @@ -7759,8 +7520,7 @@ def calculate_azimuth( [135.0, 116.56505117707799] See Also - ________ - + -------- create_linestring_from_points : Create LineString from points create_linestring_gdf : Create GeoDataFrame with LineStrings from points extract_orientations_from_map : Extracting orientations from a map @@ -7768,7 +7528,6 @@ def calculate_azimuth( calculate_orientations_from_strike_lines : Calculating the orientations from strike lines """ - # Checking that gdf is a GeoDataFrame if not isinstance(gdf, (gpd.geodataframe.GeoDataFrame, list)): raise TypeError("Data must be a GeoDataFrame or a list of LineStrings") @@ -7801,32 +7560,26 @@ def calculate_azimuth( def create_linestring_from_points( gdf: gpd.geodataframe.GeoDataFrame, formation: str, altitude: Union[int, float] ) -> shapely.geometry.linestring.LineString: - """Creating a LineString object from a GeoDataFrame containing surface points at a given altitude and for a given - formation + """Create a LineString object from a GeoDataFrame containing surface points at a given altitude and formation. Parameters - __________ - + ---------- gdf : gpd.geodataframe.GeoDataFrame - GeoDataFrame containing the points of intersections between topographic contours and layer boundaries - + GeoDataFrame containing the points of intersections between topographic contours and layer boundaries. formation : str Name of the formation, e.g. ``formation='Layer1'`` - altitude : Union[int, float] Value of the altitude of the points, e.g. ``altitude=100`` Returns - _______ - + ------- linestring: shapely.geometry.linestring.LineString LineString containing a LineString object .. versionadded:: 1.0.x Example - _______ - + ------- >>> # Loading Libraries and creating points >>> import gemgis as gg >>> from shapely.geometry import Point @@ -7849,8 +7602,7 @@ def create_linestring_from_points( 'LINESTRING (0 0, 10 10)' See Also - ________ - + -------- calculate_azimuth : Calculating the azimuth for orientations on a map create_linestring_gdf : Create GeoDataFrame with LineStrings from points extract_orientations_from_map : Extracting orientations from a map @@ -7858,7 +7610,6 @@ def create_linestring_from_points( calculate_orientations_from_strike_lines : Calculating the orientations from strike lines """ - # Checking if gdf is of type GeoDataFrame if not isinstance(gdf, gpd.geodataframe.GeoDataFrame): raise TypeError("gdf must be of type GeoDataFrame") @@ -7899,25 +7650,22 @@ def create_linestring_from_points( def create_linestring_gdf( gdf: gpd.geodataframe.GeoDataFrame, ) -> gpd.geodataframe.GeoDataFrame: - """Creating LineStrings from Points + """Create LineStrings from Points. Parameters - __________ - + ---------- gdf : gpd.geodataframe.GeoDataFrame GeoDataFrame containing the points of intersections between topographic contours and layer boundaries Returns - _______ - + ------- gdf_linestring : gpd.geodataframe.GeoDataFrame GeoDataFrame containing LineStrings .. versionadded:: 1.0.x Example - _______ - + ------- >>> # Loading Libraries and creating Points >>> import gemgis as gg >>> from shapely.geometry import Point @@ -7942,8 +7690,7 @@ def create_linestring_gdf( 0 0 Layer1 100 1 LINESTRING (0.00000 0.00000, 10.00000 10.00000) See Also - ________ - + -------- calculate_azimuth : Calculating the azimuth for orientations on a map create_linestring_from_points : Create LineString from points extract_orientations_from_map : Extracting orientations from a map @@ -7951,7 +7698,6 @@ def create_linestring_gdf( calculate_orientations_from_strike_lines : Calculating the orientations from strike lines """ - # Checking if gdf is of type GeoDataFrame if not isinstance(gdf, gpd.geodataframe.GeoDataFrame): raise TypeError("gdf must be of type GeoDataFrame") @@ -8003,28 +7749,24 @@ def create_linestring_gdf( def extract_orientations_from_map( gdf: gpd.geodataframe.GeoDataFrame, dz: str = "dZ" ) -> gpd.geodataframe.GeoDataFrame: - """Calculating orientations from LineStrings + """Calculate orientations from LineStrings. Parameters - _________ - + ---------- gdf : gpd.geodataframe.GeoDataFrame GeoDataFrame containing the orientation LineStrings - dz : str Name of the height difference column, e.g. ``dz='dZ'`` Returns - _______ - + ------- gdf : gpd.geodataframe.GeoDataFrame GeoDataFrame containing the orientation values .. versionadded:: 1.0.x Example - _______ - + ------- >>> # Loading Libraries and creating LineString >>> import gemgis as gg >>> from shapely.geometry import LineString @@ -8054,8 +7796,7 @@ def extract_orientations_from_map( 1 POINT (10.0 -5.0) 116.57 83.62 10.00 -5.00 1 See Also - ________ - + -------- calculate_azimuth : Calculating the azimuth for orientations on a map create_linestring_from_points : Create LineString from points create_linestring_gdf : Create GeoDataFrame with LineStrings from points @@ -8063,7 +7804,6 @@ def extract_orientations_from_map( calculate_orientations_from_strike_lines : Calculating the orientations from strike lines """ - # Checking that gdf is a GeoDataFrame if not isinstance(gdf, gpd.geodataframe.GeoDataFrame): raise TypeError("Data must be a GeoDataFrame") @@ -8121,11 +7861,10 @@ def calculate_distance_linestrings( ls1: shapely.geometry.linestring.LineString, ls2: shapely.geometry.linestring.LineString, ) -> float: - """Calculating the minimal distance between two LineStrings + """Calculate the minimal distance between two LineStrings. Parameters - __________ - + ---------- ls1 : shapely.geometry.linestring.LineString LineString 1, e.g. ``ls1 = LineString([(0, 0), (10, 10), (20, 20)])`` @@ -8133,15 +7872,14 @@ def calculate_distance_linestrings( LineString 2, e.g. ``ls2 = LineString([(0, 0), (10, 10), (20, 20)])`` Returns - _______ - + ------- distance : float Minimum distance between two Shapely LineStrings .. versionadded:: 1.0.x - Example: - + Example + ------- >>> # Loading Libraries and creating LineStrings >>> import gemgis as gg >>> from shapely.geometry import LineString @@ -8160,8 +7898,7 @@ def calculate_distance_linestrings( 7.0710678118654755 See Also - ________ - + -------- calculate_azimuth : Calculating the azimuth for orientations on a map create_linestring_from_points : Create LineString from points create_linestring_gdf : Create GeoDataFrame with LineStrings from points @@ -8169,7 +7906,6 @@ def calculate_distance_linestrings( calculate_orientations_from_strike_lines : Calculating the orientations from strike lines """ - # Checking that ls1 is a Shapely LineString if not isinstance(ls1, shapely.geometry.linestring.LineString): raise TypeError("Line Object must be a Shapely LineString") @@ -8203,17 +7939,15 @@ def calculate_distance_linestrings( def calculate_orientations_from_strike_lines( gdf: gpd.geodataframe.GeoDataFrame, ) -> gpd.geodataframe.GeoDataFrame: - """Calculating orientations based on LineStrings representing strike lines + """Calculate orientations based on LineStrings representing strike lines. Parameters - __________ - + ---------- gdf : gpd.geodataframe.GeoDataFrame GeoDataFrame containing LineStrings representing strike lines Returns - _______ - + ------- gdf_orient : gpd.geodataframe.GeoDataFrame GeoDataFrame containing the location of orientation measurements and their associated orientation values @@ -8223,8 +7957,7 @@ def calculate_orientations_from_strike_lines( Fixing indexing issue. Example - _______ - + ------- >>> # Loading Libraries and creating LineString >>> import gemgis as gg >>> from shapely.geometry import LineString @@ -8254,8 +7987,7 @@ def calculate_orientations_from_strike_lines( 0 85.96 135.00 150.00 POINT (10.0 15.0) 1.00 10.00 15.00 See Also - ________ - + -------- calculate_azimuth : Calculating the azimuth for orientations on a map create_linestring_from_points : Create LineString from points create_linestring_gdf : Create GeoDataFrame with LineStrings from points @@ -8263,7 +7995,6 @@ def calculate_orientations_from_strike_lines( calculate_distance_linestrings : Calculating the distance between two LineStrings """ - # Checking that gdf is a GeoDataFrame if not isinstance(gdf, gpd.geodataframe.GeoDataFrame): raise TypeError("Data must be a GeoDataFrame") @@ -8389,11 +8120,10 @@ def calculate_orientations_from_strike_lines( def load_gpx(path: str, layer: Union[int, str] = "tracks") -> Collection: - """Loading a GPX file as collection + """Load a GPX file as collection. Parameters - __________ - + ---------- path : str Path to the GPX file, e.g. ``path='file.gpx'`` @@ -8402,16 +8132,14 @@ def load_gpx(path: str, layer: Union[int, str] = "tracks") -> Collection: ``tracks`` Returns - _______ - + ------- gpx : dict Collection containing the GPX data .. versionadded:: 1.0.x Example - _______ - + ------- >>> # Loading Libraries and File >>> import gemgis as gg >>> gpx = gg.vector.load_gpx(path='file.gpx', layer='tracks') @@ -8419,8 +8147,7 @@ def load_gpx(path: str, layer: Union[int, str] = "tracks") -> Collection: See Also - ________ - + -------- load_gpx_as_dict : Loading a GPX file as dict load_gpx_as_geometry : Loading a GPX file as Shapely BaseGeometry @@ -8429,7 +8156,6 @@ def load_gpx(path: str, layer: Union[int, str] = "tracks") -> Collection: .. versionchanged:: 1.2 """ - # Trying to import fiona but returning error if fiona is not installed try: import fiona @@ -8463,11 +8189,10 @@ def load_gpx(path: str, layer: Union[int, str] = "tracks") -> Collection: def load_gpx_as_dict(path: str, layer: Union[int, str] = "tracks") -> Collection: - """Loading a GPX file as dict + """Load a GPX file as dict. Parameters - __________ - + ---------- path : str Path to the GPX file, e.g. ``path='file.gpx'`` @@ -8476,8 +8201,7 @@ def load_gpx_as_dict(path: str, layer: Union[int, str] = "tracks") -> Collection ``tracks`` Returns - _______ - + ------- gpx_dict : dict Dict containing the GPX data @@ -8486,8 +8210,7 @@ def load_gpx_as_dict(path: str, layer: Union[int, str] = "tracks") -> Collection .. versionchanged:: 1.2 Example - _______ - + ------- >>> # Loading Libraries and File >>> import gemgis as gg >>> gpx = gg.vector.load_gpx_as_dict(path='file.gpx', layer='tracks') @@ -8513,8 +8236,7 @@ def load_gpx_as_dict(path: str, layer: Union[int, str] = "tracks") -> Collection (8.496234, 52.705629),...]]}} See Also - ________ - + -------- load_gpx_as : Loading a GPX file as Collection load_gpx_as_geometry : Loading a GPX file as Shapely BaseGeometry @@ -8523,7 +8245,6 @@ def load_gpx_as_dict(path: str, layer: Union[int, str] = "tracks") -> Collection .. versionchanged:: 1.2 """ - # Trying to import fiona but returning error if fiona is not installed try: import fiona @@ -8562,11 +8283,10 @@ def load_gpx_as_dict(path: str, layer: Union[int, str] = "tracks") -> Collection def load_gpx_as_geometry( path: str, layer: Union[int, str] = "tracks" ) -> shapely.geometry.base.BaseGeometry: - """Loading a GPX file as Shapely Geometry + """Load a GPX file as Shapely Geometry. Parameters - __________ - + ---------- path : str Path to the GPX file, e.g. ``path='file.gpx'`` layer : Union[int, str] @@ -8574,8 +8294,7 @@ def load_gpx_as_geometry( ``tracks`` Returns - _______ - + ------- shape : shapely.geometry.base.BaseGeometry Shapely BaseGeometry containing the geometry data of the GPX file @@ -8584,8 +8303,7 @@ def load_gpx_as_geometry( .. versionchanged:: 1.2 Example - _______ - + ------- >>> # Loading Libraries and File >>> import gemgis as gg >>> gpx = gg.vector.load_gpx_as_geometry(path='file.gpx', layer='tracks') @@ -8594,15 +8312,13 @@ def load_gpx_as_geometry( 52.705664, 8.496181 52.705705, 8.496171 52.705754,...) See Also - ________ - + -------- load_gpx : Loading a GPX file as Collection load_gpx_as_dict : Loading a GPX file as dict .. versionchanged:: 1.2 """ - # Trying to import fiona but returning error if fiona is not installed try: import fiona @@ -8651,7 +8367,7 @@ def load_gpx_as_gdf(path: str, layer: Union[int, str] = "tracks") -> gpd.geodata """Load GPX File as GeoPandas GeoDataFrame. Parameters - __________ + ---------- path : str Path to the GPX file, e.g. ``path='file.gpx'``. layer : Union[int, str], default: `'tracks'` @@ -8659,7 +8375,7 @@ def load_gpx_as_gdf(path: str, layer: Union[int, str] = "tracks") -> gpd.geodata ``tracks``. Returns - _______ + ------- gdf : gpd.geodataframe.GeoDataFrame GeoPandas GeoDataFrame containing the GPX Data @@ -8677,25 +8393,23 @@ def load_gpx_as_gdf(path: str, layer: Union[int, str] = "tracks") -> gpd.geodata .. versionadded:: 1.2 Example - _______ - - >>> # Loading Libraries and File - >>> import gemgis as gg - >>> gpx = gg.vector.load_gpx_as_gdf(path='file.gpx', layer='tracks') - >>> gpx - +----+-----------------------------+--------+-------------------------------+ - | ID | geometry | ele | time | - +----+-----------------------------+--------+-------------------------------+ - | 0 | POINT (-71.11928 42.43888) | 44.59 | 2001-11-28 21:05:28+00:00 | - +----+-----------------------------+--------+-------------------------------+ - | 1 | POINT (-71.11969 42.43923) | 57.61 | 2001-06-02 03:26:55+00:00 | - +----+-----------------------------+--------+-------------------------------+ - | 2 | POINT (-71.11615 42.43892) | 44.83 | 2001-11-16 23:03:38+00:00 | - +----+-----------------------------+--------+-------------------------------+ + ------- + >>> # Loading Libraries and File + >>> import gemgis as gg + >>> gpx = gg.vector.load_gpx_as_gdf(path='file.gpx', layer='tracks') + >>> gpx + +----+-----------------------------+--------+-------------------------------+ + | ID | geometry | ele | time | + +----+-----------------------------+--------+-------------------------------+ + | 0 | POINT (-71.11928 42.43888) | 44.59 | 2001-11-28 21:05:28+00:00 | + +----+-----------------------------+--------+-------------------------------+ + | 1 | POINT (-71.11969 42.43923) | 57.61 | 2001-06-02 03:26:55+00:00 | + +----+-----------------------------+--------+-------------------------------+ + | 2 | POINT (-71.11615 42.43892) | 44.83 | 2001-11-16 23:03:38+00:00 | + +----+-----------------------------+--------+-------------------------------+ See Also - ________ - + -------- load_gpx : Load a GPX file as Collection load_gpx_as_dict : Load a GPX file as dict load_gpx_as_geometry : Load a GPX file as geometry @@ -8743,11 +8457,10 @@ def sort_by_stratigraphy( stratigraphy: List[str], formation_column: str = "formation", ) -> gpd.geodataframe.GeoDataFrame: - """Sorting a GeoDataFrame by a provided list of Stratigraphic Units + """Sort a GeoDataFrame by a provided list of Stratigraphic Units. Parameters - __________ - + ---------- gdf : gpd.geodataframe.GeoDataFrame GeoDataFrame containing the unsorted input polygons @@ -8758,16 +8471,14 @@ def sort_by_stratigraphy( Name of the formation column, default is formation, e.g. ``formation_colum='formation'`` Returns - _______ - + ------- gdf_sorted : gpd.geodataframe.GeoDataFrame GeoDataFrame containing the sorted input polygons .. versionadded:: 1.0.x Example - _______ - + ------- >>> # Loading Libraries and creating Polygon >>> import gemgis as gg >>> from shapely.geometry import Polygon @@ -8800,7 +8511,6 @@ def sort_by_stratigraphy( 1 POLYGON ((0.00000 0.00000, 1.00000 1.00000, 1.... Layer2 """ - # Checking that the input data is provided as GeoDataFrame if not isinstance(gdf, gpd.geodataframe.GeoDataFrame): raise TypeError("Input Geometries must be stored as GeoDataFrame") @@ -8839,25 +8549,22 @@ def sort_by_stratigraphy( def create_bbox(extent: List[Union[int, float]]) -> shapely.geometry.polygon.Polygon: - """Creating a rectangular polygon from the provided bounding box values, with counter-clockwise order by default. + """Create a rectangular polygon from the provided bounding box values, with counter-clockwise order by default. Parameters - __________ - + ---------- extent : List[Union[int, float]] List of minx, maxx, miny, maxy values, e.g. ``extent=[0, 972, 0, 1069]`` Returns - _______ - + ------- bbox : shapely.geometry.polygon.Polygon Rectangular polygon based on extent .. versionadded:: 1.0.x Example - _______ - + ------- >>> # Loading Libraries >>> import gemgis as gg @@ -8870,7 +8577,6 @@ def create_bbox(extent: List[Union[int, float]]) -> shapely.geometry.polygon.Pol 'POLYGON ((972 0, 972 1069, 0 1069, 0 0, 972 0))' """ - # Checking if extent is a list if not isinstance(extent, list): raise TypeError("Extent must be of type list") @@ -8894,11 +8600,10 @@ def set_dtype( y: str = "Y", z: str = "Z", ) -> gpd.geodataframe.GeoDataFrame: - """Checking and setting the dtypes of the input data GeoDataFrame + """Check and set the dtypes of the input data GeoDataFrame. Parameters - __________ - + ---------- gdf : gpd.geodataframe.GeoDataFrame GeoDataFrame containing the input vector data with uncorrected dtypes @@ -8924,16 +8629,14 @@ def set_dtype( Name of the column containing the z coordinates, e.g ``z='Z'`` Returns - _______ - + ------- gdf : gpd.geodataframe.GeoDataFrame GeoDataFrame containing the input vector data with corrected dtypes .. versionadded:: 1.0.x Example - _______ - + ------- >>> # Loading Libraries and File >>> import gemgis as gg >>> import geopandas as gpd @@ -8943,7 +8646,6 @@ def set_dtype( >>> gdf_dtypes = gg.vector.set_dtype(gdf=gdf) """ - # Input object must be a GeoDataFrame if not isinstance(gdf, gpd.geodataframe.GeoDataFrame): raise TypeError("Loaded object is not a GeoDataFrame") @@ -9016,11 +8718,10 @@ def create_polygons_from_faces( crs: Union[str, pyproj.crs.crs.CRS], return_gdf: bool = True, ) -> Union[List[shapely.geometry.polygon.Polygon], gpd.geodataframe.GeoDataFrame]: - """Extracting faces from PyVista PolyData as Shapely Polygons + """Extract faces from PyVista PolyData as Shapely Polygons. Parameters - __________ - + ---------- mesh : pv.core.pointset.PolyData PyVista PolyData dataset @@ -9033,16 +8734,14 @@ def create_polygons_from_faces( Options include: ``True`` or ``False``, default set to ``True`` Returns - _______ - + ------- polygons : Union[List[shapely.geometry.polygon.Polygon], gpd.geodataframe.GeoDataFrame] Triangular Shapely Polygons representing the faces of the mesh .. versionadded:: 1.0.x Example - _______ - + ------- >>> # Importing Libraries and File >>> import gemgis as gg >>> import pyvista as pv @@ -9071,7 +8770,6 @@ def create_polygons_from_faces( 4 POLYGON Z ((295827.680 5680951.574 -825.328, 2... """ - # Checking that the input mesh is a PyVista PolyData dataset if not isinstance(mesh, pv.core.pointset.PolyData): raise TypeError("Input mesh must be a PyVista PolyData dataset") @@ -9107,11 +8805,10 @@ def unify_polygons( crs: Union[str, pyproj.crs.crs.CRS] = None, return_gdf: bool = True, ) -> Union[List[shapely.geometry.polygon.Polygon], gpd.geodataframe.GeoDataFrame]: - """Unifying adjacent triangular polygons to form larger objects + """Unify adjacent triangular polygons to form larger objects. Parameters - __________ - + ---------- polygons : Union[List[shapely.geometry.polygon.Polygon], gpd.geodataframe.GeoDataFrame] Triangular Shapely Polygons representing the faces of the mesh @@ -9123,16 +8820,14 @@ def unify_polygons( Options include: ``True`` or ``False``, default set to ``True`` Returns - _______ - + ------- polygons_merged : Union[List[shapely.geometry.polygon.Polygon], gpd.geodataframe.GeoDataFrame] Merged Shapely Polygons .. versionadded:: 1.0.x Example - _______ - + ------- >>> # Loading Libraries and File >>> import gemgis as gg >>> import geopandas as gpd @@ -9156,7 +8851,6 @@ def unify_polygons( 4 POLYGON Z ((384393.963 5714293.104 -614.106, 3... """ - # Checking that the polygons are of type list of a GeoDataFrame if not isinstance(polygons, (list, gpd.geodataframe.GeoDataFrame)): raise TypeError( @@ -9215,11 +8909,10 @@ def unify_linestrings( crs: Union[str, pyproj.crs.crs.CRS] = None, return_gdf: bool = True, ) -> Union[List[shapely.geometry.linestring.LineString], gpd.geodataframe.GeoDataFrame]: - """Unifying adjacent LineStrings to form LineStrings with multiple vertices + """Unify adjacent LineStrings to form LineStrings with multiple vertices. Parameters - __________ - + ---------- linestrings : Union[List[shapely.geometry.linestring.LineString], gpd.geodataframe.GeoDataFrame] LineStrings consisting of two vertices representing extracted contour lines @@ -9231,16 +8924,14 @@ def unify_linestrings( Options include: ``True`` or ``False``, default set to ``True`` Returns - _______ - + ------- linestrings_merged : Union[List[shapely.geometry.linestring.LineString], gpd.geodataframe.GeoDataFrame] Merged Shapely LineStrings .. versionadded:: 1.0.x Example - _______ - + ------- >>> # Loading Libraries and File >>> import gemgis as gg >>> import geopandas as gpd @@ -9264,7 +8955,6 @@ def unify_linestrings( 4 LINESTRING Z (32332516.312 5722028.768 -250.00... """ - # Checking that the linestrings are of type list of a GeoDataFrame if not isinstance(linestrings, (list, gpd.geodataframe.GeoDataFrame)): raise TypeError( @@ -9320,11 +9010,10 @@ def unify_linestrings( def create_hexagon(center: shapely.geometry.Point, radius: Union[int, float]): - """Function to create one hexagon + """Create one hexagon. Parameters - __________ - + ---------- center: shapely.geometry.Point Shapely Point representing the center of the hexagon @@ -9332,8 +9021,7 @@ def create_hexagon(center: shapely.geometry.Point, radius: Union[int, float]): Radius of the hexagon Returns - _______ - + ------- geometry.Polygon(hex_coords): shapely.geometry.Polygon Shapley Polygon in the shape of a hexagon @@ -9342,13 +9030,11 @@ def create_hexagon(center: shapely.geometry.Point, radius: Union[int, float]): .. versionchanged:: 1.1.3 Optimized creation of hexagon - See also - ________ - + See Also + -------- create_hexagon_grid : Creating a hexagon grid """ - # Checking that the center point is provided as Shapely Point if not isinstance(center, geometry.Point): raise TypeError("Center point of the hexagon must be provided as Shapely Point") @@ -9371,11 +9057,10 @@ def create_hexagon(center: shapely.geometry.Point, radius: Union[int, float]): def create_hexagon_grid( gdf: gpd.GeoDataFrame, radius: Union[int, float], crop_gdf: bool = True ): - """Function to create a grid of hexagons based on a GeoDataFrame containing Polygons and a radius provided for the single hexagons + """Create a grid of hexagons based on a GeoDataFrame containing Polygons and a radius provided for single hexagons. Parameters - __________ - + ---------- gdf: gpd.GeoDataFrame GeoDataFrame containing the polygons for which a hexagon grid is created @@ -9387,8 +9072,7 @@ def create_hexagon_grid( Options include: ``True`` or ``False``, default set to ``True`` Returns - _______ - + ------- hex_gdf: gpd.GeoDataFrame GeoDataFrame containing the hexagon grid @@ -9397,13 +9081,11 @@ def create_hexagon_grid( .. versionchanged:: 1.1.3 Optimized creation of hexagon - See also - ________ - + See Also + -------- create_hexagon : Creating one hexagon based on a given center and radius """ - # Checking that the gdf is of type GeoDataFrame if not isinstance(gdf, gpd.GeoDataFrame): raise TypeError("gdf must be of type GeoDataFrame") @@ -9473,35 +9155,33 @@ def create_hexagon_grid( def create_voronoi_polygons( gdf: gpd.geodataframe.GeoDataFrame, ) -> gpd.geodataframe.GeoDataFrame: - """Function to create Voronoi Polygons from Point GeoDataFrame using the SciPy Spatial Voronoi class - (https://docs.scipy.org/doc/scipy/reference/generated/scipy.spatial.Voronoi.html#scipy.spatial.Voronoi) - + """Create Voronoi Polygons from Point GeoDataFrame using the SciPy Spatial Voronoi class. Parameters - __________ - + ---------- gdf: gpd.geodataframe.GeoDataFrame GeoDataFrame containing the Shapely Points Returns - _______ - + ------- gdf_polygons: gpd.geodataframe.GeoDataFrame GeoDataFrame containing the valid Voronoi Polygons .. versionadded:: 1.1 Example - ________ - + -------- >>> # Loading Libraries and File >>> import gemgis as gg >>> import geopandas as gpd >>> gdf = gpd.read_file('file.shp') >>> gdf_polygons = gg.vector.create_voronoi_polygons(gdf=gdf) - """ + Note + ---- + (https://docs.scipy.org/doc/scipy/reference/generated/scipy.spatial.Voronoi.html#scipy.spatial.Voronoi) + """ # Checking that the gdf is of type GeoDataFrame if not isinstance(gdf, gpd.geodataframe.GeoDataFrame): raise TypeError("gdf must be provided as GeoDataFrame") From 51bdd6676e3d07b57c2e390980edae3333bf0ec9 Mon Sep 17 00:00:00 2001 From: AlexanderJuestel Date: Fri, 3 Jan 2025 11:22:52 +0100 Subject: [PATCH 62/63] Linting File --- gemgis/vector.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/gemgis/vector.py b/gemgis/vector.py index 27d88c3a..f5085995 100644 --- a/gemgis/vector.py +++ b/gemgis/vector.py @@ -7414,7 +7414,7 @@ def extract_xy_from_polygon_intersections( gdf["formation"].isin([gdf["formation"].unique().tolist()[i]]) ], polygons2=gdf[ - gdf["formation"].isin(gdf["formation"].unique().tolist()[i + 1 :]) + gdf["formation"].isin(gdf["formation"].unique().tolist()[i + 1:]) ], ) for i in range(len(gdf["formation"].unique().tolist())) @@ -8363,7 +8363,9 @@ def load_gpx_as_geometry( return shape -def load_gpx_as_gdf(path: str, layer: Union[int, str] = "tracks") -> gpd.geodataframe.GeoDataFrame: +def load_gpx_as_gdf( + path: str, layer: Union[int, str] = "tracks" +) -> gpd.geodataframe.GeoDataFrame: """Load GPX File as GeoPandas GeoDataFrame. Parameters @@ -8442,8 +8444,7 @@ def load_gpx_as_gdf(path: str, layer: Union[int, str] = "tracks") -> gpd.geodata raise TypeError("The data must be provided as gpx file") # Opening GPX File - gdf = pyogrio.read_datamframe(path_or_buffer=path, - layer=layer) + gdf = pyogrio.read_datamframe(path_or_buffer=path, layer=layer) return gdf From b06eb221ce0485fbd81d405de60e8d5e3a0fb41a Mon Sep 17 00:00:00 2001 From: AlexanderJuestel Date: Fri, 3 Jan 2025 11:41:38 +0100 Subject: [PATCH 63/63] Update docstrings --- gemgis/vector.py | 154 ++++++++++++++++++++++++----------------------- 1 file changed, 80 insertions(+), 74 deletions(-) diff --git a/gemgis/vector.py b/gemgis/vector.py index f5085995..27a76322 100644 --- a/gemgis/vector.py +++ b/gemgis/vector.py @@ -8555,12 +8555,12 @@ def create_bbox(extent: List[Union[int, float]]) -> shapely.geometry.polygon.Pol Parameters ---------- extent : List[Union[int, float]] - List of minx, maxx, miny, maxy values, e.g. ``extent=[0, 972, 0, 1069]`` + List of minx, maxx, miny, maxy values, e.g. ``extent=[0, 972, 0, 1069]``. Returns ------- bbox : shapely.geometry.polygon.Polygon - Rectangular polygon based on extent + Rectangular polygon based on extent. .. versionadded:: 1.0.x @@ -8606,33 +8606,33 @@ def set_dtype( Parameters ---------- gdf : gpd.geodataframe.GeoDataFrame - GeoDataFrame containing the input vector data with uncorrected dtypes + GeoDataFrame containing the input vector data with uncorrected dtypes. dip : str - Name of the column containing the dip data, e.g ``dip='dip'`` + Name of the column containing the dip data, e.g ``dip='dip'``. azimuth : str - Name of the column containing the azimuth data, e.g ``azimuth='azimuth'`` + Name of the column containing the azimuth data, e.g ``azimuth='azimuth'``. formation : str - Name of the column containing the formation data, e.g ``formation='formation'`` + Name of the column containing the formation data, e.g ``formation='formation'``. polarity : str - Name of the column containing the polarity data, e.g ``polarity='polarity'`` + Name of the column containing the polarity data, e.g ``polarity='polarity'``. x : str - Name of the column containing the x coordinates, e.g ``x='X'`` + Name of the column containing the x coordinates, e.g ``x='X'``. y : str - Name of the column containing the y coordinates, e.g ``y='Y'`` + Name of the column containing the y coordinates, e.g ``y='Y'``. z : str - Name of the column containing the z coordinates, e.g ``z='Z'`` + Name of the column containing the z coordinates, e.g ``z='Z'``. Returns ------- gdf : gpd.geodataframe.GeoDataFrame - GeoDataFrame containing the input vector data with corrected dtypes + GeoDataFrame containing the input vector data with corrected dtypes. .. versionadded:: 1.0.x @@ -8724,20 +8724,20 @@ def create_polygons_from_faces( Parameters ---------- mesh : pv.core.pointset.PolyData - PyVista PolyData dataset + PyVista PolyData dataset. crs : Union[str, pyproj.crs.crs.CRS] - Name of the CRS provided to reproject coordinates of the GeoDataFrame, e.g. ``crs='EPSG:4647'`` + Name of the CRS provided to reproject coordinates of the GeoDataFrame, e.g. ``crs='EPSG:4647'``. return_gdf : bool Variable to either return the data as GeoDataFrame or as list of LineStrings. - Options include: ``True`` or ``False``, default set to ``True`` + Options include: ``True`` or ``False``, default set to ``True``. Returns ------- polygons : Union[List[shapely.geometry.polygon.Polygon], gpd.geodataframe.GeoDataFrame] - Triangular Shapely Polygons representing the faces of the mesh + Triangular Shapely Polygons representing the faces of the mesh. .. versionadded:: 1.0.x @@ -8763,13 +8763,14 @@ def create_polygons_from_faces( >>> # Create polygons from mesh faces >>> polygons = gg.vector.create_polygons_from_faces(mesh=mesh) >>> polygons - geometry - 0 POLYGON Z ((297077.414 5677487.262 -838.496, 2... - 1 POLYGON Z ((298031.070 5678779.547 -648.688, 2... - 2 POLYGON Z ((297437.539 5676992.094 -816.608, 2... - 3 POLYGON Z ((298031.070 5678779.547 -648.688, 2... - 4 POLYGON Z ((295827.680 5680951.574 -825.328, 2... - + | Index | Geometry | + +-------+-----------------------------------------------------+ + | 0 | POLYGON Z ((297077.414 5677487.262 -838.496, 2...)) | + | 1 | POLYGON Z ((298031.070 5678779.547 -648.688, 2...)) | + | 2 | POLYGON Z ((297437.539 5676992.094 -816.608, 2...)) | + | 3 | POLYGON Z ((298031.070 5678779.547 -648.688, 2...)) | + | 4 | POLYGON Z ((295827.680 5680951.574 -825.328, 2...)) | + +-------+-----------------------------------------------------+ """ # Checking that the input mesh is a PyVista PolyData dataset if not isinstance(mesh, pv.core.pointset.PolyData): @@ -8811,19 +8812,19 @@ def unify_polygons( Parameters ---------- polygons : Union[List[shapely.geometry.polygon.Polygon], gpd.geodataframe.GeoDataFrame] - Triangular Shapely Polygons representing the faces of the mesh + Triangular Shapely Polygons representing the faces of the mesh. crs : Union[str, pyproj.crs.crs.CRS] - Name of the CRS provided to reproject coordinates of the GeoDataFrame, e.g. ``crs='EPSG:4647'`` + Name of the CRS provided to reproject coordinates of the GeoDataFrame, e.g. ``crs='EPSG:4647'``. return_gdf : bool Variable to either return the data as GeoDataFrame or as list of LineStrings. - Options include: ``True`` or ``False``, default set to ``True`` + Options include: ``True`` or ``False``, default set to ``True``. Returns ------- polygons_merged : Union[List[shapely.geometry.polygon.Polygon], gpd.geodataframe.GeoDataFrame] - Merged Shapely Polygons + Merged Shapely Polygons. .. versionadded:: 1.0.x @@ -8834,22 +8835,26 @@ def unify_polygons( >>> import geopandas as gpd >>> polygons = gpd.read_file(filename='file.shp') >>> polygons - geometry - 0 POLYGON Z ((297077.414 5677487.262 -838.496, 2... - 1 POLYGON Z ((298031.070 5678779.547 -648.688, 2... - 2 POLYGON Z ((297437.539 5676992.094 -816.608, 2... - 3 POLYGON Z ((298031.070 5678779.547 -648.688, 2... - 4 POLYGON Z ((295827.680 5680951.574 -825.328, 2... - + | Index | Geometry | + +-------+--------------------------------------------------------------------------+ + | 0 | POLYGON Z ((297077.414 5677487.262 -838.496, 298031.070 5678779.547..... | + | 1 | POLYGON Z ((298031.070 5678779.547 -648.688, 297437.539 5676992.094......| + | 2 | POLYGON Z ((297437.539 5676992.094 -816.608, 298031.070 5678779.547......| + | 3 | POLYGON Z ((298031.070 5678779.547 -648.688, 295827.680 5680951.574......| + | 4 | POLYGON Z ((295827.680 5680951.574 -825.328, 297077.414 5677487.262......| + +-------+--------------------------------------------------------------------------+ >>> # Merging polygons >>> polygons_merged = gg.vector.unify_polygons(polygons=polygons) >>> polygons_merged - geometry - 0 POLYGON Z ((396733.222 5714544.109 -186.252, 3... - 1 POLYGON Z ((390252.635 5712409.037 -543.142, 3... - 2 POLYGON Z ((391444.965 5710989.453 -516.000, 3... - 3 POLYGON Z ((388410.007 5710903.900 -85.654, 38... - 4 POLYGON Z ((384393.963 5714293.104 -614.106, 3... + +----+--------------------------------------------------+ + | | geometry | + +----+--------------------------------------------------+ + | 0 | POLYGON Z ((396733.222 5714544.109 -186.252, ... | + | 1 | POLYGON Z ((390252.635 5712409.037 -543.142, ... | + | 2 | POLYGON Z ((391444.965 5710989.453 -516.000, ... | + | 3 | POLYGON Z ((388410.007 5710903.900 -85.654, 3... | + | 4 | POLYGON Z ((384393.963 5714293.104 -614.106, ... | + +----+--------------------------------------------------+ """ # Checking that the polygons are of type list of a GeoDataFrame @@ -8915,19 +8920,17 @@ def unify_linestrings( Parameters ---------- linestrings : Union[List[shapely.geometry.linestring.LineString], gpd.geodataframe.GeoDataFrame] - LineStrings consisting of two vertices representing extracted contour lines - + LineStrings consisting of two vertices representing extracted contour lines. crs : Union[str, pyproj.crs.crs.CRS] - Name of the CRS provided to reproject coordinates of the GeoDataFrame, e.g. ``crs='EPSG:4647'`` - + Name of the CRS provided to reproject coordinates of the GeoDataFrame, e.g. ``crs='EPSG:4647'``. return_gdf : bool Variable to either return the data as GeoDataFrame or as list of LineStrings. - Options include: ``True`` or ``False``, default set to ``True`` + Options include: ``True`` or ``False``, default set to ``True``. Returns ------- linestrings_merged : Union[List[shapely.geometry.linestring.LineString], gpd.geodataframe.GeoDataFrame] - Merged Shapely LineStrings + Merged Shapely LineStrings. .. versionadded:: 1.0.x @@ -8938,22 +8941,28 @@ def unify_linestrings( >>> import geopandas as gpd >>> linestrings = gpd.read_file(filename='file.shp') >>> linestrings - geometry Z - 0 LINESTRING Z (32409587.930 5780538.824 -2350.0... -2350.00 - 1 LINESTRING Z (32407304.336 5777048.086 -2050.0... -2050.00 - 2 LINESTRING Z (32408748.977 5778005.047 -2200.0... -2200.00 - 3 LINESTRING Z (32403693.547 5786613.994 -2400.0... -2400.00 - 4 LINESTRING Z (32404738.664 5782672.480 -2350.0... -2350.00 + +----+--------------------------------------------------+----------+ + | | geometry | Z | + +----+--------------------------------------------------+----------+ + | 0 | LINESTRING Z (32409587.930 5780538.824 -2350.0) | -2350.00 | + | 1 | LINESTRING Z (32407304.336 5777048.086 -2050.0) | -2050.00 | + | 2 | LINESTRING Z (32408748.977 5778005.047 -2200.0) | -2200.00 | + | 3 | LINESTRING Z (32403693.547 5786613.994 -2400.0) | -2400.00 | + | 4 | LINESTRING Z (32404738.664 5782672.480 -2350.0) | -2350.00 | + +----+--------------------------------------------------+----------+ >>> # Merging linestrings >>> polygons_linestrings = gg.vector.unify_linestrings(linestrings=linestrings) >>> polygons_linestrings - geometry - 0 LINESTRING Z (32331825.641 5708789.973 -200.00... - 1 LINESTRING Z (32334315.359 5723032.766 -250.00... - 2 LINESTRING Z (32332516.312 5722028.768 -250.00... - 3 LINESTRING Z (32332712.750 5721717.561 -250.00... - 4 LINESTRING Z (32332516.312 5722028.768 -250.00... + +----+--------------------------------------------------+ + | | geometry | + +----+--------------------------------------------------+ + | 0 | LINESTRING Z (32331825.641 5708789.973 -200.00) | + | 1 | LINESTRING Z (32334315.359 5723032.766 -250.00) | + | 2 | LINESTRING Z (32332516.312 5722028.768 -250.00) | + | 3 | LINESTRING Z (32332712.750 5721717.561 -250.00) | + | 4 | LINESTRING Z (32332516.312 5722028.768 -250.00) | + +----+--------------------------------------------------+ """ # Checking that the linestrings are of type list of a GeoDataFrame @@ -9016,24 +9025,23 @@ def create_hexagon(center: shapely.geometry.Point, radius: Union[int, float]): Parameters ---------- center: shapely.geometry.Point - Shapely Point representing the center of the hexagon - + Shapely Point representing the center of the hexagon. radius: int, float - Radius of the hexagon + Radius of the hexagon. Returns ------- geometry.Polygon(hex_coords): shapely.geometry.Polygon - Shapley Polygon in the shape of a hexagon + Shapley Polygon in the shape of a hexagon. .. versionadded:: 1.0.x .. versionchanged:: 1.1.3 - Optimized creation of hexagon + Optimized creation of hexagon. See Also -------- - create_hexagon_grid : Creating a hexagon grid + create_hexagon_grid : Create a hexagon grid. """ # Checking that the center point is provided as Shapely Point @@ -9063,19 +9071,17 @@ def create_hexagon_grid( Parameters ---------- gdf: gpd.GeoDataFrame - GeoDataFrame containing the polygons for which a hexagon grid is created - + GeoDataFrame containing the polygons for which a hexagon grid is created. radius: int, float - Radius of the hexagon - + Radius of the hexagon. crop_gdf: bool - Boolean to define if the resulting GeoDataFrame should be cropped to the extend of the provided GeoDataFrame - Options include: ``True`` or ``False``, default set to ``True`` + Boolean to define if the resulting GeoDataFrame should be cropped to the extent of the provided GeoDataFrame. + Options include: ``True`` or ``False``, default set to ``True``. Returns ------- hex_gdf: gpd.GeoDataFrame - GeoDataFrame containing the hexagon grid + GeoDataFrame containing the hexagon grid. .. versionadded:: 1.0.x @@ -9084,7 +9090,7 @@ def create_hexagon_grid( See Also -------- - create_hexagon : Creating one hexagon based on a given center and radius + create_hexagon : Create one hexagon based on a given center and radius. """ # Checking that the gdf is of type GeoDataFrame @@ -9161,12 +9167,12 @@ def create_voronoi_polygons( Parameters ---------- gdf: gpd.geodataframe.GeoDataFrame - GeoDataFrame containing the Shapely Points + GeoDataFrame containing the Shapely Points. Returns ------- gdf_polygons: gpd.geodataframe.GeoDataFrame - GeoDataFrame containing the valid Voronoi Polygons + GeoDataFrame containing the valid Voronoi Polygons. .. versionadded:: 1.1 @@ -9180,7 +9186,7 @@ def create_voronoi_polygons( Note ---- - (https://docs.scipy.org/doc/scipy/reference/generated/scipy.spatial.Voronoi.html#scipy.spatial.Voronoi) + https://docs.scipy.org/doc/scipy/reference/generated/scipy.spatial.Voronoi.html#scipy.spatial.Voronoi """ # Checking that the gdf is of type GeoDataFrame