diff --git a/README.md b/README.md
index 448972e3..c12e25b3 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
[](https://www.python.org/downloads/)
@@ -40,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.
@@ -55,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.: 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,
+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.
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/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": "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",
"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": "iVBORw0KGgoAAAANSUhEUgAAAj4AAAEjCAYAAAAykgt0AAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjYuMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8o6BhiAAAACXBIWXMAAA9hAAAPYQGoP6dpAABhMklEQVR4nO3de3wU5b0H/s/sNRc2gQDJJhBywXAJ4Rpu4SYKCVfRH+1PWxW1UqVFrRQ8tqm1BKmkcn6i52BrlSIgaPH0qNXWiAlH5WIChABCSAJoblwSAiTkQpLdze78/oi7ZJNsdmZ3Zmdm9/t+vXxJNs/OPk925pnvPFeGZVkWhBBCCCEBQCV1BgghhBBCfIUCH0IIIYQEDAp8CCGEEBIwKPAhhBBCSMCgwIcQQgghAYMCH0IIIYQEDAp8CCGEEBIwNFJnQCw2mw1XrlyBwWAAwzBSZ4eQgMSyLJqbmxETEwOVShnPWVR3ECItsesNvw18rly5gtjYWKmzQQgBcPHiRQwdOlTqbHBCdQch8iBWveG3gY/BYADQ+YcLCwtzmc5isSA3NxcZGRnQarW+yp6oqEzK4G9l6q08TU1NiI2NdVyPSkB1h/+Uyd/KAwRGmcSuN/w28LE3UYeFhbmtvEJCQhAWFuZXJxGVSf78rUx9lUdJXUZUd/hPmfytPEBglUmsekMZne6EEEIIIQKgwIcQQgghAYMCH0IIIYQEDAp8CCGEEBIw/HZwMyGEiMVqY3Gsoh4AcKyiHtPviIRapZwB3HyYO2zYXVCJqvpWxEWEYEVaPHQaZT0zt5uteOmzMlTeaEX8wBD8bnEygnVqqbMVUOzXTF1zOyINQZiaECHZNUOBDyGE8LCvuAYb/lWC+pY2bJ4KPL6rEBH9grH+nmQsTImWOnuCys4pwbZDFbCxt197OacUT8xOQObiZOkyxtPkTfthsnbeZA9dAHYfqUZ6ciS2PTJF4pwFBvs1U9PY7ngtOjxIsmtGWWE7IYRIaF9xDX6554RTBQ4AtY3t+OWeE9hXXCNRzoSXnVOCtw46Bz0AYGOBtw5WIDunRJqM8fCrv59w+bu8kjo88W6hD3MTmOR4zVDgQwghHFhtLDb8qwRsL7+zv7bhXyWwdo8UFMjcYcO2QxV9ptl2qALmDpuPcsRfm9mKL89d6zNNXkkd2sxWH+Uo8Mj1mqGurh+UHPoSKgUtstYXG9t5EvUoEwuUjhiPDhX3r53v6ejJ6ctyeZe1A2EA9tbeQGL9VRiuSfxkLcCpYv+ezh7Y7+G5J935OmzMOPQ3+le3jjvHKup7PLV2xQKoaWzHsYp6pA0f6LuMieCDwuoeLT3d2Vhgd0ElVs5O9E2meNrEsUVqU04JNt43VuTcBCau10xRVYPvMgUKfBz+b/tfwXZYpM6GIBiNFsPvf6xHmfqNS8V6JlLCnHkuiLXhvwC8eOEKhptacM97b8NqMUudLa/Yv6cvd7ytuHNv8a/+I+ACn7pm1xW4J+nkrKq+jWO6VpFz4rnKG9zyVn6tReScBC6u18L1FpPIOXFGXV0BglGp8I+Jd0mdDUGcDQqD6a7FUmeDBJhIQ5Cg6eTss9OXOaWLiwgROSeeix/ILW/ffF+viPFKSsT1WhjUTy9yTpxR4BMgNNPvRHFwf6mzIZg/x09Avxhl7PZN/MPUhAhEhwe57GBk0DlTZWpChC+zJaj9pVcBAM0m9+NeVAywIi1e5Bx57nc8Zp0pZbC20nC9ZlLjBvgyWxT4BAJtUDDeGTVD6mwIyqzS4NDc/0fqbJAAolYxWH9P5820e0Vu/3n9PcmKXc/HamPxp8/LOKd/YnaCrNfzCdapcffIwZzTy32wthLJ9ZqR71lLBNMyKwO1umCpsyG4/WFRCJp+p9TZIAFkYUo03nx4Eozhzk34xvAgvPnwJEWv45P/3XXUNrkfk6FigFVzlLGOz3//dBLntPbB2kRYcrxmaHCznwseEIE/x0+QOhuieStlDp48exKm5iaps0ICxMKUaKQnG3HkuzpcLz2Cdx6doviVm7NzSvD2wQpwWcz4P380Dj+aHCt+pgT0/6YOxZ5j7sctyXmwtpLZrxm5rNxMLT5+rmr2YrRqtFJnQzR1Gj0uzbtP6myQAKNWMY6xPFJW4EKwL1TIdSmKmAHyHdDsSuKgUE7p5DxYW+nUKgZpwwfi3glDkDZ8oKTXDAU+fqxfzFC8axwhdTZEtzMqCYak0VJngxDF4bJQoZ2SB28/MGUY3N1n5T5YmwiHAh8/djItHawqAL5ihsHHM5ZApaaeW0L42F1Q6Xahwq6UOnhbp1HhidkJfaaR+2BtIhz6lv3Y5/0DZ7r3yeD+YOdkSJ0NQhSF65iWEJ1a8YO3MxcnY9WchB4tP0oarE2EwTvwOXjwIO655x7ExMSAYRj885//dPo9y7LIyspCTEwMgoODMXfuXJw9e9YpjclkwjPPPINBgwYhNDQUy5Ytw6VLl5zSNDQ0YMWKFQgPD0d4eDhWrFiBmzdv8i4gCRx/uWMKQgYrc2Vqf/fNN99QvSFDXMe0/Hr+CEUHPXaZi5NRtnERXlwyGo+kxeHFJaNRtnERBT0Bhnfgc+vWLYwfPx5vvPFGr7/fvHkztmzZgjfeeAOFhYUwGo1IT09Hc3OzI82aNWvw8ccfY+/evTh8+DBaWlqwdOlSWK23F8168MEHcerUKezbtw/79u3DqVOnsGLFCg+KGHiCByivD14ILWotvr3rPqmzQXrR2tpK9YYMrUiL5zT25dEZ8T7Jjy/oNCqsnJ2Il+5NwcrZiU7dW+YOG7YfKscfPinG9kPltK6Pn+I9KGLRokVYtGhRr79jWRavv/46XnjhBSxfvhwAsGvXLkRFReH999/HqlWr0NjYiO3bt2P37t2YP38+AGDPnj2IjY3F/v37sWDBApSWlmLfvn04cuQIpk2bBgDYtm0b0tLScO7cOYwcOdLT8gaE5kn+tVghH/+MGIYNE6ag5VSh1FkhXaSnp+NHP/pRr7+jekNaUxMicKS83uXvA2XsS3ZOCbYdqnAa8/RyTimemE3dYP5G0NGgFRUVqK2tRUbG7bEWer0ed955J/Lz87Fq1SoUFRXBYrE4pYmJiUFKSgry8/OxYMECFBQUIDw83FF5AcD06dMRHh6O/Pz8Xiswk8kEk+n2RmdNTZ3rulgsFlgsrjeAtP+O8ZMp3yqNBp8MGYGR7fXQs/7ztGIvC5cy/X3C3fjRd2WwtMt7s0j7OafEc89qY3tcV/afu77e17VnJ2W9AXhfd3ApoxxtyS3DzoIq2FhA/8P6PXoV6/i/igEeS4vD2vQkxZaR63e0JbcMO/OroO0lvtv5TTlUrBVrM0aJkUXelH7e9aZ7mcQum6CBT21tLQAgKirK6fWoqChUVVU50uh0OgwYMKBHGvv7a2trERnZc6xGZGSkI0132dnZ2LBhQ4/Xc3NzERLivh87cflDbtMoxbr2zqe3zS3cNhpUEs5lWvYTcTMiICWee9/XN+L7nJxef5eXl+f4d2ur+8GzUtYbgPd1R9fyKskoAH+a0vvvNk7+4QGjoxw5OeU+y5NY3H1HowBsntpHAhn+HZR63vXFXiYu9YY3RJn/yzDOncYsy/Z4rbvuaXpL39dxMjMzsXbtWsfPTU1NiI2NRUZGBsLCwlx+rsViQV5eHso/eg9sh/Ij6MIHVuNE6ABsbrmM5/sNgYnxjyZqPWvjVSa1zYrf5u1B86VqH+TOM4xGi8TlDyny3Mv4xbMYOX2m02v2ayk9PR1abWcrlr31hAsp6g3A+7qja3mVwNxhw+SX83qdxq5Xsdg42Yb1RSp8k5mh+C4uLt/R7oJKvPLFObfH+s2CkbJY50ep511fupeJT73hCUEDH6PRCKDzySs6+vYMgLq6OsfTnNFohNlsRkNDg9PTW11dHWbMmOFIc/Xq1R7Hv3btWo+nQju9Xg+9vufW9lqtltPJwXZYFHfz6c4Qfwe+MgxG0A/dQSZGhXY/CXzsOJdJrULurKWY/t5/AyyPhUokoMRzT61iXF5XXa85LteelPUG4H3dwTWdXLx7pBxtHX0HlG1WBnuPX8bK2Yk+ypW4+vqOKhtMMFndr01U2WCS1festPOOC3uZxC6XoHfFhIQEGI1GpyY4s9mMAwcOOCqn1NRUaLVapzQ1NTUoLi52pElLS0NjYyOOHTvmSHP06FE0NjY60pCeyiekSZ0FWTnUbzB0aXdJnQ3iBtUbvsV17Z5A2beK65R+2s7Cf/Bu8WlpacF3333n+LmiogKnTp1CREQEhg0bhjVr1mDTpk1ISkpCUlISNm3ahJCQEDz44IMAgPDwcKxcuRLr1q3DwIEDERERgeeeew5jx451zNYYPXo0Fi5ciCeeeAJvvfUWAODJJ5/E0qVLaWaGC7qQUPxP5HCpsyE7bybPwlNnT6K9sUHqrAS0lpYWlJffHiNB9YZ05HKjN3fYsLugEuXXWlDXbEJkWBASB4ViRVq8T7vYVqTF4+Wc0j5XsKbtLPwL78Dn+PHjuOuu20/R9r7xRx99FDt37sTzzz+PtrY2rF69Gg0NDZg2bRpyc3NhMBgc73nttdeg0Whw//33o62tDfPmzcPOnTuhVt/eGvi9997Dr371K8csjmXLlrlcA4QAbGoaWlW0ZUN39Rodvpt3H4Z+tEPqrAS0kydPYunSpY6fqd6QjlQ3enugU1XfivNXm3G0vL7XjVF9PYXcvp3FWwdd71kWKFP6AwXvO+XcuXPB9jFmgmEYZGVlISsry2WaoKAgbN26FVu3bnWZJiIiAnv27OGbvYD1eeIEqbMgW38fnIiXksejueRbqbMSsGbPnk31hoy4W7vnsbQ4wW705g4bHnnnaJ+f15WNBd46WIHTFxuxa+U0nwQc9iCr+zo+Kga0jo8foiYCP2BIGo2Twf2lzoZ8MQzem7YQy78rRYfZLHVuCJFMb4v0dWVfxVmoNWuyc0rw9sGKXlt23CmoqMeI33+OMdEGLJ80VPQusMzFyViXMcrRKhUXEeLzbjfiGxT4+IHzY6e5TxTgzukNaL1rKXRffCR1VgiRRHZOSZ/dOWkJEfjbI5OwP3ef15/Ft5WnL2drmnH2s1KfdIHZt7Mg/o0CH4XTG8Lwv4MSJPnswVoN4qGG9qYZNy41Qx+iRejAIFwboMV3Jvm1rPwlbjx+P6QQLZcvSp0VQnzK3GHDtkOugx4AOFrpfZACuG9V8pS9CwwAdT0Rr1Dgo3CWSTPQrlK7TygQBkCqTo+Ws/WoKL+JM72kYQGMGz0IqqQwnOqyFYDULCo1vp57Hya/53qMCOFI5msjEWe7CyrdBiI2FvigsBoDPTi+feDyhycvoeRKs/s3eOHtgxWICNXjZzNpwDHxDAU+CvdpwniffdZIvQ4dJ6+j+GJnxeZqyS8GwPnS60DpdYwfMwi3hhtk0wL0lSEKs6bPRfuRr6XOCiE+w3VNnuqGNt6Bj1gtPK6wALI/L8Mr+8po4DHxCIXLCmYYPRalQQb3Cb2kYYCZHRpU/7sCly/ye5o7d/Y6Ln5agelNQLhaHqfbmymzERQWLnU2CPEZrmvyDBsQzOu49nFDvgp6urJ3fWXnlPj+w4miyeNORDxSnCL+oOZkvQ6JZS0o+r8qeLrZOwPgVMFlhB65jgm6nlsD+Np1jR6V8+6TOhuE+MyKtHjHjC1XVAzwwJRhnI/Z0t7R52BpX3n7YAVa2jukzgZREAp8FCq4fwQ+HBAr2vGjtBpMbbDh+08rUF3ZKMgx6+vbUPqvcqQ1A6ESt/7sjrwDhlFjJc0DIb5iX6SvL3wW6cvOKcHYrC+8zldaQgQemhqLOJ4tTV2xAFKyvvBpy4+5w4bth8rxh0+Ksf1QOcwdHj4VEknQGB+Fak2dCatIg5pnQIuzeRdx2mR1OY7HUwyAk/mXEWUMhXFqJL6XauwPw+CD6Ytwz/dlsFqUtUEoIZ7gskifhcO14G5aPBdpCRE9Fif0dqyQr2Z89ZZPX682TbxDgY8CMYwKHw5LEfy4oSoVRtdZcKLosuDH7u5q7S1o91VhWnocjnZIE/ycDQpDxl1LoMn9pySfT4ivebNIX5vZij/+6yzeK/R8OYjeAp7e8ubp7DCxZ3y5Cvpoqr2yUOCjQP3GTkS5PlTQYw7TaaE9dcMxY8sXLGYbvv2sAmmzhuJYKAurzz75tj/HT8AL0YVoqRE/2CNEDjxZpO+JdwuRV1Ln8Wdy3frBnreVsxNh7rBh5+EKbNpXxvlzus/4ei49yeM8d8dlLaRthyqwLmMUTbOXOfp2FKho9BRBjzdSr0PrV5d5z9gSysnDlzCxrgM6RuiONffMKg3y597n888lRCm8CXrGRBvw4pLRKNu4iHdLiE6jwpNzh2PVHP4LtNpbYLbkcg+a3OG6FtLugkrBPpOIgwIfhQkZFIlP+scIdrxxej2u5l5ES4u041yKT17FqKo29JNg0PMX4dEwjJng888lRO7azFaPgh4GwKo5Cfjs2TlYOTvRqxaQzMXJWDUnwaPxhjsLqjz+3O64roXENR2RDgU+CnMzdSbACPO1per0KM+pQLtMpoKeL72BmJImRGh8txK13RcT5/r8MwmRu00ezJRiAJzJWiDoWJfMxck4k7WA9/uEXF+I61pIXNMR6VDgoyAqtQb/GCpMZTJBp8fZf5ejo0NeWw9UVzYisrQJWh93e33TbxD6jRe2C5EQpau8wb/14sk5CegXJPzw0X5BGo+6vYTCdS2kFWnxPskP8RwFPgoSPH4yrmg9X+/CbrBWgysHLnWOBJShyvKbmNru+1Pzk/FzAAnGGREiR+YOG1pN3FuDVUxn95aYs5rs3V7uApDuhBjrI/RaSEQ69A0pSMHIVK+PwQCILr+FpiZ57J3lyvGvqzFe79tVnk+EDEDopDSffiYhcrQltwyjXvwcRdU3OaX/zcKRHg1g9kTm4mSUbVyE3y0cxfk97+RXCbLAoavAyxdBHxEOTWdXiH7RQ/BFmNHr48xgtSgqkf/UbQZA7cHLGDQzCtc7fDfR/X/HzsTik0fA2mglVhK43smvgo3l1qySnhyJX869Q+QcObPP+LrRauK8mOLbByvwzN0jvO6G82YtJCIPFPgoRN3EGV53w4zW63Di39LvrcNV400TRlW14cYQnc8+szgoHMunzELb0YM++0xC5ILv1gvpyZHY9oh0Y+PsLSxvH6xw23Nv39pCiJYZV2shmTtsFBApAAU+CqDW6vBBDPdm3d70U6vQUnDV441GpVJWfA0zjHEoUvmua+7vyTOw/Hg+bFZ5zHYjxFfufysfT3IYP5w6rD/2/Hw6gnW+n4HZXebiZLSYrHjvaDWn9GKtsExbWSgHhaIKoJs4Fdc13o13GdfE4to1Za4vcXx/FcbofNfqc0HfD5rpd/rs8wgRijebZ7aZrfju2i1OaccMCZdF0GOXOIjfSvZC7+hu38qi+/R5+0KKvtxAlbhHgY8CfD3Cu0HNyXo9ig5fEig3vscAqDt8xaefuXvUNKi1vgu2CPFWdk4JRr34OTZ+Vop3C6qw8bNSjHrxc843XT5r9shtrRouU827EnJHd65bWdAO7vJBgY/MGYYl4GDoII/fr2MYtBddE3yXdV9rau7s6vLVthbV2hAg7S6ffBYh3hKixeHLMu4rNMttrRouU817I0RrDG1loTwU+MjcxfFpXg1qnmzV4MplafbgEsO4m757atqRNAW6EHk92RLSnRAtDtk5Jbh8s53T5yUbDbIcsOvp1hbetsbQVhbceNMNKzT5nb3EQRsUjP8xer67sFGrwZkD3Ab8KcWZ4zWYCa1PPuuqNggtsxf65LMI8ZS3LQ5cAqeuPlw9k0fufMuTrS28bY2hrSzc87YbVmgU+MgYMykNjWrPb/JRl9pgMftfv/LxLyox2keLG26LG4fgiIE++SxCPOFti8OOb3p2kbmSnhwpq0HNvfFkawtvWmNoK4u+yXHgNwU+MpaTNMmr95cVXxMoJ/LCALB+ewO+qH5bVRpcnL3IB59EiGe8aXHIzinBnz7ntp3D0AHBkq7Zwwffbq+h4Z5vBURbWbgm14HfgfdNKETYyBScDO7v0XtD1f7/tVZXNWIq45tZV7uiRqBfTKxPPosQvjxtcbA/iXPdsu9nM+LdppGTzMXJOJI5j1Pa7H1lXrU80FYWvePaDftBoW+HZNAChjJ1Ztx0j9+b0iLT3UcFdu7gJQy6K1r0LS1sKhVOzVyEO/7xtqifoyTmYH7rphDx2Fsc+tq6oXuLA99xPUrtqgnVc7vFsfB+YUPayqInrl2I1Q1t8OWAgsD9RmQsZOAgfDjAsxaGkXodThXIfy8uIbS2WjD8um9WV/44IhaGJO9Wz/YXQeH9UR7Nf+owEQ/fFgcuT+JdKb2r5vEZcZzW+fG228W+lcVL96Zg5exERf/NhMC1G3bYAM+7Gj0R2N+KTN2cPBtWFf8RLBoGwOl6cG679gOnjl5Bii8GOjMM9k/lN1vEX1kmTIdNRVWH3Nh3LX9xyWg8khaHF5eMdrljOp/BvP7QVbM2YxR+w2E3d1pvR1gPTBnmNo2K4ZZOSILXXh0dHfj973+PhIQEBAcHIzExES+99BJsXXa7ZlkWWVlZiImJQXBwMObOnYuzZ886HcdkMuGZZ57BoEGDEBoaimXLluHSJeWuPsyVWqvFB0PHePTeaTYtqiobBc6RvDEAWouu+WRhw4OGwQidoIzBnWLKixsjeGxN9YYwuLQ4mDtsqLnZxul4v1s4SvFBj91ljmUuv9Yick4CQ3ZOCcZt+MJtOilaEwX/tFdeeQV//etf8cYbb6C0tBSbN2/Gf/7nf2Lr1q2ONJs3b8aWLVvwxhtvoLCwEEajEenp6Whuvr3Q3po1a/Dxxx9j7969OHz4MFpaWrB06VJYreKO55CabuI01GqDeL8vTqfFt19WiZAj+btyuRlTOnwzxfZ/J8wFE8CtHYa44SgKGSD4cane8A37eip5pe5XaVYxwGOz/KdLk2u3y3vHLtLeWl5yNYW9KykHfgtegxcUFODee+/FkiVLEB8fjx//+MfIyMjA8ePHAXQ+tb3++ut44YUXsHz5cqSkpGDXrl1obW3F+++/DwBobGzE9u3b8eqrr2L+/PmYOHEi9uzZgzNnzmD//v1CZ1lW/m/EZN7vYQAYzjehoyOA+ri6+fbrasTpxF/YsDgoHLoA3sD0ylhxWryo3hAfl5tRV0of19Mdn/28aGNRz3EZOM8AOL1+gWStiYLP6po1axb++te/4vz58xgxYgS+/fZbHD58GK+//joAoKKiArW1tcjIyHC8R6/X484770R+fj5WrVqFoqIiWCwWpzQxMTFISUlBfn4+FizoOdbCZDLBZDI5fm5qagIAWCwWWCwWl/m1/47R+GY14L4YEoajKDQCQSy/wXVTVVp8+90N6H9o9NCrWKf/+wO3ZWJZDLzQiKvD+LeW8fXeqOn46bfH0WHitsS/K/ZzTg7nHhcqjQY5kYmd52dHR4/ryv5z19f7uva6kqreALyvO7iWUUrmDhvezS931BGu2K+vn6fFYk16kiLK1peu35FWC6yaNQzv5HNrGX83vxy/umu44MGfucOGDwqrUd3QhmEDgvHAlGG8PkPu592egkpoOdx7/udYhWOmYPcyiV02wQOf3/zmN2hsbMSoUaOgVqthtVrx8ssv46c//SkAoLa2FgAQFRXl9L6oqChUVVU50uh0OgwYMKBHGvv7u8vOzsaGDRt6vJ6bm4sQDvstJS5/yH3hfOC/mi969L6HpvZ8beNk/1u1ue8yXQN8tS3ZvT8R7FByOfe4eKH9GtAOoOgiclykycvLc/y7tZXbIFqp6g3A+7qja3nl7E88GutGWCuRk1MpWl58zf4djQKwuZe60pX9uftEyc/AH/5DA7A/17OWJbmedwPB8W/cUIKcbq1q9jJxrTc8JXjg88EHH2DPnj14//33MWbMGJw6dQpr1qxBTEwMHn30UUc6pttgVJZle7zWXV9pMjMzsXbtWsfPTU1NiI2NRUZGBsLCwlwe02KxIC8vD+UfvQe2Q7oIOigsDP/1o6dh4jmbK7XeiuKTV51e06tYbJxsw4vHVTDZlL4veyeuZQoN1QKzjKgXeW2fMKsFv/joL2j/oXXAE4xGi8TlD0l+7nFV9uOfIzc8GgDwnyOG4r4o5wDDfi2lp6dDq+1sxWri+PeRqt4AvK87upZXrl76dwn+57j7h6qHpg7FeFQpokxcuPqOuP49gM6p8GszvF/KYktuWZ+tTVw/R+7n3e6CSrzyxTm36X6zYKRTi0/XMnGtNzwleODzH//xH/jtb3+Ln/yk84l47NixqKqqQnZ2Nh599FEYjUYAnU9n0dHRjvfV1dU5nuaMRiPMZjMaGhqcnt7q6uowY8aMXj9Xr9dD38u0Zq1Wy+nkYDsskt582ibw35crRa/H8ePlYFwszG6yMTBZ/SPwsXNXJlNTB8Zdt+LKAHHHJrRr9KibkQ7Dv/d6fSypzz0uQiON+DQ8GmB++LtqNC6vq67XHNeKWap6A/C+7uCaTirZOSXYc/QSWA4bOAwdEAo0yL9MfHUvT/wgA+e68c1D1bAxaq/Go5g7bHjrcDVsrOvPfOtwNX69YAznbi+5fkcPzxiOP35+3u3A5odnDIe2W1ntZRK7XILfHVpbW6HqNutFrVY7pqUmJCTAaDQ6NdOZzWYcOHDAUTmlpqZCq9U6pampqUFxcXGfFZhSMSoV/hE/ltd71ABM317nvBdNIPn2WA3G+2Btn+0xoxEaaRT9c+SgYeKM20EPhF8qiuoNcfDZlkKK9VSkwmegM+D9woZct27whzWElLB3meAtPvfccw9efvllDBs2DGPGjMHJkyexZcsWPP744wA6m6rXrFmDTZs2ISkpCUlJSdi0aRNCQkLw4IMPAgDCw8OxcuVKrFu3DgMHDkRERASee+45jB07FvPnzxc6y5ILHT8ZlTp+WwBMVelwskrc5kClYgA0HL2KoEkD0M5neVqeLCo1zs9ajCEfvSPaZ8iBSq3BP4aOFvUzqN4QHt9tKaS+GfkSl20+urIHJStnJ3r0eVwXjPRml3g5sbeObTvkPItQxXSeZ1KvDSV44LN161a8+OKLWL16Nerq6hATE4NVq1bhD3/4gyPN888/j7a2NqxevRoNDQ2YNm0acnNzYTAYHGlee+01aDQa3H///Whra8O8efOwc+dOqNW+Wa/Fl74ZzWO0HYAwtQrnDwbOomyeqLt6C5PaBiJfL+5Yn/cHx+OlYQloruZ+g1GakHGTcEUr7pLyVG8Ij+u2FAyAJ39YT0WuM4XEYL/5vs2xRcybhQ25riHENZ1cmTtsTnuVnV6/AB8UVstu7zLBAx+DwYDXX3/dMQ21NwzDICsrC1lZWS7TBAUFYevWrU4LmPmjfkOHIdcQ5T5hFylWNU60BE4F5amir6uRsiwRxV2mKguOUeF42gKMrP6reJ8hsWMjU3u8xgrckEb1Bjfdbyx93Ui4th48ODVW8idwqWQuTkZEqB7Zn5e5TfvesYvoF6Tx6G+1Ii0eL+eUuh33osSNYO2yc0p6tPC8nFOKJ2Yn4KV7U6TLWC+kD70C3KWJswAe2y0wAC6fviZehvwIA+Bmfi0ManFP80/7D4EhSdyuIKmERhrxWXi0+4REdPZVlzd+Vop3C6qw8bNSjHrxc5cL7V2o5ba2Q+LgfkJmU3F+NrPn5q6ueLqwoRLGvXjD1eKYNlaei0Eq86/sJ3QhodhrTOL1nvF6Pa7W3BIpR/7n+vVWjBZ7B3eGwZdT08X9DIl0H9RMpMH3xpKdU4KCinq3x1V6K4MQuAQlXXk60DlzcTJWzekZZKkY4Ocz4xFpCMIfPinG9kPlXg2k9jUuY8m8HRwuNMG7ugh3tskz0cxzCruqijbQ4+v0sRpMWZqIQot4XV5fGyJx97hUNJ8uEu0zfE2lVuN/RR7UTNzjemNZlzEKOo2K16BmJbcyCInPeB9vBjpnLk7GuoxRTt2VVxrb8M43lb12ESmhC5LPjDVPB4cLjc54CX2aOIFX+iE6Lc5+635zQdJT5YFLCBe5y+uTCXN5dVvKXeiYCbjsYlAzK/iEduIK36nQXAc1T0+MUMSN1VcyFyfjwWncpvN7M/tKp1Fh5exEvHRvCuqa27H9cM/vS65dRL1R4ow1CnwkYkgajbNBrleF7U2cGbRuj4eam80YK3IPYVHIAIRM9p/1Ys6OmCB1Fgj431i4ph8RZXCfKMAkDuK2rAjLsrB6uVSGEruIeqPEGWsU+EikIrnnTBl3mipp3R5vnDh8CfF6cVcE/XvKLKjUyu9B1oX2wycRgbGYndzxvbEo8UYkF1wXNtx9pBqzXvkS+4prPP4sf1nUkMvfTG5jySjwkYBGH4QPI4fzes8AjRrfnXc/WJG4ZrOyGFwlbnPreb0B2mmzRf0MX2DGT0V7H/vGUUeX7/C9sdTcbHN7TLndiOSCz0Dn2sZ2/HLPCY+DHyV2EfVGiTPW5JOTAKIdPxlNPAc1JzEautsI4My3dUgNChL1M/aOnKb4Vp/DifJadyOQ8bmxZOeU4G/fVLo9ptxuRHLiavZVd/bqeMO/Sjzq9vKXljlzhw2RhiAkx/TsOlUxwKo58hukTWe+BI4OH8//TbXyjvqVpPnENWhFHIT8nb4fdFNmiXZ8sYVGRePr0MFSZ4N00ddUaPuNhetsrpWz4mV3I5KbzMXJKNu4CCum993dywKoaWzHMQ5LB3SnxC6i7rquLVVypXPdKAbAmGgDXlwyGmUbF8nyXKPAx8dCBkfiizB+KzVrGODC2esi5SjwVF9sQppKJ+pnfDB6GlQK3SahaexUt7PTqPHR9+w34xeXjMYjaXE9bixcZ3PFhIu7/Yi/0GlUmBwfwSltXXO7R8dXWhdRV67WlmIBnK1pRl1zu2zzLs9c+bHm8dN5LwiXrNOjrU3kRfgCTOnBS4jQiBeYnNcboJs8U7Tji+mz2FFSZ4G40HUq9MrZiU43Fn8ZMyInkQZu3eJ5JVc9Oj6Xljw5UvqMNGUPRFCgf8fyXxAu5JqIe00FqOYWMyY32XBYxO7zfySnYVnhYbA2eV78vTHcMQrn9DTNWYn8ZcyInExNiEB0eBBqG9v7bOX89+kaDOlf4lGg0tuihvbure2HymW3wSegzEULu6LAx4fCRo7BeZ43FR3D4NypWpFyFNiKDl/CHfck4DuTWZTjl+kNuH/yDLQdOyzK8cVQMkGZrVT+iM+GpEDnWJA/flba5w1a7mNG5EatYrD+nmT8Ys8Jt2m7rp7Nl70lz66vDT+fS+e3zZEYlN66SIGPD30/mv/aPWO1Opylbi5RsCwQcqEZGKYX7TM+TJ6BJcfzFdHq0y8mFnsHxnFLTIN8RNXXjc9Vq8Krue53GJfzmBG5WpgSjR9PGoL/PXG5z3RCtXDYx870dvy3DlZAxVohdWe00lsX6QrwEW1QMD4czH0jPDvmijwjZn9xvvQ6JurEC3zOBoUhODVNtOML6cL0ebQhqQx4stO1/T19xaNyHjMidyF6bm0E5de820uRy9iZnQVVXn2Gt8wdNlit7p985Ny6SLWcj2jGT+G9IWmYWoWybz0bNEe4azp1TdQL4aMxM8DIPKAINcbg/UHy64sPNJ4MGuXyHhUDrMuQup1Aubi2XLx37KJX+2txHTsjFfv09U37lN26KM9c+aF8D9buSWa06OigPgWxXb7YjClq8Vp9zgSFI3TiFNGOL4TK6fNhU3GvDuisFIcn2xj4y9YHcsZ1KwvAu81F5TomBnDdEtmd3GekART4+ES/IbHYb4jk/T5TdbMIuSG9uXikBkFcazYPfD5GvpuXhkYasZvnFipEHJ4MGlX6QFMl4LOVBeD5VG65jonh0qrIAMhcNEq2ixZ2RYGPD1yYNs/tgnDd9VOrcL6EFi30lfr6NkyyijfW/0joQBjGTBDt+N64OH0+rH3sy0V8x5NBo0ofaKoU9jV3uNTkNhbYedj9KtrdcWlZEu/xzDUurYosAI2KkW33Vlfyz6HChUYa8fdB/Ac1j1ZpOQ0gI8IpPXQJg7XiBT8Hx8tv89Lg/gPwrpH/9Fg6M8XhyTYGK9Li3d4M5TzQVEkyFyfjwWl9b2Nht2lfGe8uLy4tS/ZrbwuHWXxC8bdWRQp8RHZp+jyPnqaZOvc7LBNhtbV1IKFOnDV9AGB/WBTC7pDXAFPL2CmwUGuPbHiyjQFNY/etxEGhnNN6Mt6H6yap7+RXeTWQmg9/a1WkK0FEwQMisMc4gvf7tAyD74qpm0sK3x6rQYpevIHOx1PvFO3Ynvgmjv9K4kRcfLYxoGnsvsdnoDPg2XifzMXJOL1+gSjH9kTNTfcP4kpqVaTAR0Q30uah3YOn6dE6HVpbLSLkiLjDAGg7eQ0akTrSP+k/FIbYeHEOzlPo4CgcDB0kdTZIL9xtSArQNHap8B3o7OmMug8Kq0U7Nh/ZOSX42zfuP0NJrYrKyKUC6Q1heHeIZ09ZoQ3idbcQ9y5fbMYURqTd2xkGZ6bPF+fYPLWmpPIedG/H0igf0fW1ISlA09ilxGegMwB8ePIS78+Qw7gaLsE1AKycFa+oVkUKfERya/pdvBcstKsquSFwbghf5QVXEMpjXRs+/hERB8PwkaIcm4+vPNgwl8iHHG6MgSxzcTJ+u4hba1rJlWbe43HkMK6GS3ANADHhwaLlQQwU+IhAGxyCd+PGefTekXod6m/QwGapNd40YYJFpMuDYfDV9Axxjs2RYfhIFAb3lzQPpPOJevuhcvzhk2JsP1TOa7yGHG6Mge5nM90PQrbjOx6H69T2Dhsr2jgffw2uKfARQcf0ubih8WyA7MAW+W9mGShKvrmMARpxZjx9ZYhCv5SJohzbLYbBvhmLPe7mAjo3eCXesS//v/GzUrxbUIWNn5Vi1Iufc24Z8GTqOxEWn/E+fLsduU5tz/68jNd5w8eQ/txacpQWXFPgIzCNToc9iZ7f0K6erxcwN8QbbW0dGG0W7xL5ZNwc0Y7dl6BpdyI/dKAkn006ebIRaW+mJkT0+XslDThVqszFyRgTbeCUlm/LCNep7XzPGy6yc0rwCoc9uZQYXNMVITBm6hxc0XrW3zlMp8XlS7RNhZxUFl0VbYbX2eAwcQ7cB11IKLanzPT555LbPNmItDt7a9GR8t4flJSwX5I/WT5pKKd0nrSM2Gf4/W6h+/FEQk1v57ovF6DM4FpZuZU5lVqDD0Z4vhnlUJrMJTv19W2Y5GG3JVcafZCox++q/q6luKJR1kBEf+PtbCx3N6W0hAhF7JfkT7iu7XOl0bPxmzqNCmq1+w8QYhYf15lcDJQbXFPgI6D2+cvwvY77qp7dNZY3CpgbIpTmsgZRj3/zriWiHt+u35BYvO3hEgvd0RAfz5Vfv8UpXW/dIlxuSkcrqbvc17iO9dl+uFL0ndvLr7V4dHw7rjO5MheOUmTQA1DgIxhD0mhsjZvg8fsHazX47oK4N1jimYrvGjBKL9K6PgDeiRkFQ5L4U8uLZyygzUgllp1TgvePul+YDui9W4TW7pGvzMXJ+PnMeLfpxN65/b1jF70a68M1wLrkYeuVHIgS+Fy+fBkPP/wwBg4ciJCQEEyYMAFFRUWO37Msi6ysLMTExCA4OBhz587F2bNnnY5hMpnwzDPPYNCgQQgNDcWyZctw6RL/RaB8QRcSil2z74PNi3VfEli1JLvuEm7CatrFOzijwt9nLoNGJ15w1W9oHP4RESfa8YXg7/UGl+0l7FwNGPXX6cX+IprDLChPA1M+W2V4M9DZX2dydSV44NPQ0ICZM2dCq9Xi888/R0lJCV599VX079/fkWbz5s3YsmUL3njjDRQWFsJoNCI9PR3NzbcH9q5ZswYff/wx9u7di8OHD6OlpQVLly6F1WoVOsteq1h4v1ddXACgbqQBPnJ2+ngNYnWeLUjJRWmQAY3z7xPv+NPmeTV9vTuhu7r8vd7gOm7CztWAUa43Ja7piLC4BpyerOTMd6sMT1qW/HkmV1caoQ/4yiuvIDY2Fjt27HC8Fh8f7/g3y7J4/fXX8cILL2D58uUAgF27diEqKgrvv/8+Vq1ahcbGRmzfvh27d+/G/Pmdy/vv2bMHsbGx2L9/PxYs6Ll5m8lkgslkcvzc1NQEALBYLLBYXO97Zf8do/HsphYycSo+GpyAINa7kfRNl1qgVwtzO9GrWKf/+wM5lCnuWjuuhQkXPOh/OGfs/39naDJeGJmC5u/PCfYZANAvegj+OXCY1+eoE2tHj+vK/nPX1/u69rqSqt4AvK87uJRxT0EltBzOXQbAz2bEYW16Uq/HVdmsnOoJlc3K+W/fFZ8yKYGvyxM/QM/p+/n+ahNe+ewM1vLcR+259CRobBbAWsmpLnz2vUL8f/dP4DTraktuGXbmV0HLoTnk8RlxYFgrLBZhHii6f09if18Mywq7FFlycjIWLFiAS5cu4cCBAxgyZAhWr16NJ554AgBQXl6O4cOH48SJE5g48fZ6N/feey/69++PXbt24csvv8S8efNQX1+PAQMGONKMHz8e9913HzZs2NDjc7Oysnp9/f3330dIiHKb5AhRstbWVjz44INobGxEWJjr6ftS1RsA1R2EyA3XesNTgrf4lJeX480338TatWvxu9/9DseOHcOvfvUr6PV6PPLII6itrQUAREVFOb0vKioKVVVVAIDa2lrodDqnysuexv7+7jIzM7F27VrHz01NTYiNjUVGRkaffziLxYK8vDyUf/Qe2A5+UaZm5t3YMsr7NVGmqrT4Nq/K6+PY6VUsNk624cXjKphs/jFySC5lGjslGscFavXRszZsbrmM5/sNgYm5/Zg1ur0ZSz/dDlOz92s6GWLjkD3/Ya/Gn/Vm/R0xeDjaeRFE+7WUnp4OrbazBdXeeuKOVPUG4H3d0bW8ruwuqMQrX7hvyfvNgpF9diEIdRxX+JRJCaQoz5bcMryTz60+9+R7speJb104OsqAZRNi8MCUYU4tQFtyy7Ajv4pT9/Vz80fgsVncu9u46v49ca03PCV44GOz2TB58mRs2rQJADBx4kScPXsWb775Jh555BFHOqbbeAOWZXu81l1fafR6PfT6nuutaLVaTic822HhFfhog4Lxzh1T0M54f0NputgGk1X4m7nJxohyXClJXaaiY7UYsDgONZYOwY5pYlRO59HJ4HCELX4Yaf/zV1gtno/96jd0GP5890/Qqhb8MgfUGpfXVddrjuvNRqp6A/C+7uCS7uEZw/HHz8/3OSNLxXSm0/bRLXGl0ez2/OdyHHe4ll0pfFme3ywZi4PfNeBsjfsHl/89VYPH5yR59DkWll9deOpKC05dOY8/fn4e0+IjkGQ04PzV5h8WweR2nItNZlH/jvbvSezvSvDBzdHR0UhOdp7bP3r0aFRXd07hNBqNANDjCayurs7xNGc0GmE2m9HQ0OAyjdTaZ2WgVuv9wnPhajXOlV4TIEfEF1gbkNAk/kDZA4ZIfH/fI2A8DKz7xcTirYUrBDlHfcHf6w0uA1PdrYCbnVOCv31T6fazlLiSrr/hupKzJ7u22z2W5tksTRsLFFTU492CKpcrf7ui5JlcXQl+dcycORPnzjk3xZ4/fx5xcZ1fUkJCAoxGI/Ly8hy/N5vNOHDgAGbMmAEASE1NhVardUpTU1OD4uJiRxopBRnC8LeECYIcaxSjgZBjTon4io9cEW3z0q7+MTAeX61YAwPPzUz7RQ/B24seUdQKzYFQb7jad4nL9hJcZ4WtnBWv2EXl/AmfqedvH/RsXZ+1GaM47eMlFKXP5OpK8DbwX//615gxYwY2bdqE+++/H8eOHcPbb7+Nt99+G0BnU/WaNWuwadMmJCUlISkpCZs2bUJISAgefPBBAEB4eDhWrlyJdevWYeDAgYiIiMBzzz2HsWPHOmZrSOnGnEVoUgvTFNdx2btVNonvmc02pLYz+EaEHqTuCkMiUDjrR0gfNxOzj+ai+fvzLtMG949A26QZeCtxvOhBj8BzIgKi3gA6g591GaOwu6ASVfWtiIsIwYq0eLctNFxX040JV06w68/sLXxvHXQfrLIAHnnnKPY+mcb7c+zn01PvFSGvtM6DnHLnTy2JglfdU6ZMwccff4zMzEy89NJLSEhIwOuvv46HHnrIkeb5559HW1sbVq9ejYaGBkybNg25ubkwGG7vcPvaa69Bo9Hg/vvvR1tbG+bNm4edO3dCrZZ25dmQQZHYKtCy/+FqNcrOXBHkWMS3zh+rQfCsSLRxuRsJIC/MiLz5KzA77TpGN17D4Bu1CKq7gvaaS9DH34HToyfj44hYxa7M7O/1Rlc6jQorZyfyeg8tXKg8mYuTcfjCdU5jfY6U18PcYfMosNBpVPjzQ6kY9eLnnIJjvhgATyp0Ty5XRHlmXbp0KZYuXery9wzDICsrC1lZWS7TBAUFYevWrdi6dasIOfRc1ayFaBfo5jIaapyy+s9aO4GkudmMidCiAD5ceJJhcKjfYBzqNxgQKPiWE3+rN8wdNt4tO64Ewmq6/mj5pKE4+1kpp7S7Cyp5B8R2fFqY+GAAnMlagH5BPmje9iH/Ko3I+g2JxauRdwh2vNYKcafsEXFdLqqDZlJ/dARg7BqAReYlO6cE2w4576D+ck4pnpjN/8nZfix3/GkMhr9YkRaPP35Wyul68XZzUft51f2888aTcxL8LugBaJNSXk7PWCjYeihRWg3Old4Q5FhEGnV1tzBZJd7+WkSZ7Htydb/52Fj+eyi5OlZv/GkMhr/QaVSYnhDBKa23m4sCncFP2cZFeHHJaCTHGNy/wQUuA+6VjK4SjgyJSfgwYphgxxtuYWhTUj9wufAqNPRFkh9wmX3FdQ8lrjO5GPj3TUrpdq2cxjmtN5uL2tnHkOX8ag6vWV9pCRF4JC0OLy4ZjbKNiwQ/n8wdNmw/VI4/fFKM7YfKPZrJJhT/a8MSyeHpCwTd5PF6WYP7RET2rl1rxWRVFI5YA2uTWerq6h2X2Vf23bndjefgOpMrc+EoPDl3OI9cEl/SaVRYNYf7+JtthyqwLmOUIK133WcRnr/ajKPl9U7Xr4qBR12wfAjZ9SsECnw4UM9ZgNwwo2DHS9TpUF3VKNjxiLQuF16FNjUCFoGneBPlEXL2FddjXWps45SOSMd+c3/7YIXbhwaugTFX3WcRCjnongt7d2139q5foHPzVV+iwMcNwx0jsXGUsIufRTd2gCax+49r11oxSWXEUavJfWLi17jOquKSTshjEellLk5Gi8mK945Wu00r5rIEniyn4CmuXb+/usu3LZY0xqcPQYYwvDNnOSwCro2iVzEoO+F6w0SiTLUnrkI+K8UQqXBZsZfr7KsVafFuxwHSTC5lSRwUyind+aveb1AsB1y7fj8odB8MCokCH1cYBieWPIRKHbcTlavxah1utfDbBZ7I39XaW0jV9Nzo0l9paf2pXgmxJ5fdq7llbtPQTC5l4bqVxZHyeq8HOcsB15ar6gbfdtfSFeOCdd49+Kz/EMGP2/Ydje3xV9dPXQuImXpqAOrr7VJnQzZ2F1Q6zVTxZk8uO/u4iL7CS5rJpTxcAmM7rrP/5OxCLbeWq2EDfLvVCo3x6YUheRz+MHyy4MeN02lxvuxyQNwcA9GVy82YPHEwCi3+PdZntF6Pjnbxd6iXszazFT95swCPxwOvfHEOJmvnVd11poone3IB3MZFqBhgXcYoIYpCfCxzcTJOX2xEQUXfO6MLPcjZ18wdNhytdL/7OwPggSnDsD/Xdy1cFPh0ExwxEH9JWwYwwjeGDWm24qrgRyVy0njmBjCqn9TZEFVYowUIC5I6G5J54t1C5JXUQa9mgXjn33WdqZK5ONmjm5aQU+KJPCUZDW4DH0DZe69xXY5hWmKEz7trqaurC5VajYOLHkKtVvhKXa9icP44DWr2d9VVjUjR+/dYn8vn3FfY/soe9LjjTTcFbUjq/7jOxBsa7tsuICF9dOISp3QjojxfYdpTFPh00bzgR/jaECnKsSeotGihQc0BIeiy/96Qhuq0uFp7S+psSKLNbOUU9AC3W2Q8QdPY/R/XQc7Z+8oUOcjZ3GFDCYdd6QFpzmMKfH5gmr8Mfxk2TrTjN5bSSs2B4uzJWkRp/bMXeag1cEeobeJ5A/K0ReaBKe63xqFp7MrGdZAzC2G2sfC13QWVnFZ3ZyDNeUyBzw/eihsv2rFH6nWoLL8p2vGJvLA2YLifNvp01PppwTiovMGv7J48yWbnlGDchi/cpqNp7MrnavZfb5Q2w4tr0D86xiDJeUxXjg/0r/PvWT6kp++O10An4N5ucqBXMThffA0AwPhZ2biIH8g9kPGkRYbLTuz+vmt2oMlcnIzfLHQ/O8+brlMpcJ3G/qOJQ0XOSe8o8BHZAI0aJUU0qDnQNDWZMUGtkzobgkrW6GA2K+epU2i/4xFs8G2RMXfY8LabTSwZAKfXL6Cgx89cvslt8T6lDGbPzinhNGNNyu5aCnxENtqigsUSuDeLQNZe0SR1FgQVVB9YO9B3F6xTIz3Z/eQHT1pkHt1+1O2YCBa+X9qfiI9rl6gStrHgsgaVnZTdtRT4iKi/Ro1z31yWOhtEIudKrmOITit1NgRTWXJN6ixIbtsjU1wGPyOi+uH8HxfxDnq4PiEDynnqJ9z50zYWXNfumZ4YIWnLJQU+IkpuZnHrFk1hD1QMgPhW/9jT6g69Dg0NNFYN6Ax+Sl9aiJ9MiQUA/GRKLEpfWojcX9/J+wmWzxMyQFPY/ZE/bWPBNTCXYu2erijwEUmsTotT33BbwIn4r6pv/WP/rkizfwRwQgnWqfH7JZ1PrL9fkoxgndqj43B9QgZoCrs/y1ycjLSECLfppNjJnA+ug5qlDuAp8BHJkOsW2GgH64B3/XorxvrBSs6mAJ7GLiY+XVc0hd2/JRm5tYL4eidzrvjszSV1AE9XkQgGadQoLrwidTaITATVyrOi4koNoDyAt6kQE9cnZKnHRBDxcW0F8fVO5lzJeW+u7ijwEcHIdgYdHdTaQzqVnrqKfmrlXmoj9Tq0tXU4vRaAy/gIjs+033cfn+aDHBEpcR3kXNPYLn5mPCDnvbm6U25tLFNBKgbnj9VInQ0iIxazDWMY5c7uCq+nAfpCU8q0X+I7Oo0Kj8+Id5tuz9Eq8TPDk9z35uqOriaBTWS0aG4O7PVOSE+t3zdKnQWP6BgG509dlTobfkcp036Jb0X3d9+NxXUwvC/JfW+u7ijwEdjVb69LnQUiQ+fLbiBegWv6JGt1uNVCLT5CU8q0X+JbfAa7b8ktEzEn/Mh9b67upM+BHxmv1+PKZfmvrkl8jwEQ02iVOhu8BTdQ66UYuDb3y6FbgPgOn+97Z0GVbNb04Zpvqfbm6o4CHwGpq1qkzgKRsbLjtQjmMnpRRqrLbkidBb/EZSArrdsTeLgOcAbktXHpirR4t+uVyel8psBHIEN1Wpz9tk7qbBAZa221YKxKORuXDtfrcOO6sqfiy9lUNwvW0aDmwMNnFWdAPluYvMqh201O57M8cuEHhjVZ/WKFXiIu62XltApGtbserkjnuueyc0ow6sXPcaS896nsKsazjU6Jf+C6ijMgj67Q7JwSvHWwos/BzXI7n0UPfLKzs8EwDNasWeN4jWVZZGVlISYmBsHBwZg7dy7Onj3r9D6TyYRnnnkGgwYNQmhoKJYtW4ZLl+S5BUSISoWywlqps0EUoOzMNYQrZE2fxkrpdpf313rDfpNwNTMnLSECZRv5b3RK/MuuldMU0XXEZVkGFQOsyxjloxxxI2oNXFhYiLfffhvjxo1zen3z5s3YsmUL3njjDRQWFsJoNCI9PR3NzbcHBq9ZswYff/wx9u7di8OHD6OlpQVLly6F1Sq/AaLjoUFrK818Ie5ZrSxGMRqps+HWAI0a31+QZrVmf603uNwkuCz5T/yfTqPCk3P67vKysdy6mMTEZVkGOY1FshMt8GlpacFDDz2Ebdu2YcCAAY7XWZbF66+/jhdeeAHLly9HSkoKdu3ahdbWVrz//vsAgMbGRmzfvh2vvvoq5s+fj4kTJ2LPnj04c+YM9u/fL1aWPcIAqDtNU9gJd+aL8u/uuoPRgNPCHALz53pDqTcJIo3MxclYNSehz5aftw5WIDunxGd56o7rGCO5jEWyE+3R86mnnsKSJUswf/58/PGPf3S8XlFRgdraWmRkZDhe0+v1uPPOO5Gfn49Vq1ahqKgIFovFKU1MTAxSUlKQn5+PBQsW9Pg8k8kEk8nk+LmpqbOZ3mKxwGJx3Rpj/52e9Wxa4CSNDmdrm6D3bHNmUehVrNP//YE/lamy9Bqihg5Fu6XznPP03BNTcH0b9Oo+/tY2a4/ryv5z19f7uvZ64+t6A/C+7uBaxsrrzX3/TX9wsb6F999NKHzLJHdKL8+v7hqOXd+UOz2DdK8L380vx6/uGi7JwOH4AXpO53T8AD2va0ns70uUwGfv3r04ceIECgsLe/yutrZzLExUVJTT61FRUaiqqnKk0el0Tk989jT293eXnZ2NDRs29Hg9NzcXISHuB4BtbrnsNo1LUz1/q5g2TpbfDdVbflOmhkrHP70690T0UF/n9fVi5OQU9/qrvLw8x79bW7k/6UlRbwDe1x1dy9uXySpgMqe6ogI5Ody2sxAL1zIphZLL84qLc6ZrXbg/d5+PcuNsIIDNXM7phhLkcGiZsn9PfOoNTwge+Fy8eBHPPvsscnNzERQU5DId022XQ5Zle7zWXV9pMjMzsXbtWsfPTU1NiI2NRUZGBsLCwlwe02KxIC8vD8/3GwITwy9inqrS4ts8+e2bolex2DjZhhePq2Cy+cf8G38r0x0jB+K7IVpsbrns0bknpmitFvX7+j6vf794NH4ydZjTa/ZrKT09HVpt5yrV9tYTd6SqNwDv646u5e3NltwyvJPPrZ5QMcDxF9Ilm/bLtUxKofTyvJxTir8fq3Z6rbe68KdTh+GFxaN9mjdzhw2TX85z2337yPQ4PL+w78HN3b8nrvWGpwQPfIqKilBXV4fU1FTHa1arFQcPHsQbb7yBc+fOAeh8OouOjnakqaurczzNGY1GmM1mNDQ0OD291dXVYcaMGb1+rl6vh16v7/G6VqvldMKbGBXaedx8glQMLhypg8kq35uwycbIOn+e8JcyFZfUY1hCZ+DA99wTWxSrRo27v7FK7fK66nrNcb3ZSFVvAN7XHX2lM3fY8OahanBdAGDVnASEBvfMi69xLbtSKLU8sRH9XNZ3XevC2Ih+Pi/fu0fK0dbh/rw29g/lnDf79yR2WQSvbefNm4czZ87g1KlTjv8mT56Mhx56CKdOnUJiYiKMRqNT06PZbMaBAwcclVNqaiq0Wq1TmpqaGhQXF/dZgfnSJFaLhoZ2qbNBFIoBEN3YIXU2etVxlcOihW5aWfjy13pjxzfcu6zkttYJkR6XlZwZAA9MGdZ3IhEodWAzIEKLj8FgQEpKitNroaGhGDhwoOP1NWvWYNOmTUhKSkJSUhI2bdqEkJAQPPjggwCA8PBwrFy5EuvWrcPAgQMRERGB5557DmPHjsX8+fOFzrJHzFfl92USZTl34iowXupcOAtRqXDuTI3PP9df6413DnMLfCYN609BD+nBvpLzWwddn0csgHEbvsATs30bOCt5vzlJFhR5/vnn0dbWhtWrV6OhoQHTpk1Dbm4uDIbbOxG/9tpr0Gg0uP/++9HW1oZ58+Zh586dUKulnz5l1GpQcvqK1NkgCtdukl+Lz2iNFmfN8hxArrR6w9xhw9Vmk/uEAOf9mUjgsQcz2w65XvjSxsIRHPkq+Km56b5lWA6LLPbGJ4HP119/7fQzwzDIyspCVlaWy/cEBQVh69at2Lp1q7iZ80Bisw0nrMqfVk3kQU73PE2tfPbmUnq98ej2o5zTZow2ipgTonSZi5PxzN0jkJL1RZ/pth2qwLqMUaIPjs/OKcHfvql0m05O+3N1Jb8cyVyERo2zR6i1hwgnRSuPjUv1KgbnzlyTOht+ITunBAUV3FdhfmwW940pSWD6oLDabRpfLIDJZQVyAFg5K1623bcU+PA0upmFyST98vfEj1TJYyXnFI0O7e3y635TGq43BrvpiRGyfCom8sJ1kPCHJ8Xdm47LCuQAEBMeLGo+vEFXGw8xOg2+zZfPhofEP5wvvY5EvfStProGs9RZ8AtcbwxAZzfnu49PEzU/xD9wHSRccqVZ1G0slDyby44CHx5iLrejo4PG9hDhRV6TPui4canZfaIfyGlcktx8dIL7w9GTc+Q5BoLID5ep7XbbDlXA3CHOJIULtdzqCTnO5rKjK46jiTo9ik9clTobxE+dOXYFg7XS7doepGJwsbpRss/3F+YOG87WcLsxTE+MkO0YCCI/9qntXIg11sfcYcPRSvdj1xjIczaXHQU+HGgZBjeOUdBDxGO1skjqkO5yTNTqIMO9UhWHz0wu6uIifGUuTsboKIP7hBBnrA/XbtxpMh+3Jt+cychEtQ51V29JnQ3i51ovSjfIeUCjMnevlhM+M7mSYwyyvjEQ+Vo2IYZTutIrzYJ3d3EdtzOCY3AmFbryOGgs4T4tlRBPXSi7jn5q31+SWobBhZPUoukNvjO5fjRxqIi5If6M6/YULITv7vKH8T0ABT5ujdHrUFVxU+pskABgtbIYrfL9RopjtTo0NUk/uFrJ+MzkkutqtkQZ+LQUCjmzyl/G9wAU+Lilq5DHGiskMLA1vp8Cqroi32mnSsFnJpdcV7Ml/kfIlhd/Gd8DUODTpxF6HUpoJVviQ+fO1CFU5bvLUgWgvPQG7/cJvDm7otFMLiIFLlPbrzQKtwWNv4zvASjw6ZOh+hatV0J8ymK2IVntu+6uO/Q6tLbSwGZv/GL3cc5paSYXEcqKqXFu0+z4plKwAc5K3o29Owp8XEjS61B8qk7qbJAA5MvurkEmWpDTW8eqGjilo5lcREjG/kFu0wi5no+Sd2Pvjq5CF8KotYdIpOIc/64nT7XX0vgeT/F9kqaZXERI1Q3curGEGOBs7rBhe36l23Q/mxmviOBe/jmUALX2ECndarEgQS9+d5eWYVBxnpZq8NR7R6s4p1XKkzBRjmEDuG0Cev4q961oXPndR6fBKnxj0q4o8OlF+MVWau0hkjJ2iH8GjtPp0NZGu7F7ancB98CHZnIRoT0wZRinAc5Hyuu92rTUamPx79M1nNLKeWPSruhK7CZJr8MZWsyNSMx2wyT6ZwTXif8Z/mrDJ8Woa+H294sy6GkmFxEcn727vNm09Nm9J9HO8b1KGNgMUODTQ7+KFmrtIZK7/P1NUY/fT61C6bcU4Hvinq2HsINHa8/KmdxuToTwlbk4GWkJEW7TeTrIOTunhHNrD6Og7lwKfLoYp9fTuj1EFq5fb0WcTrxxPuPUOtzyopuLCdDHg2VvHMKZy02c0zMAHptFgQ8RT5KR27o5fLuh+G7DsmRstGK6c5WRSx8YotWi5qtLAVqdEzkaImJPVHsl95s36dTS3oHTl/j93Z6cQ2N7iLjEWl+HzzYsITo1/usnE3kdX0p0Rf5AdfI6mptpvyIiHze/bxTluBEaNUqKqWWTr19/cJJXelqlmfjCirR4t4OcPZlVyKeFaMv946HmMtJaJijw+UHd1VtSZ4EQJ99/34AorUbw4yaqNJ1bNxNeuK6bAnTeaGiVZuILXAY521jg1dwyXsfl2kL040lDsTAlmtexpUaBDyEyxQAYbhX+EtU2UMumJ7iumwLQ9HXiW5mLk7FqTkKfQzXeOljBa1o7lxYiFQNsWj6W8zHlgq5MQmSspdL7xce6q6sQpwvN3732ALcxDD9Li6MuLuJz6zJGuU3DZ1o7lxYipQb4yssxIQHkfNl1RGjUgh1vkEaNS5eFD6YCQb8gDcYNDeszzdghYVh/b4qPckTIbbsLKt32YHOd1s5lRhcDbsGWHFHgQ4icscAIVrjAJ4HRCDJzkVHOOEZBffr0bJfBz7ihYfjXM7N9nCNCOnEdjMwlHZcZXSyE2wDV14QfOUkIEVRrZTMQqxfkWGofrAjt7z59ejZa2jvwHx8UAajF3SMG4z8fSEW/IKpOiXSEnNYuZBAlR9TiQ4jMnS+9jsECzO7SMgwunKHNd4XQL0iD/35wEgDgvx+cREEPkRyXae0AcKXR/exEsdYGkgsKfAiROxZI6vD+Uh2j1eFWi0WADBFC5Ibr3l3bD1e6nd0l1tpAckGBDyEK0CjA3l36a+3eZ4QQIluZi5Px85nxbtNxmd011c0eYEqd0QVQ4EOIInx3oQFDvNi7S69icP5b6uYixN9F93e/3lRfs7uyc0ow6sXPcaS8vtffqxhg1ZwERS/ZIHjgk52djSlTpsBgMCAyMhL33Xcfzp0755SGZVlkZWUhJiYGwcHBmDt3Ls6ePeuUxmQy4ZlnnsGgQYMQGhqKZcuW4dKlS0JnlxBFYADEeTEuOUWjQ5sXm5KKjeoNQoThzcDk7JwSvHWwwuWMrrSECJRtXKTooAcQIfA5cOAAnnrqKRw5cgR5eXno6OhARkYGbt26vSXE5s2bsWXLFrzxxhsoLCyE0WhEeno6mptvry+yZs0afPzxx9i7dy8OHz6MlpYWLF26FFarVegsE6IIdWW9P4Fxoa6R9+wLqjcIEQbXAcfnrzqv58Vl7Z6jlZ7XQXIieOCzb98+PPbYYxgzZgzGjx+PHTt2oLq6GkVFRQA6n9pef/11vPDCC1i+fDlSUlKwa9cutLa24v333wcANDY2Yvv27Xj11Vcxf/58TJw4EXv27MGZM2ewf/9+obNMiCJcqm5Col7H+30hKhXKTgvbzSX0Mj5UbxAiDK6zu46W1zuN8+Gydg/XBRDlTvQ5mI2NncvjR0R0DpSqqKhAbW0tMjIyHGn0ej3uvPNO5OfnY9WqVSgqKoLFYnFKExMTg5SUFOTn52PBggU9PsdkMsFkut0X0NTUBACwWCywWFzPZLH/Tq/yn10b7WWhMsmbJ2WKbbbgipbbkvN2qVDjtNUKvXDrIAI2a4/ryv5z19f7uvb64qt6A/C+7vC0jHLkb2Xyt/IA7svEAJiZ0B/HqhrcHuvxHQXY8dhUAMC/Tl6EXu2+LrpY3yL437N7mcT+vkQNfFiWxdq1azFr1iykpHQu415bWwsAiIqKckobFRWFqqoqRxqdTocBAwb0SGN/f3fZ2dnYsGFDj9dzc3MREuK+6W/jZH43EyWgMikDrzK1V+JBTyZnTfXgPX25eho5Oad7/VVeXp7j362t/LvYfFlvAN7XHV3L6y/8rUz+Vh6g7zL9OKrzP/euIycnBwDwszgAcVzeU4GcnL67xDxlL5Mn9QYfogY+Tz/9NE6fPo3Dhw/3+B3Tbc17lmV7vNZdX2kyMzOxdu1ax89NTU2IjY1FRkYGwsJc769jsViQl5eHF4+rYLL5xzr8ehWLjZNtVCaZ87RM4zLiUGjl9kQ0QatD6b5KD3Po2oZ7xuBHqUOdXrNfS+np6dBqO2eg2VtP+PBlvQF4X3d0La/S+VuZ/K08ALcy7S6oxCtfnOv1d90xAFKH9cfx6puc0hb9Pl3waezdy+RJvcGHaIHPM888g08//RQHDx7E0KG3K0ij0Qig8+ksOjra8XpdXZ3jac5oNMJsNqOhocHp6a2urg4zZszo9fP0ej30+p7L+mu1Wk4nvMnGwGT1jxuqHZVJGfiWqerEDXRM7I8ON63SGgaoL74pzt9LpXZ5XXW95vjebHxdbwDe1x1c0ymJv5XJ38oD9F2mh2cMxx8/P+92zI7dNxWN4DJyLznGgNBgYbbP6Y29TGJ/V4IPbmZZFk8//TQ++ugjfPnll0hIcF5JMiEhAUaj0amZzmw248CBA47KKTU1FVqt1ilNTU0NiouL+6zACAkEdVdvYZrF/TPLNIsG1VWNPsiR96jeIEQ4XFdx5utHE4e6T6QAgrf4PPXUU3j//ffxySefwGAwOPrWw8PDERwcDIZhsGbNGmzatAlJSUlISkrCpk2bEBISggcffNCRduXKlVi3bh0GDhyIiIgIPPfccxg7dizmz58vdJYJUZyiL6uQfE8iSky9L+4zWq/DiTxx+uHFQPUGIcLKXJyM0xcbUVAhzBR0JW9R0Z3ggc+bb74JAJg7d67T6zt27MBjjz0GAHj++efR1taG1atXo6GhAdOmTUNubi4MBoMj/WuvvQaNRoP7778fbW1tmDdvHnbu3Am1WsipKYQoFAvc/KYGKTOjUdwt+EnW69BytA6siOPA3Qyr4Y3qDUKEt2vlNIz4/eeCHEvJW1R0J3jgw7LuOxUZhkFWVhaysrJcpgkKCsLWrVuxdetWAXNHiP+ov9GGG5+WY3raEJRFqGFQqRB1uR1nTlQIvs6O2KjeIER4Oo0KaQkRXrf6TE+MUPxqzV2Jvo4PIUQ8DIBTBZcR2k+LG20duGZlFRf0EELE422rj4oB3n18moA5kp5/tFsREuButVhgs/rP4o6EEGHoNCqsmuP5QGd/6uKyoxYfQgghxI/Zu6m2HXK9AWl3KqYz6PGnLi47CnwIIYQQP5e5OBnrMkbhkXeO4ki56zE/Y6INWD5pKFakxftdS4+df5aKEEIIIU50GhX2PpmGVXMSemxkqmKAVXMS8Nmzc7BydqLfBj0AtfgQQgghAcXe+rO7oBJV9a2Iiwjx6xae7ijwIYTwxtDcMUIUTadRYeXsRKmzIYnACO8IIYQQQkCBDyGEEEICCAU+hBBCCAkYFPgQQgghJGBQ4EMIIYSQgEGBDyGEEEICBgU+hBD+aDY7IUShKPAhhBBCSMCgwIcQQgghAYMCH0IIIYQEDAp8CCGEEBIwKPAhhBBCSMCgwIcQQgghAYMCH0IIIYQEDAp8CCG80TI+hBClosCHEEIIIQGDAh9CCCGEBAwKfAghhBASMCjwIYQQQkjAoMCHEEIIIQGDAh9CCCGEBAwKfAghvDEMTWgnhCgTBT6EEEIICRgU+BBCCE9WG4tjFfUAgGMV9bDaWIlzRIi8WW0sCr6/gU9OXUbB9zckvWZkH/j85S9/QUJCAoKCgpCamopDhw5JnSVCiMyJWW/sK67BrFe+xOO7CgEAj+8qxKxXvsS+4hrBPoMQf2K/Zn667Qie3XsKP912RNJrRtaBzwcffIA1a9bghRdewMmTJzF79mwsWrQI1dXVUmeNECJTYtYb+4pr8Ms9J1DT2O70em1jO3655wQFP4R0I8drRtaBz5YtW7By5Ur8/Oc/x+jRo/H6668jNjYWb775ptRZI4TIlFj1htXGYsO/StBbA739tQ3/KqFuL0J+INdrRuPTT+PBbDajqKgIv/3tb51ez8jIQH5+fo/0JpMJJpPJ8XNTUxMAwGKxwGKxuPwc++8MGgY6lfAzVdhev3Khjt07/Q/hbKgG0HgQ2op5CrKsZ0fXqzrfF6xm4eprEjffwh/TXib7/xXFZu1xXdl/7vp6X9eeGPjWGwD3uuNYRT3qW9qgV3f+3Nv3V9/ShiPf1WFqQoQg5fG13r5DJfO38gDKKlP3a6Y39S1tKCy/BsB3ZZNt4HP9+nVYrVZERUU5vR4VFYXa2toe6bOzs7Fhw4Yer+fm5iIkJMTt5704qcPzzMrU+klWqbMguA2pNqmzILiNkxVYpiunkHPlVK+/ysvLc/y7tbXVRxnqxLfeAPjVHZun9nx/9+/veukR5JTyzLjMdP0O/YG/lQdQTpl6u2a6qz9/HMDtMoldb8g28LHrvl4Iy7K9riGSmZmJtWvXOn5uampCbGwsMjIyEBYW5vL4FosFeXl5SE9Ph1arFS7jEqIyKYO/lam38thbT3yNa70BcK87jlXUOwY0A50tPRsn2/DicRVMttvHfufRKYpu8fH3c1LplFSm7teMK397eCLqzx93lEnsekO2gc+gQYOgVqt7PKXV1dX1eJoDAL1eD71e3+N1rVbL6eTgmk5JqEzK4G9l6loeX5eLb70BcK87pt8RiYh+wahtbHfqVjXZGJisDBgAxvAgTL8jEmoRus19yZ/PSX+hhDK5umbs7NfMlMTB+OL87TKJXS7ZDm7W6XRITU3t0ZyXl5eHGTNmSJQrQoiciVlvqFUM1t+TDKCzwu7K/vP6e5IVH/QQIhS5XjOyDXwAYO3atfjb3/6Gd955B6Wlpfj1r3+N6upq/OIXv5A6a4QQmRKz3liYEo03H54EY3iQ0+vG8CC8+fAkLEyJ9vozCPEncrxmZNvVBQAPPPAAbty4gZdeegk1NTVISUlBTk4O4uLipM4aIUSmxK43FqZEIz3ZiCPf1eF66RG88+gUv+jeIkQs9mvmWEU96prbEWkIwtSECMmuGVkHPgCwevVqrF69WupsEEIUROx6Q61iMDUhAjmlkLQCJ0Qp1CoGacMHSp0NADLv6iKEEEIIERIFPoQQQggJGBT4EEIIISRgyH6Mj6fsWyO4WwjJYrGgtbUVTU1Nsl8TgSsqkzL4W5l6K4/9+vN0qxIpUN3hP2Xyt/IAgVEmsesNvw18mpubAQCxsbES54QQ0tzcjPDwcKmzwQnVHYTIg1j1BsMq6VGMB5vNhitXrsBgMLhcqh64vTz9xYsX+9zaQkmoTMrgb2XqrTwsy6K5uRkxMTFQqZTRs051h/+Uyd/KAwRGmcSuN/y2xUelUmHo0KGc04eFhfnNSWRHZVIGfytT9/IopaXHjuoO/yuTv5UH8P8yiVlvKOMRjBBCCCFEABT4EEIIISRgBHzgo9frsX79+l53Z1YqKpMy+FuZ/K087vhjef2tTP5WHoDKJAS/HdxMCCGEENJdwLf4EEIIISRwUOBDCCGEkIBBgQ8hhBBCAgYFPoQQQggJGBT4EEIIISRgBHzg85e//AUJCQkICgpCamoqDh06JHWWepWdnY0pU6bAYDAgMjIS9913H86dO+eU5rHHHgPDME7/TZ8+3SmNyWTCM888g0GDBiE0NBTLli3DpUuXfFkUh6ysrB75NRqNjt+zLIusrCzExMQgODgYc+fOxdmzZ52OIafyxMfH9ygPwzB46qmnACjj+zl48CDuuecexMTEgGEY/POf/3T6vVDfSUNDA1asWIHw8HCEh4djxYoVuHnzpsilEw7VG1RvCInqDu5lEKTuYAPY3r17Wa1Wy27bto0tKSlhn332WTY0NJStqqqSOms9LFiwgN2xYwdbXFzMnjp1il2yZAk7bNgwtqWlxZHm0UcfZRcuXMjW1NQ4/rtx44bTcX7xi1+wQ4YMYfPy8tgTJ06wd911Fzt+/Hi2o6PD10Vi169fz44ZM8Ypv3V1dY7f/+lPf2INBgP74YcfsmfOnGEfeOABNjo6mm1qapJleerq6pzKkpeXxwJgv/rqK5ZllfH95OTksC+88AL74YcfsgDYjz/+2On3Qn0nCxcuZFNSUtj8/Hw2Pz+fTUlJYZcuXeqTMnqL6g2qN4RGdQf3MghRdwR04DN16lT2F7/4hdNro0aNYn/7299KlCPu6urqWADsgQMHHK89+uij7L333uvyPTdv3mS1Wi27d+9ex2uXL19mVSoVu2/fPjGz26v169ez48eP7/V3NpuNNRqN7J/+9CfHa+3t7Wx4eDj717/+lWVZ+ZWnu2effZYdPnw4a7PZWJZV3vfTvfIS6jspKSlhAbBHjhxxpCkoKGABsGVlZSKXyntUb1C9ITaqOzqJVXcEbFeX2WxGUVERMjIynF7PyMhAfn6+RLnirrGxEQAQERHh9PrXX3+NyMhIjBgxAk888QTq6uocvysqKoLFYnEqc0xMDFJSUiQr84ULFxATE4OEhAT85Cc/QXl5OQCgoqICtbW1TnnV6/W48847HXmVY3nszGYz9uzZg8cff9xph2+lfT9dCfWdFBQUIDw8HNOmTXOkmT59OsLDw2VRzr5QvdFJ6vPSX+sNgOoOX9QdARv4XL9+HVarFVFRUU6vR0VFoba2VqJcccOyLNauXYtZs2YhJSXF8fqiRYvw3nvv4csvv8Srr76KwsJC3H333TCZTACA2tpa6HQ6DBgwwOl4UpV52rRpePfdd/HFF19g27ZtqK2txYwZM3Djxg1Hfvr6fuRWnq7++c9/4ubNm3jsscccrynt++lOqO+ktrYWkZGRPY4fGRkpi3L2heqN26jeEAfVHeLXHRpeufdDXSNqoLNy6P6a3Dz99NM4ffo0Dh8+7PT6Aw884Ph3SkoKJk+ejLi4OHz22WdYvny5y+NJVeZFixY5/j127FikpaVh+PDh2LVrl2Pgniffjxy+w+3bt2PRokWIiYlxvKa078cVIb6T3tLLrZx9oXqD6g2xUN2BPtMIUXcEbIvPoEGDoFare0SJdXV1PaJSOXnmmWfw6aef4quvvsLQoUP7TBsdHY24uDhcuHABAGA0GmE2m9HQ0OCUTi5lDg0NxdixY3HhwgXHLI2+vh+5lqeqqgr79+/Hz3/+8z7TKe37Eeo7MRqNuHr1ao/jX7t2TRbl7AvVG7fJpcz+Um8AVHf4qu4I2MBHp9MhNTUVeXl5Tq/n5eVhxowZEuXKNZZl8fTTT+Ojjz7Cl19+iYSEBLfvuXHjBi5evIjo6GgAQGpqKrRarVOZa2pqUFxcLIsym0wmlJaWIjo6GgkJCTAajU55NZvNOHDggCOvci3Pjh07EBkZiSVLlvSZTmnfj1DfSVpaGhobG3Hs2DFHmqNHj6KxsVEW5ewL1Rud5HRe+ku9AVDd4bO6g/MwaD9kn5a6fft2tqSkhF2zZg0bGhrKVlZWSp21Hn75y1+y4eHh7Ndff+00pbG1tZVlWZZtbm5m161bx+bn57MVFRXsV199xaalpbFDhgzpMV1w6NCh7P79+9kTJ06wd999t2TTONetW8d+/fXXbHl5OXvkyBF26dKlrMFgcPz9//SnP7Hh4eHsRx99xJ45c4b96U9/2uv0R7mUh2VZ1mq1ssOGDWN/85vfOL2ulO+nubmZPXnyJHvy5EkWALtlyxb25MmTjqnaQn0nCxcuZMeNG8cWFBSwBQUF7NixYxU3nZ3qDao3hER1h+/qjoAOfFiWZf/85z+zcXFxrE6nYydNmuQ0zVNOAPT6344dO1iWZdnW1lY2IyODHTx4MKvVatlhw4axjz76KFtdXe10nLa2Nvbpp59mIyIi2ODgYHbp0qU90viKfR0HrVbLxsTEsMuXL2fPnj3r+L3NZmPXr1/PGo1GVq/Xs3PmzGHPnDnjdAw5lYdlWfaLL75gAbDnzp1zel0p389XX33V63n26KOPsiwr3Hdy48YN9qGHHmINBgNrMBjYhx56iG1oaPBRKb1H9QbVG0KjusN3dQfDsizLvX2IEEIIIUS5AnaMDyGEEEICDwU+hBBCCAkYFPgQQgghJGBQ4EMIIYSQgEGBDyGEEEICBgU+hBBCCAkYFPgQQgghJGBQ4EMIIYSQgEGBDyGEEEICBgU+hBBCCAkYFPgQQgghJGD8/y56mkyW6nhSAAAAAElFTkSuQmCC\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": "iVBORw0KGgoAAAANSUhEUgAAAj4AAAEjCAYAAAAykgt0AAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjYuMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8o6BhiAAAACXBIWXMAAA9hAAAPYQGoP6dpAABx80lEQVR4nO29e1yUZf7//5qBYQBDEg2GSQT0gylibXkEj5uCJ7Q+7idtLdM0Y9dDy6KbkbliFqT9Ir9f3LV0TU0y+z6+G219QwI7qAQqkm4iHmo5qAlSOXIInBmY+/cHzcgwp+ue433PvJ+PBw+de95zz3XNdd/X/b7e1/sg4TiOA0EQBEEQhA8g9XQDCIIgCIIg3AUpPgRBEARB+Ayk+BAEQRAE4TOQ4kMQBEEQhM9Aig9BEARBED4DKT4EQRAEQfgMpPgQBEEQBOEz+Hu6Aa5Cp9Ph+vXrCAkJgUQi8XRzCMIn4TgOra2tUCqVkEppnUUQhOfxWsXn+vXriIqK8nQzCIIAcPXqVQwcONDTzSAIgvBexSckJARA94Tbt29fi3JarRbFxcVISUmBTCZzV/NcCvVJHHhbn8z1p6WlBVFRUYb7kSAIwtN4reKj397q27evTcUnODgYffv29YqHD0B9Egve1idr/aHtZoIghAJtuhMEQRAE4TOQ4kMQBEEQhM9Aig9BEARBED4DKT4EQRAEQfgMXuvczEKXjsOp2psAgFO1NzH+v8LhJ/VOJ0xNpw4HyutQf7Md0WHBWJwYgwB/8em9r3xajZqfbyOmfzBenB2PoAA/TzfJp9DfM02ttxEeEoixsWFee88QBOGd+KziU1TVgM2fVONmWwe2jQWW7a9A2F1B2DQ3HjMTIj3dPKeSU1iN3cdroePuHHu18AJWTIpF5ux4zzWMB8+9/w1mhgKHKq5C3SXB8e+AAyeuIDk+HLufGuPp5vkE+numofm24VhkaKBX3jMEQXgv4lvyO4Giqgb8Mf8bowkcABqbb+OP+d+gqKrBQy1zPjmF1Xj7mLHSAwA6Dnj7WC1yCqs90zAerHi3Al9c+tHseyXVTVjxboWbW+R7+NI9QxCEd+Nzik+XjsPmT6rBmXlPf2zzJ9Xo6q0piBBNpw67j9daldl9vBaaTp2bWsSfDk0XSqqbrMqUVDehQ9Plphb5Hr50zxAE4f34nOJzqvamyaq1JxyAhubbBt8fMfNBxRUTS09vdBxwoLzOLe2xh2xGixSrHMEf1numsl7lvkYRBEHYic8pPk2tlidwe+SETP3NDka5dhe3xH7qfmZrW82PbS5uie/Cei/81KZ2cUsIgiAcx+cUn/CQQKfKCZlPv/2BSS46LNjFLbGfmP5sbfv6PzdF4a8kRljvhQF3yV3cEoIgCMfxOcVnbGwYIkMDYSkAV4LuSJWxsWHubJZTOXLhBgCgVW3b70UqARYnxri4RfbzIo+oM7E4a4sN1ntmVHQ/dzaLIAjCLnxO8fGTSrBpbvfDtPdErn+9aW68aHOTdOk4vHb4IrP8ikmxgs7nExTgh+T4cGZ5oTtrixFvv2cIgvAthPvEcyEzEyKx88mHoAg1NuErQgOx88mHRJ2T5FTtTTS22PbJkEqAtMniyOOz+6kxePi+e5hkhe6sLVa8+Z4hCMK38NkEhjMTIpEcr8CJ75vw04UTeGfJGK/I3PyP4/9hknv9d/fjd6OjXNwa5/G/f/8QCgsLmWSF7KwtZvT3DGVuJghCzPis4gN0m/DHxoah8AK8YgLPKazG5xd/hJyhioOyn3Admh1FyM7aYsdPKkHikP6ebgZBEITd+ORWlzfCkqxQj5idt23ppkJ31iYIgiA8i09bfLyJA+V1NpMV6hGzI+rSxGjsPH7F4vtCd9YmhI9Op8P169cREhICiUSc9wlBiBmO49Da2gqlUgmp1PnzOSk+XgKrX8uUoQNE7YiakTIMOomfSdFVqQSiKrpKCJfr168jKko8/m8E4a1cvXoVAwcOdPp5eSs+x44dw+uvv47Kyko0NDSgoKAAjz76qOF9juOwefNm7Nq1CyqVCuPGjcPf/vY3jBgxwiCjVquxbt06vP/+++jo6MC0adPw97//3aiDKpUKzz33HD7++GMAwLx585CXl4e7777b/t56Max+LZPj2KKjhEzm7HisTRmGA+V1qL/ZjuiwYCxOjCFLD+EUQkJCAHRPun379jV5X6vVori4GCkpKZDJZO5unkvwxj4B3tkvX+hTS0sLoqKiDPeis+Gt+Pzyyy944IEH8PTTT+N3v/udyfvbtm1Dbm4u9u3bh6FDh+KVV15BcnIyLl26ZOhEeno6PvnkExw6dAj9+/fH2rVrkZqaisrKSvj5dXvmLlq0CNeuXUNRUREA4Nlnn8XixYvxySefONJfr2VxYgxeLbxgdbvLm/xfAvylWD5psMX3NZ06UowIu9Bvb/Xt29ei4hMcHIy+fft61YPH2/oEeGe/fKlPrtpq5q34zJo1C7NmzTL7Hsdx2L59OzZs2ID58+cDAPbv34+IiAgcPHgQaWlpaG5uxp49e3DgwAFMnz4dAJCfn4+oqCgcOXIEM2bMwIULF1BUVIQTJ05g3LhxAIDdu3cjMTERly5dwn333Wdvf72asbFhOFFjubiqr/i/5BRWm2yFvVp4gbbCCIIgCOf6+NTW1qKxsREpKSmGY3K5HFOmTEFZWRnS0tJQWVkJrVZrJKNUKpGQkICysjLMmDED5eXlCA0NNSg9ADB+/HiEhoairKzMrOKjVquhVt8pktjS0gKgW5PUarUW26x/z5qM0Mktvoh95fXQcYDcD5BLu5/4+n+lkm6n4IzkONH2k3WccosvYl9ZPWRm9Lt9X9dAynUhI2WYK5rIG2+49npirj/e0jeCILwHpyo+jY2NAICIiAij4xEREaivrzfIBAQEoF+/fiYy+s83NjYiPNy0TEF4eLhBpjc5OTnYvHmzyfHi4mIEB9v2fykpKbEpI1SGAXhtjOnxLaN7lG7orEFhYY3b2uQqbI3TMADbxloREODvIOZrzxw9+9PeTskkxUaXjvPaJJW0BU4ALorq6r0vx3Gczb263jLm5K2dJzMzExkZGYbXeueolJQUs/v0erRaLUpKSpCcnCy6/VJNpw6jXy0x8euRSzlsGa3DxtNSaDkJTm9IFv3NzTJOB8rrsPWzSzbPtX7GfYLwdRLztWcOc/3RW14JcXDkwg28/OklNDTfKXsTGRqITXPjRR0NCtAWOHEHpyo+CoUCQLfFJjLyzk3S1NRksAIpFApoNBqoVCojq09TUxOSkpIMMjdu3DA5/48//mhiTdIjl8shl8tNjstkMqaHCquckHj3RA06Oi0rlGqdBOouCQ6d/sGqI7CYsDZOdSo11F22V6Z1KrWgxlqM1541evbHm/rlC/z5g7O43eseamy+jT/mfyPqmmy5xRfxtpn8XzoOePtYd+JXUn58B6eaAWJjY6FQKIxM3RqNBkePHjUoNaNGjYJMJjOSaWhoQFVVlUEmMTERzc3NOHXqlEHm5MmTaG5uNsgQ7Ll7fKV2FWtIP5W0IAhjun41g5gLCtUf2/xJtUFObOwrr7f6/u7jtdB06qzKEN4Db4tPW1sbvv/+e8Pr2tpanD17FmFhYRg0aBDS09ORnZ2NuLg4xMXFITs7G8HBwVi0aBEAIDQ0FMuXL8fatWvRv39/hIWFYd26dRg5cqQhymv48OGYOXMmVqxYgbfffhtAdzh7amoqRXT1QCgPev2+ec2PbWhqVSO8byAGD+jj9v1zXwvpJwhncarmZ6vvcwAamm/jVO1NUdZqs6Wv6bjurXJvsYwT1uGt+Jw+fRq//e1vDa/1fjVLlizBvn378Pzzz6OjowMrV640JDAsLi42SkT05ptvwt/fHwsWLDAkMNy3b58hhw8AvPfee3juuecM0V/z5s3Djh077O6oN+KpB31PB8HLN1pxsuam2ZWiu/fPA/ylWDEp1mC6NoevhPQTBCs5hdXY/3UNtloLCviVptbbtoVEiq9Yxgk7FJ+pU6eC4yw/aSUSCbKyspCVlWVRJjAwEHl5ecjLy7MoExYWhvz8fL7N8znclbtHr+z888w1VF9vZfqMfv+8q4vDS3NH2P6AE9ArWVTSgiBsk1NYjbeP1ULuZ1sWAMJDAl3bIA9CW+C+A9XqEinmIhR6sywpGuud8KBn+S5r/OPrOpz7oRn3RfZ1SwgplbQgCNtoOnXYfdyydbQnEgCK0O7QdjFiKxqftsB9C1J8RIh+lWaJsdH9APzkcKI+TacOT71z0qpFiZWTdSqcrFMBcM8WmK2SFgTh6xwor+O1mNk0N160+XyWJkZjp5moLj20Be5bkOIjMlhWaaevqPA/5qP+mXHUymMNCiElCM/D6tMSHOCH3AUPiDaUHQAyUoZBJ/GjLXACgJPD2QnXw7JKc0RZ0XTq8Piucrx9zDVKT092HatF2+1O134JIQi+/vprzJ07F0qlEhKJBB999JHR+xzHISsrC0qlEkFBQZg6dSrOnz9vJKNWq7FmzRoMGDAAffr0wbx583Dt2jUjGZVKhcWLFyM0NBShoaFYvHgxbt265eLeiRNWn5Y/Tx8qaqVHT+bseFzcMgsb5wzHU4nR2DhnOC5umUVKjw9Cio/IcGXkQU5hNYZtPOyUrS0WOAAJWZ8hp7DaLd9HeI729nY88MADFiMzt23bhtzcXOzYsQMVFRVQKBRITk5Ga+sdR/r09HQUFBTg0KFDKC0tRVtbG1JTU9HV1WWQWbRoEc6ePYuioiIUFRXh7NmzWLx4scv7J0YWJ8Yw+b4sSYpxS3vcgX4L/OVHErB80mCj7S1Npw57jtfgr/+qwp7jNZTXx4uhrS6R4arIA1t+Q66Etr28n+TkZPzud78z+x7Hcdi+fTs2bNiA+fPnAwD279+PiIgIHDx4EGlpaWhubsaePXtw4MABQ76v/Px8REVF4ciRI5gxYwYuXLiAoqIinDhxwlDgePfu3UhMTMSlS5coB5gZ3BUVKnSonIVvQYqPyGDN3cOHttudHlN69Ow6VouwPnI8PcE3JlriDrW1tWhsbDTk7AK6S9BMmTIFZWVlSEtLQ2VlJbRarZGMUqlEQkICysrKMGPGDJSXlyM0NNSg9ADA+PHjERoairKyMouKj1qthlqtNrzW1xfTarVmq8ubq0IvNnKLL2JfeT10HCD3667vB9z5VyrpdgjOSI4TdT9Zxiq3+CL2ldVDZmba2fd1DaRcl8OBIs7EG66/3vTuk6v7RoqPyGBJ0rc0MRroZKtAnlNYjV0OKj1SCTAuJgy3OrS40MiW46c3HICcwxexteiiW1dZVK3Z8zQ2NgKASR2+iIgI1NfXG2QCAgKM6vvpZfSfb2xsRHh4uMn5w8PDDTLmyMnJwebNm02OFxcXIzjYsoW1Z9kdsTEMwGtjTI9vGd1je6ezBoWFbPOI0LE2VsMAbLOWvFGgv4OYrz9L6PvU3u7aZJKk+IgQW0n6MpLjmG5UR7e3RkSGYP5DA42UBUejwdwZ8UXmbWEhkRibKjmOMznWm94y5uRtnSczM9OQgR7otvhERUUhJSUFffv2NZE3V4VeLGg6dRj9aonJ/SmXctgyWoeNp6XQchKc3pDsFQsAW2N1oLwOWz+7ZPM862fcJ5g8P2K+/izRu096q6urIMVHpFhL0mfLTNih6cIrn5zHexVX7fruxNgw7F8+zuzE2Ltdl2+02uUs7eqtL0tKH4Xaux+FQgGg22ITGXkneqipqclgBVIoFNBoNFCpVEZWn6amJkPhYoVCgRs3bpic/8cffzSxJvVELpdDLpebHO9ZZd4ctt4XIu+eqEFHp2UlUK2TQN0lwaHTP3hVHixLY1WnUkPdZds3oE6lFtxYi/H6s4W+T67ul/hVeh/GWoSCJVa8W4Hhfy2yS+mRAEibHIv30xKtflfPdh16NhFVWTN4f5d+62vYxsNOj/piyYVE1ZrdR2xsLBQKhZHpXqPR4OjRowalZtSoUZDJZEYyDQ0NqKqqMsgkJiaiubkZp06dMsicPHkSzc3NBhlfhzUq1FfqVgml0DPhXsji40OseLcCJdVNdn3WmpXHFncF+iNtsnW/JEvoLTBSrgvOci9kzYVE1ZqdR1tbG2pq7my/1tbW4uzZswgLC8OgQYOQnp6O7OxsxMXFIS4uDtnZ2QgODsaiRYsAAKGhoVi+fDnWrl2L/v37IywsDOvWrcPIkSMNUV7Dhw/HzJkzsWLFCrz99tsAgGeffRapqakU0fUrQnnQC8W3zlOFngnPQoqPj9Ch6bJL6ZEAeHay4z4vlvySWNlXXm/WGdMeaNXrfs6cOYPU1FTDa71PzZIlS7Bv3z48//zz6OjowMqVK6FSqTBu3DgUFxcjJCTE8Jk333wT/v7+WLBgATo6OjBt2jTs27cPfn53Kmy+9957eO655wzRX/PmzbOYO8gX8dSDvqeic/lGK07W3ETPJrzy6QWMd2BxZS8swSK+EtLvS5Di4yNk27FdJAFwLmsG7gp0zmWi9//ZV1qL7KKLvD7rzCzSQln1+hKTJk0Cx1keRIlEgqysLGRlZVmUCQwMRF5eHvLy8izKhIWFIT8/35Gmej3uzN3DWu+PA1BeexNDXzpsNmjCldgKFiFfP++DFB8foe5n/taLZyfHOk3p0RPgL8WzU4fg53a1x3IHkXmb8EVYIi6XJUVjvZMe9PpUGXzXLOcbWnH+0wtujbC0FixCeB+k+PgAmk4d2tXsNbHcsdKxd+srt/gi1s8Z6dB3k3mb8DVspa4YG90PwE9OSdTHauWxhd6/79urzW7ZAtMHZRDeDyk+Xg7fvDrrZ96H5RPZIsQcxZ6tr3fK6qGT+LnM54jM24S3wRLFePqKCv9jOeKfGUfzeJmjvPYm7nvpsFN8DQkCIMXHq3nt8EXsKbvCLJ8cH44/Tv0vF7bIFHu2vnYdq8Wah4c6vA1H5m3CF2CNYnQUV9b749Bt/Sn97ie3+v8Q3gkpPl5M/sl6dLso2yY5Phy7n3JS2JQd6FdyLD4B+qruaU5YAVoybwsl3JYgHMXV0YmaTh32lda6xWfPE/4/hPdBio8X8tz732BmKJvsqEF3I/+Z8QgK8LMt7GIyZ8ejTd2F906yWalclWGZSlkQ3oQroxNdsbXFAmVYJxyBlrACRdOpw57jNfjrv6qw53gNcxbhDk0Xvrj0I/P3jLg3VBBKj57BA/rwknd2hmW9ub73RK6faJ2dRZogXM3ixBhIbRh+bb3fG02nDo/vKjd7r7iTXcdq0XabPXCDIABSfARJTmE1hm08jC2fXsC75fXY8ukF5tINfPP1CC1XDcsk3RMdBzz1zkmnfDeVsiC8EX0UozWWJkYzn08/PzkatRUZalofjS/6bW93L0jsXZgSwoC2ugSGo8Uzv7jInp1ZiLlqWELNe3Oi5iZyCqsdNnlTKQvCW7EVxZiRHIfCwhoLn76DMxyYe5a/0fvS/fPMNVRfb7X7nO7c9qKtcPFDio+AYLU4rE0ZZtbRNqewGj/cug05486VUHPV8HF01mPtd2GFSlnYhpy+xYu1KEatVmvz8yzzkzXM1fvTBxcsnzTY4fw/u47VIqyPHE9PcN285ujClBAGNGMJCD4Wh97wnZSenhAj6Bs0c3Y8zvGo6m7pd+EDlbKwjiNbsIQw0CsaLz+SgOWT2PN1dWi68Ptd5Xb580glQNrkWLyflmj1+wL8pTj0bCLSJsfy9jkCure9cg5fdNk1SVvh3gMpPgLCEYvD3q/ZnQynDbsHm+aO4NM0j6Cv6s6Ko5YYVidQoW0PugNy+vZdVrxbgeF/LULllVu8PicBkDlrGC5umcVrkZU5Ox4Xt8zCxjnDEa8Msf2BXrjqmnRkYUoIC1J8BIS9Foecwmq8dpgt8/HAfkHYs3Qs77Z5iszZ8UiMDWOSHRga5NB3sTiBCnV70JXQStd3WfFuBUqq2f0Ge/Ls5FikTRli1/2it0wVPjcZaZNjGbORGePsa5K2wr0H35rBBY49Fgf9SpzVAv10UoxNGaGxf/k4pokvp+iiw6u8zNnxZk3tenO9kLcHXQXrSveDCvYs4YTw6dB02aX0SODce4XvtrceZ1+TtBXuPZBzs4DgWzyTr1+PWLdpAvyleDopGui0HnWiT2sPOOZgSKUsjGFdwV5RdaC/i9tCuA++qTEA8w7MzkC/7c03oqz2p1/Q30lpyhYnxuDVwgtWFwFinWN9Dd+cyQUMH4sDy0q8J2LeptFXjWZxenSGidteJ1BvhHUFO6ifY1uNhHDQdOpQ+v1PzPKsDsyOYGlutMb/qbzmtO+nrXDvgUZIgPR07nsqMRob5ww36yDIZy/ZW7Zp/jx9qE0ZcjB0LosTY2xuNUolwMIxg9zSHsK16KP3an9im19GDbqbtwOzvejnxhdnDuP1udxiNh9Ilu+nrXDx43TFp7OzEy+99BJiY2MRFBSEwYMH4+WXX4ZOd2cFznEcsrKyoFQqERQUhKlTp+L8+fNG51Gr1VizZg0GDBiAPn36YN68ebh2zXnau9BhsTjcezfbCvvFmcO85oa83nybSa7mxzYXt8R3eIPhoUErXe8gt/gi7zIU+c+Md+vYB/hL8ezUIbwiPveV1zvN0Zl1YUoIF6dfrVu3bsVbb72FHTt24MKFC9i2bRtef/115OXlGWS2bduG3Nxc7NixAxUVFVAoFEhOTkZr653Mnenp6SgoKMChQ4dQWlqKtrY2pKamoqury9lNFiU5hdXYWmT7gSSVAEsnsk8QQod1O+W9U1cpxNoJsDjP00rXe9hXXs9LPjk+3GO1/vTWF5adLx0H7Ct1XvV4SwtTKmUhDpzu3FxeXo5HHnkEc+bMAQDExMTg/fffx+nTpwF0W3u2b9+ODRs2YP78+QCA/fv3IyIiAgcPHkRaWhqam5uxZ88eHDhwANOnTwcA5OfnIyoqCkeOHMGMGfw9/L0JPmnjvW0lvnDMILxy+DLTipQyqToGi/O8VAKsTeG37UAIFz6WnuT4cOx+aozrGsNA5ux4tKm78N5J29Fb2UUX8XO72mXzAZWyEA9OV3wmTpyIt956C5cvX8bQoUPx73//G6Wlpdi+fTsAoLa2Fo2NjUhJSTF8Ri6XY8qUKSgrK0NaWhoqKyuh1WqNZJRKJRISElBWVmZW8VGr1VCr1YbXLS0tAACtVms1Hbv+PZaU7UJA06nDu2U1VstSyKXdd94ziVFIT44TTd+soe+DhOtC2sRBeKeMbWX6blkNnvutfblErKHp1OGDiiu4ourAoH5BWDhmEO/vEPq1l19eB5nU9pMwv+w/WJwYY7Y/Qu0bYT9Dw+/Cv1ZP9JilpzeDB/RhlnXVYohKWYgLpys+69evR3NzM4YNGwY/Pz90dXXh1Vdfxe9//3sAQGNjIwAgIiLC6HMRERGor683yAQEBKBfv34mMvrP9yYnJwebN282OV5cXIzgYNtRKSUlJbY7JxBeY1xkDe2qQ2FhnUvb4m5KSkowDMA2HjkYjxQXuaQt/X/9gwo4Umz/tppQr73+YPydVdUo7LGt2LM/7e2UzM1dOFJHraWDXUFdOCZKMEoPwBZm3pNdx2qx5uGhuCvQOY8/R2ssEu7H6YrPBx98gPz8fBw8eBAjRozA2bNnkZ6eDqVSiSVLlhjkJBLjnVmO40yO9caaTGZmJjIyMgyvW1paEBUVhZSUFPTt29fiObVaLUpKSpCcnAyZTMbSRY/y8v+rxv85fdWqjFzKYctonWj6xIK5cWL5LfQsS4o2hMQ7Qm7xRavWJj7fI/Rr70B5HbZ+dsmm3PoZ9xksPr37o7e8Eq7FkW2WnMJq7Pu6hnkxIbQ8NSz5z3rCAUjI+sxpvml8SlksnzTY4e8jHMfpis9f/vIXvPDCC3j88ccBACNHjkR9fT1ycnKwZMkSKBQKAN1WncjISMPnmpqaDFYghUIBjUYDlUplZPVpampCUlKS2e+Vy+WQy+Umx2UyGdNDhVXOk+QUViP/5DVwjAncxdAnvvTsU8yAEKi72H6LncevQCfxc2ii03Tq8HbpFeg4y9/5dukV/HnGCF4rO6GO05NJQ2z6U0kl3XKyHv3t2R8h9svbcGSbRf9Za1vnPbn37iBBWi30/dvFI4u9s7agqJSF+HD6Fdze3g6p1Pi0fn5+hnD22NhYKBQKI3O4RqPB0aNHDUrNqFGjIJPJjGQaGhpQVVVlUfHxdviUprCnsrEYYSnx0RNHExv6WpFCStgmfBypo8Y38zsAPDzsHl7y7iRzdjxemMXPquuMZKdUyoINIUW8OX3Gmjt3Ll599VV8+umnqKurQ0FBAXJzc/Hf//3fALq3uNLT05GdnY2CggJUVVVh6dKlCA4OxqJFiwAAoaGhWL58OdauXYvPP/8cZ86cwZNPPomRI0caorx8Cb4T1NLEaBe2RjiwPJh74qhS4osrO0rYJmwcUcb5Zn4HgBcFPt5PT+CX2dkZCxV7aiz6GvqkmFs+vYB3y+ux5dMLGLbxsMdSjjh9qysvLw8bN27EypUr0dTUBKVSibS0NPz1r381yDz//PPo6OjAypUroVKpMG7cOBQXFyMkJMQg8+abb8Lf3x8LFixAR0cHpk2bhn379sHPTzhOde6CdYKSoLsickZyHAoLrde18hb4mrgPVzXYXXfLl1Z2vR1lv900Ax9UXKHaZQLDEWW85qdfeH2XJ3P2sMLX3wdwPNkp3xqLvgbLVuy65Di3tsnpik9ISAi2b99uCF83h0QiQVZWFrKysizKBAYGIi8vzyjxobfAN/qCdXJbNDYKmbPjfS6EOHN2PML6yJFz2HZCx9P1tzBs42G7cmv4SpFCa46yLz+S4LmGESbYq4znFFbjIEPuGz1CyNnDSs/FEAvvnbqKuwL9HS5sDMDkvpFK4NN5fFi3Yp/77RA3tagbqs7uZuyJvviusdXs8d4MvucuZzRRlDw9IRZbiy4yWcbsza3hCys7ykciLuxRxvkkQAWAf/81BaHB4nJSz5wdjzUPD8WoLWypLJxxbWfOjsfalGF2pxTwRli3Yj+ouNKdGsRN+O6IeAD9hNP7QtA/VMztd+YUVqO89qbNc3uDpcER+Pr7APY5NlrzeXlmQgzCQwIF4bxnD444yhKega8DOl9/wbTJsaJTevTcFeiPZUns/o7OuLZZaiz6Eqy7FVdUHS5uiTG+PSpuxJ6HCp9JSuyWBmdgSSmxhL2OjeaKFD49IQbvlNUJxnnPHnwtas1b4OOAzsdf0Buc1/U5tVjredG17VxYt2JZazA6C99+UroRex4qrJPU+MFhop+gnIVeKRkd3c+2MOyPwOq5smtqvY09paZjZc2SJ0R8MWrNW2CtGM7q0Kz3F/QWHhsdxSTnqKMzYczCMYNsykglbHLOhBQfN2HPQ4X1M0MjQmwL+RAB/lLMSlAwyUb1cywCy5u2hzwZtdbZ2YmXXnoJsbGxCAoKwuDBg/Hyyy8b8n8B3Znbs7KyoFQqERQUhKlTp+L8+fNG51Gr1VizZg0GDBiAPn36YN68ebh27ZrT2ytEbG2z8HFo9jZ/wdj+bNfse6euimahInRyCqtx/+bPbMp5YreCFB83Yc9DxZfCp50Na3LDfxyvQVFVg93f403bQ57MR7J161a89dZb2LFjBy5cuIBt27bh9ddfN4rq3LZtG3Jzc7Fjxw5UVFRAoVAgOTkZra13nP/T09NRUFCAQ4cOobS0FG1tbUhNTUVXV5fT2ywm+CZA9TZ/wYVjBjFvgYvJSitULPmz9sSTucBI8XET9jxUGm7ZdvjyxknKGbA6Oze1qvHH/G/sVn68aXvIk5may8vL8cgjj2DOnDmIiYnB//zP/yAlJQWnT58G0G3t2b59OzZs2ID58+cjISEB+/fvR3t7Ow4ePAgAaG5uxp49e/DGG29g+vTpePDBB5Gfn49z587hyJEjTm+zWODr0OyN/oJ8gx/EYqUVIizXmwTAt5tmeGw7lcLZ3QTfUOicwmr84+s6m+f1xknKWVjKrdETDt034eZPqpEcr4Afz3of3mSV03TqEB4SiHhlCKqvG6dQcHU+kokTJ+Ktt97C5cuXMXToUPz73/9GaWmpIR9YbW0tGhsbkZKSYviMXC7HlClTUFZWhrS0NFRWVkKr1RrJKJVKJCQkoKysDDNmzDD73Wq1Gmq12vBaX1hVq9WazYmlPyaWfFn55XWQSa3beuS/vv9MYhTSk+NE0zdb9ByrdclxkHJd2FtWz2T5yi/7j9MXlZpOHT6ouIIrqg4M6heEhWMG8Z6/hX79sVxvAPB/TtUaft/efXJ130jxcSOsSa5YV2jLJ8Z4lQOiK8icHY+J/3UPFr9zyqIMB6Ch+TZO1d5E4hB+2SS8JamhufxSEgDxkSGY/9BAl+cjWb9+PZqbmzFs2DD4+fmhq6sLr776Kn7/+98D6C5qDMBQyFhPREQE6uvrDTIBAQFGhY31MvrPmyMnJwebN282OV5cXIzgYMsKa89agkKmP8BceX1oVx0KC+tc2RyPoB+rYQC2Mv4WUFWj0AVbXv1//YMKOFJs//mFev0xX29mfl99n9rbXWshJ8XHzbAkuWKN5lKGujcEUKzcbNcwyV1XtQM802h5Q1JDSwntOADnG1oxsfW2y9v/wQcfID8/HwcPHsSIESNw9uxZpKenQ6lUYsmSJQY5icTYIsdxnMmx3tiSyczMREZGhuF1S0sLoqKikJKSgr59+5rIa7ValJSUIDk5WfDV53OLLzJZOORSDltG60TRJz6YG6sD5XXY+tklps8vS4o2hMQ7Qm7xRbxTVu+U7xH69cf6+66fcZ+Rxadnn/RWV1dBio8H0EdfWMKb/EaEQHhIIJPcun9+i8tNrbytaGJOV88albY2ZZhLlZ+//OUveOGFF/D4448DAEaOHIn6+nrk5ORgyZIlUCi6o/QaGxsRGRlp+FxTU5PBCqRQKKDRaKBSqYysPk1NTUhKSrL43XK5HHK53OS4TCaz+mCx9b6nySmsxtvHr4Ali41+h1fofbKXnv16MmkIXjl8mWlxufP4Fegkfg7dw5pOHd4uvQIdZ3kc3i69gj/PGMHrHhPqWLH8vlJJt5ysV3/1fXJ1v4S7DPVhvMlvRAiMjQ1DZGigzemfcyDvjqU8KmtThmHP8RrBZnMWSlRae3s7pFLj6cjPz88Qzh4bGwuFQmFk3tdoNDh69KhBqRk1ahRkMpmRTENDA6qqqqwqPt4IX4fmpYnsGY7FjrsdnYVyj7kLTwZJsEIWHxfDtyAp0B16ueXTC1ZlxOA3IhT8pBJsmhuPP+Z/wyRvr4WjtyXPnrps7kYo1sW5c+fi1VdfxaBBgzBixAicOXMGubm5WLZsGYDuLa709HRkZ2cjLi4OcXFxyM7ORnBwMBYtWgQACA0NxfLly7F27Vr0798fYWFhWLduHUaOHInp06e7tP1Cg0+G5mcnxyIjOQ6FhTUub5dQ6FnI1NbPpFdKrFnprSGUe8wd6J93HVodxg8Ow8mam0a/r1Cs4KT4uBB7Hnz6z9jC0xqz2JiZEImdTz6Ev/zff6P1tvWcLo5OdABbsc91yXF2n99ZCMW6mJeXh40bN2LlypVoamqCUqlEWloa/vrXvxpknn/+eXR0dGDlypVQqVQYN24ciouLERJyJ4Hnm2++CX9/fyxYsAAdHR2YNm0a9u3bBz8/P5e239XwXUCxPkT1GZqFGiHkSjJnx6NN3YX3GJI6Hq5qsNvBXyj3mKsx97yTSoDxMWGIU4QIqmgrKT4uwp4q1yxVk4WiMYuRmQmR+Pr7n3HghGUnQz2OpK5n9Zt57rdD7P4OZ6Dp1KGry7ZZwB3WxZCQEGzfvt0Qvm4OiUSCrKwsZGVlWZQJDAxEXl6eUeJDsWPPAuq7xlazx3vjbRma+TJ4QB8mudP1tzBs42G75l5vify0hrXnXXntTdwfFerQQtLZeF718kJcVZDU00mfvIEYN6SuZ93T/6CCrXyAK8gprMawjYeRXXTRpixZFz2HpQy41urA5RRWo7z2ps1zi/1h6wxYM7wD9tfeE4PPiyOIsWyPOH9pgeOqgqQcPPuw9Ab4THT2OjqzbjNcUdnOzO0KWNLJA55NKU+4bgGlR8wPW2fB19EZsO8hnjk7HmmTY03mHqkEeGZCDMJDAgUbAGELMTpv01aXC3BlQVJvcIDzJCx5d3pij6Mz6179oH5BgIr5tE6B1bL4wqxheHoCPRg9CZ8Hin4bgdWpefzgMFJof4Ulw3tP7PUBNJfD7XpzB975uk7QARC2EOOzi2Y1F0AFSYWNfvXFYvixZ6XCYlWSAJj/0EBe53UGrJZFf6mElB4P48oF1NCIENtCPoQ+HcXo6H62hWH/Q1wf+fnyIwloar2NPaWm96O9W2qeQozPLprZXIA9BUkXJ8bYfBDTnrzzyJwdj0XjBjHJ/vPMNV7nZjGfcwASX/uc13mdgRhXZ74KLaDcS4C/FLMSFEyyUf0c+/3E6BdjCXued56GFB8XYI8z2xvF5GTqblgjOqqvt/JefVna0++JfqWXyzD2zoIejOKBFlDuh9UH8B/Ha1BU1WD394jRL8YSYnTeFk5LvAxrzmy9HUb1zqbW7gNyMnU+fByd7XVo/HaT+YrgPdlXXu+2lV3DLdsO1fRgFAa0gHI/rM7OTa1q/DH/G7uVH2+zvPJ53gkBcm52ISwFSVlMnlIJsNYJhfIIY/g4Otvr0MgSheeMhIks5BRW4x9f19mUowejcOBTB44lD5gQH0JCg8XZmUO3n97mT6qRHK+AH+sK6le8yfLaM1vzCzOGgZMAP9zqEFTCwt6Q4uNibBUktSdyg3AembPjUfrdTzjfYDvh2z/PXOM9BkJZ2bGGOS+fGEMPRoFBCyj3kzk7HhP/6x4sfueURRkOQEPzbZR99xMm3XcPr/N7S1JDS9maV0yKFfTzSniqmI8hlAejL8MaXWWPr49QVnasYc7K0CCXtoOwj57RQMsnDTZZRXuTz4hQuNmuYZJ7au8pn0xqaE9yTaEg3F9VZGg6dXZV4RbKg9GXcaWvD2toe6eOc6mfDynY3g2Nr/MJDwlkkuNg34NezEkNxR6VRltdTsCRKtzeYvIUM6709WE5Nwcg5/BFbC266LLEZffezWbJIQVbnNACyvmMjQ1DZGggGptv26zgDtiX7FSsSQ3F7qJBFh8HcdTcF+AvxdMTYqzKCN3k6Q1kzo7HiEi2pG58V80soe2A60zEOYXV2MpQk4sUbGFgj/V44RjbOalofPnhJ5Vg01x2RcPerUQxJjUUu4WRnqYO4AxzX05hNfZaiLQRaiigt8Lq6zPQDj8YfWbYF2fadi51pomYtS4XQAq2ENAXj93y6QW8W16PLZ9ewLCNh60+8HIKq3H/5s9snpvGlz8zEyKx88mHECzzY5J35EEvpu0jsVsY6S5wAEcdCm09lJYlUYSNO2H19ckpumjXyivAXwo/P9tf4CwnVNZILglIwRYC9lZit6XY0gLKMWYmROLPyUOZZO1ZFOlhfZ54ulC1plOHri7bKykhWxhJ8XGAmp9+YZIztwpgeSi9U1YnCO3eV2BNXmavMyPAviKs+bGN97l7wxrJlTlzGD0UPYyrKrFLAHy7aQaNr4MsSXLtoghgnxuuqGwnIXUVeotkNsPWuZAtjMJslQjIKazGwZNsmrc5cx+FnwoTVn8cwD6zM6vp971TVx3ez2edSK81e24iJbqxZz5gLTjraQuBN+CORRHr3DCon2dSTrBum4vBwugSxeeHH37Ak08+if79+yM4OBi/+c1vUFlZaXif4zhkZWVBqVQiKCgIU6dOxfnz543OoVarsWbNGgwYMAB9+vTBvHnzcO0av2KRroKlxIQeS+a+D79h64tQncO8mczZ8VjP4Ivjqsrtehx1ZqRILvHgykrsNIc4B1cvilhrs7E4sjsbVuti5qxhuLhllqCVHsAFio9KpcKECRMgk8lw+PBhVFdX44033sDdd99tkNm2bRtyc3OxY8cOVFRUQKFQIDk5Ga2td7Lnpqeno6CgAIcOHUJpaSna2tqQmpqKrq4uZzeZF6x+E3rMmfs0nTpUM2QKBuih5Cl+YKhpBbimcntP7HVmpEgucUGV2MWBKxdFLHPDcEWIRyx4rNZFf6lEsNtbPXF6Hp+tW7ciKioKe/fuNRyLiYkx/J/jOGzfvh0bNmzA/PnzAQD79+9HREQEDh48iLS0NDQ3N2PPnj04cOAApk+fDgDIz89HVFQUjhw5ghkzTAs/qtVqqNVqw+uWlhYAgFarhVartdhe/XvWZHqSX14HmdS2rUcC4OmkaGQkx5mcO7+8DgF+bOd4fPS9zG3Tw7dPYsDdfYrpJ4ecYYz+c6MFWz89hwwepQDWJcdBynXh4Ik6AIDcxvX0p/cq8P8t+A3zhJJbfBH7yuohYxBflhQNCdcFrdbxBYW5MfKma9CV2JPPa3FiDF759IJVyzMpts6Hz6KIbw4bW3XCzje04vumFmwb232fr58zktf57cXbrItOV3w+/vhjzJgxA4899hiOHj2Ke++9FytXrsSKFSsAALW1tWhsbERKSorhM3K5HFOmTEFZWRnS0tJQWVkJrVZrJKNUKpGQkICysjKzik9OTg42b95scry4uBjBwbZXPCUlJUz96w9g21gmUaCzBoWFNQ6d40hxEeOXmcLaJzHhrj45Y5ytMQzAy6O7/79ltC2LTiOv62AYXNt2W/Qco/Z2cUyEnoYl0SVVYhcGrBY0fYkbvts+PZMa/vPMNVRfN7878E5ZPXQSP7dsK3mbddHpik9NTQ127tyJjIwMvPjiizh16hSee+45yOVyPPXUU2hsbAQAREREGH0uIiIC9fX1AIDGxkYEBASgX79+JjL6z/cmMzMTGRkZhtctLS2IiopCSkoK+vbta7G9Wq0WJSUlSE5Ohkwms9m/A+V12PrZJZty62fcZ3Gl5YxzWINvn8SAJ/qUW3wR75TVM8naM1b5Zf9B2K1L2HhaCrWOzfFneEQI5v1GiYVjBhk90HKLL2JfeT1TFBcArJs+FEsnsm+5sWBujPSWV8I2VIldHLBY5/TYk80Z6FaE9d/jivPzpYHByiUm66LTFR+dTofRo0cjOzsbAPDggw/i/Pnz2LlzJ5566imDnERiPNFzHGdyrDfWZORyOeRyuclxmUzG9KBklXsyaQheOXzZpkn6yaQhkFm4GK83a6Dust5XW+dggbVPYsKdfVo/ZySOfa9iqtz+f882YNnkOF7nXzA2FkeKL0Gtk9i8HvScvd6Gs9cv45XDl7EsKQaRdwf1WBUyek0DuNqicdnv2HOMvO36czVUiV34uLLETU+EUhYip7Aa/7CQZLcnYrIuOr2VkZGRiI83XmUMHz4cV650O2QpFAoAMLHcNDU1GaxACoUCGo0GKpXKooyncLSqrjdeRN6MKyu3OzK+Og74x9d12PLpBYumcGuIxSTti1AlduHDp8QN3wAIPULwq2EN5lk+UVzJdp3+ZJ0wYQIuXTLexrl8+TKio6MBALGxsVAoFEZ+ABqNBkePHkVSUhIAYNSoUZDJZEYyDQ0NqKqqMsh4EmtVda2Zl731IvJm+ISf7zpmXwTWsqRo5u9wBmIySROmCOGBSLh2UQQIw6+GNQmq0oGM1Z7A6YrPn//8Z5w4cQLZ2dn4/vvvcfDgQezatQurVq0C0L3FlZ6ejuzsbBQUFKCqqgpLly5FcHAwFi1aBAAIDQ3F8uXLsXbtWnz++ec4c+YMnnzySYwcOdIQ5eVp9LWXNs4ZjqcSo7FxznCb+Qu89SLyZviEn3MAnnrnJO/vyEjpzn2RPDyc92ftgayJwsCeYqSAMB6IBL9Fkavy+kjg2rw+3qpkO332GzNmDAoKCvD+++8jISEBW7Zswfbt2/HEE08YZJ5//nmkp6dj5cqVGD16NH744QcUFxcjJOSO6fDNN9/Eo48+igULFmDChAkIDg7GJ598Aj8/tmJx7sCWSbo33noReTt8zNonam7aZfUJ8Jfib0+McqnlRwwZVX0Fe4qR6mFNdEdWPdfCZ1Hkqrw+HID7N3/msqrt3poE1enOzQCQmpqK1NRUi+9LJBJkZWUhKyvLokxgYCDy8vKQl5fnghayo+nUWXU05AOt1MTL/IcG4vyn1iMs9NjrbMjHaZIPEgAvzBqGpyeQpUcIWIrI0hcjBWBVOQ3wl+LpCTHYU1pnUYaseu4hc3Y8Sr/7iSkAwp4Fra28PgD7dcOXnMJqJtcMMSrZdGdYwZFVmTm8LSTQl1icGMMcM+WIxY5PWnxWnp0ci7QpQ+hBKADsKUbam5zCauy1ECBBVj33w+rrY2/l9szZ8fh2k2nuut7Ym+XdHKx1uQBxKtniaq0bsTTweu2ar/JD0VziJsBfivGxYUyyl2/wj7LqSU//sXgl2xabOeghKDwcjciy9UBalkSBEe6G1dfHkcrtLGUqnBXJxxqEI4F45xd6wprBGasyvucDKJpL6OxfPo5J7kTNTYf33PX+Y4XPTeZtARoRGcLkbG8P9jrkEt2/3eEq8wlYe2POasgyj7xTVkdj4mbcUbmd1Yp8uKrB4fFnDcLJnDlMtM8rUnzM4Ow8GRTN5R0E+EuRNpnNmdGZZmdzEYTLJ5quMvUWnk//NJnJ2Z4vzt769SX0v93pepVtYZj386P8PcLF1ZXbWf0+T9ffcvieZFWyrjWz1SwTIqT4mMHZ0VcUzeU9ZM6ORyLDlpezH0C9Iwg3po7gnU7BEZy99WuOH374AU8++ST69++P4OBg/OY3v0FlZaXhfY7jkJWVBaVSiaCgIEydOhXnz583OodarcaaNWswYMAA9OnTB/PmzcO1a/YlkHMWfPwlAMt+fh9+w9YPmkc8gysrt/MJnXf0nvTWSK6ekOJjBmdHX1E0l3cRp2Dzu6n5sc2l7eCbTsFenL31aw6VSoUJEyZAJpPh8OHDqK6uxhtvvIG7777bILNt2zbk5uZix44dqKiogEKhQHJyMlpb7/hUpaeno6CgAIcOHUJpaSna2tqQmpqKri7Hq8/bA+s2d0/M+flpOnWoZogcAmge8SR8KrfzgU/ovB577smcwmpsLbJd/FbsQTguCWcXOyxF6PgM/MIxg7DFRii02C8kX4L1wfLeqau4K9BftPvgeli3WD6ouIL+dn7H1q1bERUVhb179xqOxcTEGP7PcRy2b9+ODRs2YP78+QCA/fv3IyIiAgcPHkRaWhqam5uxZ88eHDhwwJDoND8/H1FRUThy5AhmzDAfGaNWq6FWqw2v9YVVtVottFqtibz+mLn3epNfXgeZlM3UI5UASxOjkZEcZ3Lu/PI6BPjZPo8EwOOj72VqW0/49ElMuLtfMf3kkDOM039utGDrp+eQwaOe2rrkOEi5Lrx/sg4AIGe4rvYf/565GHFu8UXsK6uHjGH9tCwpGhKuC1qtcxYUvcfJ1eNFio8ZWPKpsEZfseZCoGgu8cCnOrMr8mu4G9atkyuqDrsVn48//hgzZszAY489hqNHj+Lee+/FypUrsWLFCgBAbW0tGhsbkZKSYviMXC7HlClTUFZWhrS0NFRWVkKr1RrJKJVKJCQkoKyszKLik5OTg82bN5scLy4uRnCwZSW3Z0kdS/QHsG2sTbE7dNagsLDGofMcKS7i8YXGsPRJjLirX7zG28JYW2MYgM2juv+/ZTSDNaflAgptVHjveW5Xtp0F/Ti1t7t2u5YUn185UF6HOpXakKTQUuIoqaRbSWF5kFlKVNYTPucjhAHfRIO7j9dibcow0Sq23zWybbEM6hcEsPnumlBTU4OdO3ciIyMDL774Ik6dOoXnnnsOcrkcTz31lKGoce8ixREREaivrwfQXfg4ICAA/fr1M5HpXRS5J5mZmcjIyDC8bmlpQVRUFFJSUtC3b18Tea1Wi5KSEiQnJ9usPn+gvA5bP7tkVQYA1s+4z6rF11nnsQSfPokJT/Qrt/gi3imrZ5K1Z7z0fdp4Wgq1js3xZ2x0PwyJCMGgfkFYOGaQ0VyUW3wRe8vqwWKXXDd9KLMFiQ+9x0lvdXUVPq34dGi68PjOciyLAbZ+dgnqru6L6NXCCwZlZG3KMLsyN7Ps7UsAfLtpBu4K9OlhECV6RXXXsVqbE4bemdGebM6eRtOpw8m6mzbl9DWDjhTb51Cp0+kwevRoZGdnAwAefPBBnD9/Hjt37sRTTz1153skxhM9x3Emx3pjS0Yul0Mul5scl8lkVh+Wtt4HgCeThuCVw5dtbps/mTQEMivzyvVmjWF+cuQ8tmDpkxhxZ7/WzxmJY9+rmLI5/9+zDVg2Oc6u79FyEpvXhJ7jNbdwvOYWAOCVw5cxLiYMcYoQXL7RihM1NwHG9KxXWzQu/R314+TqsRLnEtQJrHi3AsP/WoSLTaaaZU+veHsdSFn8IjiwJaYihEnm7HgsGsdWIFCskTasqRjGDQ5zyKIVGRmJ+Hhjq+fw4cNx5Ur3/aFQKADAxHLT1NRksAIpFApoNBqoVCqLMu6GxSnV1jY3JT8VH66u3A50+4PZg44Dymtv4t3y+l+VHna8xXHeJ++SFe9WoKS6yaacI5EqFMLuGwwe0IdJTqwTBmsI9dAI+zNMA8CECRNw6ZLxVs7ly5cRHd09ucfGxkKhUBj5amg0Ghw9ehRJSUkAgFGjRkEmkxnJNDQ0oKqqyiDjCSzleGHJrE3JT8UJn/DzXcfse85kpAxD2uRY5lI6juJNATg+p/h0aLqYlB7AsVwsFMLuG7BOcNdFmOzLnSHUf/7zn3HixAlkZ2fj+++/x8GDB7Fr1y6sWrUKQPcWV3p6OrKzs1FQUICqqiosXboUwcHBWLRoEQAgNDQUy5cvx9q1a/H555/jzJkzePLJJzFy5EhDlJenMJeEkiXvEiU/FSd8ws85AE+9c9Ku78mcHY8XZrFHhjmCN1kUfc65JJunWdFeiwyFsPsGrI7Oe0rr4C+ViGpVfqC8jsnhUYJfr2PO/tDWMWPGoKCgAJmZmXj55ZcRGxuL7du344knnjDIPP/88+jo6MDKlSuhUqkwbtw4FBcXIyTkjrXpzTffhL+/PxYsWICOjg5MmzYN+/btg5+fn91tcxb6bXM+kOVYvPCp3H6i5iY0nTq7FIunJ8Ria9FF5gSZfJGgu9CxmOYuW/ic4lP3M78Jwp6VLIWw+xaZs+PR1cXZ9MMQW3QX68N0uDIEAf5Sh3N6pKamIjU11eL7EokEWVlZyMrKsigTGBiIvLw85OXlOdQWR9B06uwKiDAHWY7FzfyHBuK8jQWwHnsDIPhGmfJBAuBclvcF4IhjBnYiMf3ZJwh7LDIs6emparb3EcmQ5l1sdZRYH6a/e5DNkdMXcHY9s8WJMTZ9OMhyLFxYxk+PI1Y7PrXC+PDs5FivU3oAH7T4vDg7HgdOsEVS8bXIUAi77+KNWxINDOn36aF7B0t5u/RRogD/RJZvFNsuH0CWY+ES4C/F+NgwlNfajp66fIPNn84SvdOv3AlV54+355fzubslKMAPyfHhNuXsschQCLvvwmodcXRycxeaTh3eKauzKff0BPu3cbwJV9Qz0ytS1qYUshwLn/3LxzHJnai56XCx357pVw49m8jLCpQYG+aWgsdCwCdnrN1PjbGo/AyNuAuXX7Fv0Kl6su/CGt3ljMnNHVA0ET9Y65mxbnWyKFJSCbCWR60nwjME+EuRNpktwsvRYr+96R1NOH5wmMnWm9714v20RJcXPBYKPrvfsvupMejQdOG1wioAdXh8TBRemJ2AoAD7oj+oerJvw8fBUO/k7K78G/bgjVt3rsTZvxcfRUqMGcF9jczZ8fj2arPNLS9XjGnvaEJnOt+LFd/qbS+CAvzw0pxuy85Lc+LtVnoAO0J/Ca8jc3Y8EmPDbMqJwcmZtT4XKfHdODv6ihRP7yNOwZbk09Vjam81Am/C93rsImp++oVJTh/6S3gnQpncHIFPfS5S4rth2erk4wh+L0OUIECKp5hgHauBtH3scugJ7ARyCqtx8CSbwzKF/no33uDk7K76XN6EM2py6ckprMbWItvRXBRRJy5Y/QBzii6Kwg+QD5pOHfYcr8Ff/1WFPcdrnOrHZA8+6+PjLCyFsJqDJirvZ3FiDF4tvGBTcThRcxO5xRchRNdUd9XnEjsHyutQp1Ib/CT0ARG7jxvn8eITGsxnPqEwdnHB6gfIwf70B0JEn9C35z3xauEFj4bLk+LjAKwFBPXQROX98HFy3ldej9fGuKFRPCAnfdvoFdatn12Cuqt7Cd9zIu+ZS4WP8yjrfOKNJQR8BUvKsTnElundHK7IbeUMxPuLCgDWLQEJKN+GL8HHyVlokJO+dXIKq/FOWb3Jcf1EnlNYbbfzKOt8kjlzGM0lIiZzdjzWz7Rt6xVDEIQ1NJ06mwtAZ4fvs0KKjwOwOqguGhtFE5WPwerkLDT41ufyJVyRpLAnrL/9tWbbGbUJYfMDQ1Z0QNhBELZYssd2xXlPKXe+NXM5GVZT/+B77nJxSwihwWcbyNOOfj2h+lyWcXaSwt5QQVLfwRuCIKyh6dThBEOZDsAzyh0pPg7g7BBWwnvgU5xQSCVMqCimZVydW2fhmEE2ZXz1t/c2vC3Te29Yt8wBzyjypPg4yFgbvhzk0OybBPhLER/Jtt11RSWcrQsqimkZV1pkcgqrcf/mz2zK+epv722wpD/Q4yk/GEfgo/x7QpGnO8hOcgqrMWzjYYvVb/X1T8i3x3eZ/xDbdtCgfsJIWEZFMa3jKguv/ne3to1G84n34U2Z3nvDqvyP91AuMJd/Y05ODiQSCdLT0w3HOI5DVlYWlEolgoKCMHXqVJw/f97oc2q1GmvWrMGAAQPQp08fzJs3D9euseUXcTW2JqrE2DCvr25L2IbVnM2qILkSKoppG2cmKdTD8rtLAHy7aQbNJ16IN2R6N0cDg/O2BMC7y9gq1zsblyo+FRUV2LVrF+6//36j49u2bUNubi527NiBiooKKBQKJCcno7X1jiNXeno6CgoKcOjQIZSWlqKtrQ2pqano6upyZZNtwjJRsaT7J7wfVnN24mufe3wf39WOu95C5ux4LEuKNjlur0WG5XfnICw/MMJ5eKNDu6ZTh3fK6mzKLZvoueKoLvvWtrY2PPHEE9i9ezf69etnOM5xHLZv344NGzZg/vz5SEhIwP79+9He3o6DBw8CAJqbm7Fnzx688cYbmD59Oh588EHk5+fj3LlzOHLkiKuazAQ9IAg+ZM6OR9rkWKuWn545YDwFFcVkJ+NXq9f6GffhqcRobJwz3G4LL2uWbPrdvRNWq/B1EaUwYM1HpfRgTTKXZW5etWoV5syZg+nTp+OVV14xHK+trUVjYyNSUlIMx+RyOaZMmYKysjKkpaWhsrISWq3WSEapVCIhIQFlZWWYMWOGyfep1Wqo1WrD65aWFgCAVquFVqu12E79e9ZkelL3UyvkfrZH9erNNuZzOhu+fRIDYu7TuuQ4/HFyLMblfG50XC7ljP59t6wGz/12iEdWQbU3mpmu65h+cotjYG6MxDherCxOjIFMJrP785Qlm2DN9L6ntA7+UokotjvFsIhyieJz6NAhfPPNN6ioqDB5r7GxEQAQERFhdDwiIgL19fUGmYCAACNLkV5G//ne5OTkYPPmzSbHi4uLERxse9IoKSmxKQMAo6XA6LEskrUoLGQvZ+EKWPskJsTcp20Wrpsto+9EbBwpLnJTa4z5n4juP5uoqlFowzLVc4za28lSYQnKkk0A3Vbhri4O//i6zqqcvoQFa5oMT/Fdo/CVeacrPlevXsWf/vQnFBcXIzAw0KKcRGI8fBzHmRzrjTWZzMxMZGRkGF63tLQgKioKKSkp6Nu3r8VzarValJSUIDk52erqLbf4otlU9eaQSoDTG5I9tn/J2icxIfY+vVp4Ae+fMvbTkEs5bBmtw8bTUqh13df178cOwobZw93WLk2nDqNfLWEyTS9LijZs85jD3BjpLa+EKTU//cIk54tZsn2NyLttb/voXSieGh/lhhbZR05hNcoZEhd6Oh+V0xWfyspKNDU1YdSoUYZjXV1dOHbsGHbs2IFLly4B6LbqREZGGmSampoMViCFQgGNRgOVSmVk9WlqakJSUpLZ75XL5ZDL5SbHZTIZ04PSmpymU4edx68AjLp22uRY9AkybYu7Ye27mBBrn6LC7jIUtOyNWicxvBcVdpdb+/fuiRp0dNq+rscPDsP6OSOZztlzjMQ4Vu4gp7AaB0+yOSz7YpZsX4N12+efZ64JVvFhqc2lx9P5qJz+zdOmTcO5c+dw9uxZw9/o0aPxxBNP4OzZsxg8eDAUCoWROVyj0eDo0aMGpWbUqFGQyWRGMg0NDaiqqrKo+LiSvV+zb1lRrg3CHCxOjBKwZe91JqwT7tAIcdYeEyIs+ZL0eHplTLgH1m2f6uutyGVIMuoJWGpzAd2LKE8/I52u+ISEhCAhIcHor0+fPujfvz8SEhIMOX2ys7NRUFCAqqoqLF26FMHBwVi0aBEAIDQ0FMuXL8fatWvx+eef48yZM3jyyScxcuRITJ8+3dlNtsk7pWyKz0OD7vb4gBLChCW0nQNw/+bP3Brd5Y3htEKGJR1GTzy9MibcA2t0FwDsK2dzuXAnfGpzCWER5bKoLms8//zz6OjowMqVK6FSqTBu3DgUFxcjJOTOD/Lmm2/C398fCxYsQEdHB6ZNm4Z9+/bBz8/PrW3VdOpwo1VtWxBgvnAJ30SvFO8+bjn5pT60vae8K2FJNEZWB+fBGuorAfAsWY99BtboLgBM14+7EXptrt64ZSnx1VdfYfv27YbXEokEWVlZaGhowO3bt3H06FEkJCQYfSYwMBB5eXn4+eef0d7ejk8++QRRUe7f2+STjydluMJ1DSG8gszZ8fh2k2k6ht64oz5PTmG1zUgSgKwOzoR1a3HR2ChSenyMzNnxGMFY309oCL02V29oNrMBa4IxAFg6ka3oHOHbsGThdXUSTNYtl+UTY+gB7ERYV7uD77nLxS0hhAif8jVCKlwq9NpcvfF8CwSMplOH84wJxoQyoITwEUKCLzFkV/VGXFXolPAOFifGMOfpEVIZE5Z2e7I2V2/oSW0FVi91QDgDSggf1tXR5RtsSrc9CEH58lXG2qjITVuLvkuAvxTxjNtdV1TCKWPxBkOk2bOThXNdC6MVAoQ1ERMAxFOCMYIHrBEcJ2tuusycTdFc7iensBrDNh7GiRrz84q9hU4J74J1u2tQP2FYY1nSMwjtuqantRn4hpxSgjGCDwH+UoyLsb7qB7rD213l58OaL8jdeYX05OTkGFJf6OE4DllZWVAqlQgKCsLUqVNx/vx5o8+p1WqsWbMGAwYMQJ8+fTBv3jxcu8bup+cq9A8HS9uLibFhdhc6JbwL1oWRp+7NnrA8K6USYK2VjO+egBQfM7D6PwC0H0/YR5yCzZz9zzOueWiz+gd4wo+goqICu3btwv333290fNu2bcjNzcWOHTtQUVEBhUKB5ORktLbe2RJMT09HQUEBDh06hNLSUrS1tSE1NRVdXV3u7oYBlofDyTo26zLh/bDk/BIKLM9KVwdq2AMpPmbgE8lF+/GEPbBuIV243uqS7S7Wa9zdPj5tbW144oknsHv3bqNyNRzHYfv27diwYQPmz5+PhIQE7N+/H+3t7Th48CAAoLm5GXv27MEbb7yB6dOn48EHH0R+fj7OnTuHI0eOuLUfPRHrw4HwHJmz45E2Odaq5Wf0qyVuTXZqDrH6CnokgaGQ4RvJRaZpwh4WJ8bg9SLbk5Z+u2v5pMFO++6cwmrma9zdPj6rVq3CnDlzMH36dLzyyiuG47W1tWhsbERKSorhmFwux5QpU1BWVoa0tDRUVlZCq9UaySiVSiQkJKCsrAwzZpjPn6RWq6FW30lSqi+sqtVqodVqTeT1x8y9Z466n1oh97NtQr56s435nM6Gb5/Egpj7tS45Ds/9dgjS8k+jok5lOC6Xdl9LMgmHfV/XQMp1WS0e7Epi+smZru2YfnKrY9B7nFw9XqT49IIiuQh3EOAvxbCIEAC3bMo6c7XEx3/N3du4hw4dwjfffIOKigqT9xobGwHAUMhYT0REBOrr6w0yAQEBRpYivYz+8+bIycnB5s2bTY4XFxcjONiy4tezlqA1RkuB0WNZJGtRWMjuW+gKWPskNsTcr8fCu/96s2X0r5bgzhoUFta4t1G/0h/ANpZrW1WNQgbrlH6c2ttdayEixacHFMlFuJN5v1ECqls25ZwZ1s7Hf82d27hXr17Fn/70JxQXFyMwMNCinERibPvnOM7kWG9syWRmZiIjI8PwuqWlBVFRUUhJSUHfvn1N5LVaLUpKSpCcnGy1+nxu8UW8U8ZWV0kqAU5vSPbYnMLaJ7Eh9n4dKK/D1s8uGR2TSzlsGa3DxtNSqHXd1/X6Gfd5xNeU5RpflhRt0yLVe5z0VldXQYrPr2g6ddjFUCdFD0VyEY6ycMwgHCm2vQo6UXMTOYXVTtlWZbUexStD3LqNW1lZiaamJowaNcpwrKurC8eOHcOOHTtw6VL35N/Y2IjIyEiDTFNTk8EKpFAooNFooFKpjKw+TU1NSEpKsvjdcrkccrnc5LhMJrP6sLT2vqZTh53HrwCM6ejSJseiT5BpG9yNrT6LFbH2q06lhrrL/DWk1kkM79Wp1G7vn6ZTh7dLr0DHWb7GJQD+PGMEZIwKvX6cXN0XMln8yh8OnGYuskaRXIQz4LO6d1btLlafHXcr9tOmTcO5c+dw9uxZw9/o0aPxxBNP4OzZsxg8eDAUCoXRloVGo8HRo0cNSs2oUaMgk8mMZBoaGlBVVWVV8XEFe79mX0QJLccJIRxY79eBHsiwzmI9dmVKDkcgi8+vnKpXgXV1RpFchDMZG90Px2tuWZXRR/046uQs1GrsISEhJoWK+/Tpg/79+xuOp6enIzs7G3FxcYiLi0N2djaCg4OxaNEiAEBoaCiWL1+OtWvXon///ggLC8O6deswcuRITJ8+3a39Kam+wST30KC7SekhLLI4MQavFl6wqWDkFF3Ez+1qt15LYo3oAkjx4b2KpkguwtkMiQixqfgAjk8gmk4d9pTV2ZR7ekKMIBX7559/Hh0dHVi5ciVUKhXGjRuH4uJihITcyYn05ptvwt/fHwsWLEBHRwemTZuGffv2wc/Pz61tvco4ViyJ6gjfRZ/T520bbhgcYJBx1/NJzNnffV7xee9kPSJsiwHonqQokotwNqyp5x2dQF788FtwIipM+tVXXxm9lkgkyMrKQlZWlsXPBAYGIi8vD3l5ea5tnBU0nTrcaFXbFgSQMlzh4tYQYkevyOw+bjnzt57dx2uxNmWYWxYuQrUesyC8ZZ2b+fJiE7MsbXERrmDhmEFMK//rzfYXJezScSg8ZzmkuydCNE2LCT4pMZZOFEeGXsKzZM6Ox/qZtnP1uCsRZk5hNf7xte3vEeozU3gtciNttztx7odmJtmIEDltcREuIcBfimVJMTbl9n5dZ7eD86nam2jXspVtEKJpWixoOnWUEoNwCT8wWFgA15W50cOaC2z5xBjBPjN99q6bt+M4ErI+g5YxqcnyCbQyI1xH5N22t5ccWc394/h/mOQkAjVNiwU+1h5KiUHwgXVBUn291aWlLFhzgQlly9wcPqn4zNtxHN9e45cgiUzShCtxZYSEplOHLy79yCQ7Z2QkWSHshE8CVIAUTIIfrFXbAeelvzCHUOv88cHnZri22528lR4ySROuxpUREgfK65icmmVSCf7X4w/yPj/BPwHq+MFhNKcQvOBTtd1Vvj6aTh2qBVrnjw8+d+f9+YMzvD9DJmnC1bCs5uyNkGBdeSX9V3/4UXy1XSzZc5I5AaoEFB1K2Efm7HgMjwixLQjXWFwOlNcxXecSCNui6XOKzxUVv8gYoYbjEd4Fy2pOxwFvFF/kfW7WldfkuHt4n5vgv8X17GRhRroQ4mDOyEjbQnBNNmdWZWq4wHdJhNsyF8GaM0WPUMPxCO8jc3Y80ibHWs0f/vaxWt6Oi2LOtyF0+FS7BygBKuE4Vkpj2SXHh+8a2ba5hL5L4nNP9DcXsvkwSEA1dAj3s9ZGFWOAn+Oi2PNtCB0+1e4pASrhDK4332aSYw1/Z0XTqcPJOtuWTaFvcwE+qPjcFeiP+wf2tSoTGSrHpVdmkdJDuB2WPXRWx0VvyLchdFgjXABSLgnnwLprcfkGm3WGFVYlf5wIHPeF3ToX8fHqSRaVn/sH9kV55nTBDxzhnTgzrN0b8m0IGT4RLrTFRTiLhWMGMcmdqLnp1Hw+rHPTUEbna0/is7W6Pl49CW23O/GXDyoBNOLhoffg9YWjcFegz/4khABwZli7mKsni4EPKq4wR3LRFhfhLPgsyp1Zu4vVv0fIYex6fNqscVegP/73oocAAP970UOk9BAehzVJGUvdLjFXTxYDH5+9ziRHecAIVzA2up9NGWfl82GNXBRLkATdjQQhIFiTlO0prbNpxnZlbiACuMDoQyH0CBdCnAxxUz4fPpGLYvFjE34LCcLHyJwdj2cmxNiUsxXdFeAvxdM2ziOWiUpI8CkFQIol4SpYnZwdteiy+gqKyY+NZjyCECDOKFqaU1iNvRZC2aUSStdgL88eOMUsS4ol4SoWjhlk06LrjNDyup/ZLEZx4cJ3atbj9DsyJycHY8aMQUhICMLDw/Hoo4/i0qVLRjIcxyErKwtKpRJBQUGYOnUqzp8/bySjVquxZs0aDBgwAH369MG8efNw7Rp76ChBiBlW8/Q/z5i/J3IKq/H2sVqLK7VlSRTCbg85hdU4Xd/MJBsRIqffmHAZLNviHOzL9m56FmfKeR6nKz5Hjx7FqlWrcOLECZSUlKCzsxMpKSn45ZdfDDLbtm1Dbm4uduzYgYqKCigUCiQnJ6O19c6eeXp6OgoKCnDo0CGUlpaira0Nqamp6OrqcnaTCUJwsJqnq6+3mvj6sBTMfKeszmXVm70Vvlmao8IoTQDhWpyd8NQcVxkXYb8ZeLfd3+FunK74FBUVYenSpRgxYgQeeOAB7N27F1euXEFlZSWAbmvP9u3bsWHDBsyfPx8JCQnYv38/2tvbcfDgQQBAc3Mz9uzZgzfeeAPTp0/Hgw8+iPz8fJw7dw5HjhxxdpMJQnCwRncBphMbS8FMV1Vv9mb4ZGkGgJThCtc1hiDAdg87cq/nFFbjq8s/Mckq+4knOtTl8dvNzd1m4bCwMABAbW0tGhsbkZKSYpCRy+WYMmUKysrKkJaWhsrKSmi1WiMZpVKJhIQElJWVYcaMGSbfo1aroVarDa9bWloAAFqtFlqt1mL79O9ZkxEb1CdxYK1PEgBpEwfhnbJ6pnPll/0HixNjkFt8Ed9c+RlyP9ufuXqzzam/p7n+eNN48YmOkQBYOtF2dB5BOIIrc3XxsXBGhgZibGwY7+/wFC5VfDiOQ0ZGBiZOnIiEhAQAQGNjIwAgIiLCSDYiIgL19fUGmYCAAPTr189ERv/53uTk5GDz5s0mx4uLixEcbFsTLSkpsd0hkUF9EgeW+jQMwLaxjCdRVaOwsJrfZ1CLwkL2rRtWevanvd17kiPyiY6hCuyEO3Blri4+Fs5Nc+Phx2qiFgAuVXxWr16Nb7/9FqWlpSbvSSTGPxLHcSbHemNNJjMzExkZGYbXLS0tiIqKQkpKCvr2tVybS6vVoqSkBMnJyZDJZFa/XyxQn8QBS58OlNdh62eXzL7Xm4i7AnCjTcMkK5UApzckO/XhbK4/esurN7A4MQavFl6wKUe1zwh3ob8mrSkoErCXuegJq5VoytABmJkQyfv8nsRlis+aNWvw8ccf49ixYxg48E4CL4Wie9+7sbERkZF3fqympiaDFUihUECj0UClUhlZfZqampCUlGT2++RyOeRyuclxmUzG9KBklRMT1CdxYK1PTyYNwZbCy0zxEleateie5myTNjkWfYJM7xdn0LM/3jRW+iiafV/XWJR5ZkIMXkod4cZWEb6M/pp820owAwfg/s2fYcUkfukrWK1Ek+PuYT6nUHC6LZbjOKxevRoffvghvvjiC8TGGu9zx8bGQqFQGJnDNRoNjh49alBqRo0aBZlMZiTT0NCAqqoqi4oPQXgjAf5SxEc6Nz+GmBKNCY3M2fFYlhRtclyfF+mluaT0EO4lc3Y80ibHWg2G0HHA28dqeRUtZcn/I9YEnU63+KxatQoHDx7Ev/71L4SEhBh8ckJDQxEUFASJRIL09HRkZ2cjLi4OcXFxyM7ORnBwMBYtWmSQXb58OdauXYv+/fsjLCwM69atw8iRIzF9+nRnN5kgBM38hwbi/Ke2t1hYkEqoYKajZKQMQ2FhDdbPuA91KjWiw4KxODGGfHoIj5E5Ox5rHh6KhKzPrMrxKVrKkv9HrAk6na747Ny5EwAwdepUo+N79+7F0qVLAQDPP/88Ojo6sHLlSqhUKowbNw7FxcUICbmzsn3zzTfh7++PBQsWoKOjA9OmTcO+ffvg58cQrkIQXgTLPj4rYp2ohMjixBiv2sojxM0HFVdsyuhD25dPGmxVjiWiSwK2PEJCxOmKD8fZnp0lEgmysrKQlZVlUSYwMBB5eXnIy8tzYusIQnyw7OOzQFtcBOG9ODO0nSWiiwObEiVEaOlHECKAtXCpJWiLiyC8G2eGtrsyP5AQIMWHIETCS3NHYPnEGLs+S1tcBOHdsGZ7v97cYVPGlfmBhADNhAQhIjamjrAZwdETCagKO0H4AixFSwFgT2mdzeguFiVKrBFdACk+BCE6MmfH4+KWWRg/2HqK+MTYMFx6ZRYpPQThI7BuibMULrVVgkLMVmRxtpogfJwAfykOPZto1vqjzynzflqiaCcmgiDsI/LuIJsy1gqX5hRWY9jGwzhRc9Ps+/r5RcwLKpoVCULE6K0/G+cMx1OJ0dg4ZzgubhGflScnJwdjxoxBSEgIwsPD8eijj+LSJeNSHRzHISsrC0qlEkFBQZg6dSrOnz9vJKNWq7FmzRoMGDAAffr0wbx583Dt2jV3doUgPIojjsk5hdV4+1itxYiuxNgwUc4vvSHFhyBEToC/FMsnDcbLjyRg+aTBorTyHD16FKtWrcKJEydQUlKCzs5OpKSk4JdffjHIbNu2Dbm5udixYwcqKiqgUCiQnJyM1tZWg0x6ejoKCgpw6NAhlJaWoq2tDampqejq6vJEtwjC7bA6HF++0Wr0miV3z8k681YgsSG+GZIgCK+jqKgIS5cuxYgRI/DAAw9g7969uHLlCiorKwF0W3u2b9+ODRs2YP78+UhISMD+/fvR3t6OgwcPAgCam5uxZ88evPHGG5g+fToefPBB5Ofn49y5czhy5Ignu0cQboM1uutkzU0jPx+W3D3WtsjEhEursxMEQdhDc3MzACAsrNvBsra2Fo2NjUhJSTHIyOVyTJkyBWVlZUhLS0NlZSW0Wq2RjFKpREJCAsrKyjBjxgyz36VWq6FWqw2v9RXltVottFqtibz+mLn3xIo39gnwzn7Z6pMEwITYu3GqXmXzXPll/zFEZn1y5irkfrYTEF+92eb037N3n1w9XqT4EAQhKDiOQ0ZGBiZOnIiEhAQAMNT8i4iIMJKNiIhAfX29QSYgIAD9+vUzkdF/3hw5OTnYvHmzyfHi4mIEB1veNuhZRNlb8MY+Ad7ZL2t9+p+I7j+bqKpR+Gto+9PRAEzr75qhFoWFjmWRt4S+T+3trk2MSIoPQRCCYvXq1fj2229RWlpq8p5EYmzD5zjO5FhvbMlkZmYiIyPD8LqlpQVRUVFISUlB3759TeS1Wi1KSkqQnJzsNbW6vLFPgHf2i6VPB8rrsPWzS2bf681T46NxsaGFyUIkAVD5UrLT/Qh790lvdXUVpPgQBCEY1qxZg48//hjHjh3DwIEDDccVCgWAbqtOZGSk4XhTU5PBCqRQKKDRaKBSqYysPk1NTUhKSrL4nXK5HHK53OS4TCaz+rC09b4Y8cY+Ad7ZL2t9ejJpCLYUXgZLXePdX+uLm9p2DIpXhqBPkOm94iz0fXL1WJFzM0EQHofjOKxevRoffvghvvjiC8TGGmegjY2NhUKhMDLvazQaHD161KDUjBo1CjKZzEimoaEBVVVVVhUfgvA2AvyliI8Mcfp5f/fgQNtCIoAsPgRBeJxVq1bh4MGD+Ne//oWQkBCDT05oaCiCgoIgkUiQnp6O7OxsxMXFIS4uDtnZ2QgODsaiRYsMssuXL8fatWvRv39/hIWFYd26dRg5ciSmT5/uye4RhNuZ/9BAnP/0gtPOJ+YSFb0hxYcgCI+zc+dOAMDUqVONju/duxdLly4FADz//PPo6OjAypUroVKpMG7cOBQXFyMk5M7K9s0334S/vz8WLFiAjo4OTJs2Dfv27YOfn5+7ukIQgmBxYgxeLbxgM0SdFTGXqOgNKT4EQXgcjrM9O0skEmRlZSErK8uiTGBgIPLy8pCXl+fE1hGE+NAXLX37mOMRWOMHh4k+W3NPvEN9IwiCIAjCCNaipdaQSoB3l41zToMEAik+BEEQBOGlvDR3BJZPjLH78960xaXHu3pDEARBEIQRG1NHIG1yLFMpCz3eUIXdEuTjQxAEQRBeTubseKxNGYan3jmJEzWWi42OiAzB/IcGYnFijNdZevR4Z68IgiAIgjAiwF+KQ88mmrX+6C08n/5pMpZPGuy1Sg9AFh+CIAiC8Cn01p8D5XWov9mO6LBgr7bw9IYUH4IgCILwMQL8pVg+abCnm+ERfEO9IwiCIAiCAFl8CIIgjNAnU7RUIVqr1aK9vR0tLS1eU/jSG/sEeGe/fKFP+nuPJbGpPZDiQxAE0YPW1lYAQFRUlIdbQhC+TWtrK0JDQ51+XlJ8CIIgeqBUKnH16lWEhIRAIjFNfNLS0oKoqChcvXoVffv29UALnY839gnwzn75Qp84jkNrayuUSqVLvo8UH4IgiB5IpVIMHDjQplzfvn295sGjxxv7BHhnv7y9T66w9Ogh52aCIAiCIHwGUnwIgiAIgvAZfFrx6dJxOFXbnbr7VO1NdOlc40FOEN5Cl45D+X9+xr/O/oDy//zsk/eMXC7Hpk2bIJfLPd0Up+GNfQK8s1/UJ8cRvI/P3//+d7z++utoaGjAiBEjsH37dkyaNMnh8xZVNWDzJ9W42daBbWOBZfsrEHZXEDbNjcfMhEgntJwgvAv9PdPQfNtwLDI00OfuGblcjqysLE83w6l4Y58A7+wX9clxBG3x+eCDD5Ceno4NGzbgzJkzmDRpEmbNmoUrV644dN6iqgb8Mf8bowkcABqbb+OP+d+gqKrBofMThLdB9wxBEN6CoBWf3NxcLF++HM888wyGDx+O7du3IyoqCjt37rT7nF06Dps/qYY5A73+2OZPqn3ShE8Q5qB7hiAIb0KwW10ajQaVlZV44YUXjI6npKSgrKzMRF6tVkOtVhte6zM/arVaaLVaw/FTtTdxs60Dcr/u13IpZ/QvANxs68CJ75swNjbMaf1xJ/r+9uy32KE+eY7e94w5brZ1oKLmRwDG/RF63wiC8D0knKtyQjvI9evXce+99+Lrr79GUlKS4Xh2djb279+PS5cuGclnZWVh8+bNJuc5ePAggoODXd5egiBMaW9vx6JFi9Dc3Ox1OUcIghAngrX46OmdOZXjOLPZVDMzM5GRkWF4rc8EmZKSYjThnqq9iWX7Kwyv5VIOW0brsPG0FGrdnfO+s2SMqC0+JSUlSE5O9qpaLtQnz9D7nrHEP558EDcvnzbqj6V6VwRBEJ5CsD4+AwYMgJ+fHxobG42ONzU1ISIiwkReLpcbsj72zP4ok8mM/sb/VzjC7gqCpksCdZfEoOyodd2vNV0ShN0VhPH/FW7yWTH9meu72P+oT575M7lnev3p75kxg+8x2x9v4+9//ztiY2MRGBiIUaNG4fjx455ukllycnIwZswYhISEIDw8HI8++qiJpXzp0qWQSCRGf+PHjzeSUavVWLNmDQYMGIA+ffpg3rx5uHbtmju7YiArK8ukvQqFwvA+x3HIysqCUqlEUFAQpk6divPnzxudQ0j90RMTE2PSL4lEglWrVgEQxzgdO3YMc+fOhVKphEQiwUcffWT0vrPGRqVSYfHixQgNDUVoaCgWL16MW7du8WqrYBWfgIAAjBo1CiUlJUbHS0pKjLa++OInlWDT3HgAQG+7kf71prnx8JOaWpUIwhehe+YOroo0dQVHjx7FqlWrcOLECZSUlKCzsxMpKSn45ZdfjORmzpyJhoYGw19hYaHR++np6SgoKMChQ4dQWlqKtrY2pKamoqury53dMTBixAij9p47d87w3rZt25Cbm4sdO3agoqICCoUCycnJhsKzgPD6AwAVFRVGfdI/9x577DGDjNDH6ZdffsEDDzyAHTt2mH3fWWOzaNEinD17FkVFRSgqKsLZs2exePFifo3lBMyhQ4c4mUzG7dmzh6uurubS09O5Pn36cHV1dTY/29zczAHgmpubzb5/+Nx1bnz2EW7oi59wH330ETf0xU+48dlHuMPnrju7G25Ho9FwH330EafRaDzdFKdBffI8+nsmev3/M/z1vGfM9cfWfSg2xo4dy/3hD38wOjZs2DDuhRde8FCL2GlqauIAcEePHjUcW7JkCffII49Y/MytW7c4mUzGHTp0yHDshx9+4KRSKVdUVOTK5ppl06ZN3AMPPGD2PZ1OxykUCu61114zHLt9+zYXGhrKvfXWWxzHCa8/lvjTn/7EDRkyhNPpdBzHiW+cAHAFBQWG184am+rqag4Ad+LECYNMeXk5B4C7ePEic/sEa/EBgIULF2L79u14+eWX8Zvf/AbHjh1DYWEhoqOjHT73zIRIlK5/GO8sGQOg26endP3DPpWIjSD4oL9n3l8xHv/r8d/g/RXjfeqe0UeapqSkGB23FGkqNJqbmwEAYWHGvotfffUVwsPDMXToUKxYsQJNTU2G9yorK6HVao36rFQqkZCQ4LE+f/fdd1AqlYiNjcXjjz+OmpoaAEBtbS0aGxuN2iqXyzFlyhRDW4XYn95oNBrk5+dj2bJlRv6sYhunnjhrbMrLyxEaGopx48YZZMaPH4/Q0FBe/RS8c/PKlSuxcuVKl5zbTyrB2NgwFF4AxsaG+YSpniAcwU8qQeKQ/p5uhkf46aef0NXVZeJjGBERYeKLKDQ4jkNGRgYmTpyIhIQEw/FZs2bhscceQ3R0NGpra7Fx40Y8/PDDqKyshFwuR2NjIwICAtCvXz+j83mqz+PGjcO7776LoUOH4saNG3jllVeQlJSE8+fPG9pjbnzq6+sBQHD9McdHH32EW7duYenSpYZjYhun3jhrbBobGxEeHm5y/vDwcF79FLziQxAEISRYI02FxOrVq/Htt9+itLTU6PjChQsN/09ISMDo0aMRHR2NTz/9FPPnz7d4Pk/1edasWYb/jxw5EomJiRgyZAj2799vcPa1Z3yENIZ79uzBrFmzoFQqDcfENk6WcMbYmJPn209Bb3URBEEIBb6RpkJhzZo1+Pjjj/Hll19i4MCBVmUjIyMRHR2N7777DgCgUCig0WigUqmM5ITS5z59+mDkyJH47rvvDNFd1sZH6P2pr6/HkSNH8Mwzz1iVE9s4OWtsFAoFbty4YXL+H3/8kVc/SfEhCIJgwFWRpq6C4zisXr0aH374Ib744gvExsba/MzPP/+Mq1evIjKy229r1KhRkMlkRn1uaGhAVVWVIPqsVqtx4cIFREZGIjY2FgqFwqitGo0GR48eNbRV6P3Zu3cvwsPDMWfOHKtyYhsnZ41NYmIimpubcerUKYPMyZMn0dzczKufXrvVxf2akNpWAjWtVov29na0tLR4Tc4R6pM48LY+meuP/v7jhJkgnjcZGRlYvHgxRo8ejcTEROzatQtXrlzBH/7wB083zYRVq1bh4MGD+Ne//oWQkBDDajs0NBRBQUFoa2tDVlYWfve73yEyMhJ1dXV48cUXMWDAAPz3f/+3QXb58uVYu3Yt+vfvj7CwMKxbtw4jR47E9OnT3d6ndevWYe7cuRg0aBCamprwyiuvoKWlBUuWLIFEIkF6ejqys7MRFxeHuLg4ZGdnIzg4GIsWLRJkf3qi0+mwd+9eLFmyBP7+dx7NYhmntrY2fP/994bXtbW1OHv2LMLCwjBo0CCnjM3w4cMxc+ZMrFixAm+//TYA4Nlnn0Vqairuu+8+9sbyCVETE1evXuXQXUOR/uiP/jz8d/XqVU9PCU7jb3/7GxcdHc0FBARwDz30kFF4uJCwNBZ79+7lOI7j2tvbuZSUFO6ee+7hZDIZN2jQIG7JkiXclStXjM7T0dHBrV69mgsLC+OCgoK41NRUExl3sXDhQi4yMpKTyWScUqnk5s+fz50/f97wvk6n4zZt2sQpFApOLpdzkydP5s6dO2d0DiH1pyefffYZB4C7dOmS0XGxjNOXX35p9npbsmQJx3HOG5uff/6Ze+KJJ7iQkBAuJCSEe+KJJziVSsWrrYKt1eUoOp0O169fR0hIiFWnJ31pi6tXr3pNLSHqkzjwtj6Z6w/HcWhtbYVSqYRUSjvrBEF4Hq/d6pJKpTYd+XrSs8yFt0B9Egfe1qfe/QkNDfVgawiCIIyhJRhBEARBED4DKT4EQRAEQfgMPq/4yOVybNq0CXK53NNNcRrUJ3HgbX3ytv4QBOGdeK1zM0EQBEEQRG983uJDEARBEITvQIoPQRAEQRA+Ayk+BEEQBEH4DKT4EARBEAThM5DiQxAEQRCEz+Dzis/f//53xMbGIjAwEKNGjcLx48c93SSz5OTkYMyYMQgJCUF4eDgeffRRXLp0yUhm6dKlkEgkRn/jx483klGr1VizZg0GDBiAPn36YN68ebh27Zo7u2IgKyvLpL0KhcLwPsdxyMrKglKpRFBQEKZOnYrz588bnUNI/YmJiTHpj0QiwapVqwCIY3yOHTuGuXPnQqlUQiKR4KOPPjJ631ljolKpsHjxYoSGhiI0NBSLFy/GrVu3XNw7giAIH1d8PvjgA6Snp2PDhg04c+YMJk2ahFmzZuHKlSuebpoJR48exapVq3DixAmUlJSgs7MTKSkp+OWXX4zkZs6ciYaGBsNfYWGh0fvp6ekoKCjAoUOHUFpaira2NqSmpqKrq8ud3TEwYsQIo/aeO3fO8N62bduQm5uLHTt2oKKiAgqFAsnJyWhtbTXICKk/FRUVRn0pKSkBADz22GMGGaGPzy+//IIHHngAO3bsMPu+s8Zk0aJFOHv2LIqKilBUVISzZ89i8eLFLu8fQRCE11ZnZ2Hs2LHcH/7wB6Njw4YN41544QUPtYidpqYmDoBRZeglS5ZwjzzyiMXP3Lp1i5PJZNyhQ4cMx3744QdOKpVyRUVFrmyuWTZt2sQ98MADZt/T6XScQqHgXnvtNcOx27dvc6Ghodxbb73FcZzw+tObP/3pT9yQIUM4nU7HcZz4xgcAV1BQYHjtrDGprq7mAHAnTpwwyJSXl3MAuIsXL7q4VwRB+Do+a/HRaDSorKxESkqK0fGUlBSUlZV5qFXsNDc3AwDCwsKMjn/11VcIDw/H0KFDsWLFCjQ1NRneq6yshFarNeqzUqlEQkKCx/r83XffQalUIjY2Fo8//jhqamoAALW1tWhsbDRqq1wux5QpUwxtFWJ/9Gg0GuTn52PZsmWQSCSG42Ibn544a0zKy8sRGhqKcePGGWTGjx+P0NBQQfSTIAjvxmcVn59++gldXV2IiIgwOh4REYHGxkYPtYoNjuOQkZGBiRMnIiEhwXB81qxZeO+99/DFF1/gjTfeQEVFBR5++GGo1WoAQGNjIwICAtCvXz+j83mqz+PGjcO7776Lzz77DLt370ZjYyOSkpLw888/G9pjbXyE1p+efPTRR7h16xaWLl1qOCa28emNs8aksbER4eHhJucPDw8XRD8JgvBu/D3dAE/TczUOdCsVvY8JjdWrV+Pbb79FaWmp0fGFCxca/p+QkIDRo0cjOjoan376KebPn2/xfJ7q86xZswz/HzlyJBITEzFkyBDs37/f4PRrz/gIYQz37NmDWbNmQalUGo6JbXws4YwxMScvtH4SBOGd+KzFZ8CAAfDz8zNZYTY1NZmsaIXEmjVr8PHHH+PLL7/EwIEDrcpGRkYiOjoa3333HQBAoVBAo9FApVIZyQmlz3369MHIkSPx3XffGaK7rI2PUPtTX1+PI0eO4JlnnrEqJ7bxcdaYKBQK3Lhxw+T8P/74oyD6SRCEd+Ozik9AQABGjRpliLzRU1JSgqSkJA+1yjIcx2H16tX48MMP8cUXXyA2NtbmZ37++WdcvXoVkZGRAIBRo0ZBJpMZ9bmhoQFVVVWC6LNarcaFCxcQGRmJ2NhYKBQKo7ZqNBocPXrU0Fah9mfv3r0IDw/HnDlzrMqJbXycNSaJiYlobm7GqVOnDDInT55Ec3OzIPpJEISX4ymvaiFw6NAhTiaTcXv27OGqq6u59PR0rk+fPlxdXZ2nm2bCH//4Ry40NJT76quvuIaGBsNfe3s7x3Ec19rayq1du5YrKyvjamtruS+//JJLTEzk7r33Xq6lpcVwnj/84Q/cwIEDuSNHjnDffPMN9/DDD3MPPPAA19nZ6fY+rV27lvvqq6+4mpoa7sSJE1xqaioXEhJi+P1fe+01LjQ0lPvwww+5c+fOcb///e+5yMhIwfaH4ziuq6uLGzRoELd+/Xqj42IZn9bWVu7MmTPcmTNnOABcbm4ud+bMGa6+vp7jOOeNycyZM7n777+fKy8v58rLy7mRI0dyqampbukjQRC+jU8rPhzHcX/729+46OhoLiAggHvooYeMwsOFBACzf3v37uU4juPa29u5lJQU7p577uFkMhk3aNAgbsmSJdyVK1eMztPR0cGtXr2aCwsL44KCgrjU1FQTGXexcOFCLjIykpPJZJxSqeTmz5/PnT9/3vC+TqfjNm3axCkUCk4ul3OTJ0/mzp07Z3QOIfWH4zjus88+4wBwly5dMjoulvH58ssvzV5nS5Ys4TjOeWPy888/c0888QQXEhLChYSEcE888QSnUqnc1EuCIHwZCcdxnEdMTQRBEARBEG7GZ318CIIgCILwPUjxIQiCIAjCZyDFhyAIgiAIn4EUH4IgCIIgfAZSfAiCIAiC8BlI8SEIgiAIwmcgxYcgCIIgCJ+BFB+CIAiCIHwGUnwIgiAIgvAZSPEhCIIgCMJnIMWHIAiCIAif4f8H4UGZOd9c/nAAAAAASUVORK5CYII=\n",
+ "image/png": "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",
"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",
@@ -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": {
diff --git a/environment.yml b/environment.yml
index 69f597fd..a9af2506 100644
--- a/environment.yml
+++ b/environment.yml
@@ -1,13 +1,19 @@
-# Requirements as of October 2023
+# Requirements as of December 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
+ - shapely>=2.0.6
+ - pandas>=2.2.3
+ - numpy>=2.1.3
+ - affine>=2.4.0
+ - pyproj>=3.7.0
# rasterio will also install affine
- - rasterio>=1.3.8
+ - rasterio>=1.4.3
# pyvista also install pooch and matplotlib
- - pyvista>=0.42.2
+ - pyvista>=0.44.2
+ - matplotlib>=3.9.3
- gemgis>=1.1
diff --git a/environment_dev.yml b/environment_dev.yml
index 11c6b989..a67ec7c1 100644
--- a/environment_dev.yml
+++ b/environment_dev.yml
@@ -2,13 +2,19 @@ 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
+ - shapely>=2.0.6
+ - pandas>=2.2.3
+ - numpy>=2.1.3
+ - affine>=2.4.0
+ - pyproj>=3.7.0
# rasterio will also install affine
- #- rasterio>=1.3.8 will be installed through rioxarray
+ - rasterio>=1.4.3
# pyvista also install pooch and matplotlib
- #- pyvista>=0.42.2 will be installed through pvgeo
+ - pyvista>=0.44.2
+ - matplotlib>=3.9.3
- gemgis>=1.1
- rioxarray
- scipy
diff --git a/gemgis/download_gemgis_data.py b/gemgis/download_gemgis_data.py
index a96c4d98..1c327a7d 100644
--- a/gemgis/download_gemgis_data.py
+++ b/gemgis/download_gemgis_data.py
@@ -23,11 +23,8 @@
import zipfile
-def create_pooch(storage_url: str,
- files: List[str],
- target: str):
- """
- Create pooch class to fetch files from a website.
+def create_pooch(storage_url: str, files: List[str], target: str):
+ """Create pooch class to fetch files from a website.
Parameters
__________
@@ -46,58 +43,62 @@ def create_pooch(storage_url: str,
See also
________
download_tutorial_data: Download the GemGIS data for each tutorial.
+
"""
try:
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'):
- """
- Download the GemGIS data for each tutorial.
+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.
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:
- import pooch
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)
diff --git a/gemgis/gemgis.py b/gemgis/gemgis.py
index ebef8c08..e2e10c62 100644
--- a/gemgis/gemgis.py
+++ b/gemgis/gemgis.py
@@ -20,9 +20,9 @@
"""
import numpy as np
+
# import scooby
import pandas as pd
-from pandas.core import frame
import rasterio
import geopandas as gpd
import rasterio.transform
@@ -54,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
@@ -65,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
@@ -83,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
@@ -130,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")
@@ -144,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")
@@ -154,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")
@@ -163,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")
@@ -185,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
@@ -216,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)):
@@ -235,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))):
@@ -259,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
@@ -274,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):
@@ -303,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)):
@@ -312,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:
@@ -334,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:
@@ -391,91 +447,110 @@ 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)):
@@ -491,8 +566,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]
@@ -518,15 +599,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]
@@ -547,7 +628,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)
@@ -558,14 +639,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
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
diff --git a/gemgis/postprocessing.py b/gemgis/postprocessing.py
index 28bc19d2..cdfe72e1 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,14 +61,15 @@ 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:
- 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
@@ -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,29 @@ 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.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
- mesh['Azimuths [°]'] = azimuths
+ mesh["Dips [°]"] = dips
+ mesh["Azimuths [°]"] = azimuths
return mesh
@@ -502,41 +599,54 @@ 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:
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 +654,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 +684,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 +707,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 +730,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 +783,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,56 +965,67 @@ 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 not 'geometry' 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 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
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 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
- 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 +1034,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 +1167,170 @@ 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):
- 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 not fault 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 not which 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 +1365,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 +1447,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
diff --git a/gemgis/raster.py b/gemgis/raster.py
index 8f619bec..8a5b5f17 100644
--- a/gemgis/raster.py
+++ b/gemgis/raster.py
@@ -28,18 +28,19 @@
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
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
@@ -96,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):
@@ -155,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)
@@ -179,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
@@ -241,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):
@@ -315,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
@@ -371,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)
@@ -403,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)
@@ -420,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)
@@ -435,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
@@ -515,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
@@ -672,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]]
@@ -735,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
@@ -768,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
@@ -827,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):
@@ -862,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
@@ -887,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
@@ -938,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):
@@ -957,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)
@@ -973,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
@@ -1024,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):
@@ -1043,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)
@@ -1060,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
@@ -1112,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:
@@ -1127,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)
@@ -1151,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
@@ -1225,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)
@@ -1258,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
@@ -1395,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)
@@ -1424,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
@@ -1474,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
@@ -1525,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
@@ -1592,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):
@@ -1604,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
@@ -1620,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]:
@@ -1676,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)
@@ -1694,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
@@ -1761,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)
@@ -1790,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)
@@ -1868,7 +1996,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)
@@ -1883,21 +2011,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
@@ -1905,10 +2033,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
@@ -1966,7 +2101,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)
@@ -2007,20 +2142,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
@@ -2030,14 +2167,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
@@ -2092,7 +2231,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"):
@@ -2109,57 +2248,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
@@ -2205,7 +2349,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)
@@ -2218,11 +2362,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)
@@ -2233,10 +2379,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
@@ -2293,7 +2441,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)
@@ -2306,24 +2454,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 = []
@@ -2338,13 +2489,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
@@ -2434,45 +2587,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))
@@ -2480,11 +2635,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
@@ -2525,7 +2682,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)
@@ -2536,7 +2693,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)
@@ -2553,31 +2710,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),
@@ -2586,14 +2746,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
@@ -2625,11 +2787,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):
@@ -2639,26 +2804,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):
@@ -2673,15 +2847,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))])
@@ -2696,28 +2871,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
@@ -2736,15 +2915,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)
@@ -2766,26 +2945,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"
+ )
diff --git a/gemgis/utils.py b/gemgis/utils.py
index 596b3e60..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,11 +572,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 = []
@@ -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 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)))
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,143 +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
- bbox = box(*extent_rotated.bounds)
- 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
@@ -2463,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
@@ -2534,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())
@@ -2581,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
@@ -2636,36 +2848,35 @@ 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')
+ "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())
@@ -2674,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
@@ -2720,36 +2935,35 @@ 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')
+ "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())
@@ -2760,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:
diff --git a/gemgis/vector.py b/gemgis/vector.py
index e421aec2..27a76322 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
@@ -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]
@@ -36,153 +35,200 @@
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:
- """Extracting X and Y coordinates from a GeoDataFrame (Points) and returning a GeoDataFrame with X and Y
- coordinates as additional columns
+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:
+ """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 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
- _______
-
- >>> # Loading Libraries and File
- >>> import gemgis as gg
- >>> 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
- >>> 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
+ -------
- See Also
- ________
+ >>> # Loading Libraries and File
+ >>> import gemgis as gg
+ >>> import geopandas as gpd
+ >>> gdf = gpd.read_file(filename='file.shp')
+ >>> gdf
- extract_xy_linestring : Extracting 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
+ +----+------+-----------+------------------------+
+ | 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
+ 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')
+ 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)
@@ -192,289 +238,374 @@ 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:
- """Extracting the coordinates of Shapely LineStrings within a GeoDataFrame
- and storing the X and Y coordinates in lists per LineString
+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:
+ """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
+ 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
- Example
- _______
+ .. 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 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...
- See Also
- ________
+ +----+-----------+-----------+----------------------------------------------------+-----------------------------------------------------+-------------------------------------------------------------+
+ | | 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...] |
+ +----+-----------+-----------+-------------------------------------------+--------+-----------------------------------------------------+-------------------------------------------------------------+
- 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
+ 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
"""
-
# 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:
- """Extracting X and Y coordinates from a GeoDataFrame (LineStrings) and returning a GeoDataFrame with X and Y
- coordinates as additional columns
+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:
+ """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
- 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
>>> import gemgis as gg
>>> 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
- ____
+ ----
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')
+ 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)
@@ -488,253 +619,297 @@ 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:
- """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
+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:
+ """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)
-
- reset_index : bool
+ 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, 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
- _______
-
+ -------
>>> # Loading Libraries and File
>>> import gemgis as gg
>>> 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
- See Also
- ________
+ +----+-----------+------------------------+--------+--------+
+ | 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 |
+ +----+-----------+------------------------+--------+--------+
- 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
- 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
+ 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
-
- """
+ ``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)
@@ -747,88 +922,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)
@@ -840,117 +1022,163 @@ def extract_xy(gdf: 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
+def extract_xyz_points(
+ gdf: gpd.geodataframe.GeoDataFrame,
+) -> gpd.geodataframe.GeoDataFrame:
+ """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.
- Returns
- _______
+ +----+-----------------------------------+
+ | | 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
- Example
- _______
+ .. versionchanged:: 1.2
+ Example
+ -------
>>> # Loading Libraries and creating Shapely Point
>>> import gemgis as gg
>>> from shapely.geometry import Point
>>> 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
+ 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
+ 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')
+ 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:
- """Extracting X, Y, and Z coordinates from a GeoDataFrame containing Shapely LineStrings with Z components
+def extract_xyz_linestrings(
+ gdf: gpd.geodataframe.GeoDataFrame,
+ reset_index: bool = True,
+ drop_index: bool = True,
+) -> gpd.geodataframe.GeoDataFrame:
+ """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.
+
+ +----+-----------------------------------------------------+
+ | | 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
+ 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
- Example
- _______
+ .. versionchanged:: 1.2
+ Example
+ -------
>>> # Loading Libraries and creating Shapely LineString
>>> import gemgis as gg
>>> from shapely.geometry import LineString
@@ -962,111 +1190,146 @@ def extract_xyz_linestrings(gdf: gpd.geodataframe.GeoDataFrame,
>>> # 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
- See Also
- ________
+ +----+------------------------+----------------+-------+-------+-------+
+ | | 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 |
+ +----+------------------------+----------------+-------+-------+-------+
- extract_xyz_points: Extracting X and Y coordinates from a GeoDataFrame containing Shapely Points with
+
+ See Also
+ --------
+ 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
"""
# 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:
- """Extracting X, Y, and Z coordinates from a GeoDataFrame containing Shapely Polygons with Z components
+def extract_xyz_polygons(
+ gdf: gpd.geodataframe.GeoDataFrame,
+ reset_index: bool = True,
+ drop_index: bool = True,
+) -> gpd.geodataframe.GeoDataFrame:
+ """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.
+
+ +----+--------------------------------------------------+
+ | | 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
+ 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
- Example
- _______
+ .. versionchanged:: 1.2
+ Example
+ -------
>>> # Loading Libraries and creating Shapely Polygon
>>> import gemgis as gg
>>> from shapely.geometry import Polygon
@@ -1078,176 +1341,211 @@ def extract_xyz_polygons(gdf: gpd.geodataframe.GeoDataFrame,
>>> # 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
+ See Also
+ --------
+ 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')
+ 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:
- """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
+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:
+ """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
+ 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
- _______
-
+ -------
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
>>> 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')
@@ -1257,203 +1555,223 @@ def extract_xyz_rasterio(gdf: gpd.geodataframe.GeoDataFrame,
>>> # 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
- See Also
- ________
+ +----+-----------+------------------------+-------+-------+-------+
+ | 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|
+ +----+-----------+------------------------+-------+-------+-------+
- 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
- """
+ 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
+ """
# 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=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:
@@ -1465,105 +1783,140 @@ 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:
- """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
+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:
+ """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
+ 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``
Returns
- _______
-
+ -------
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
- Example
- _______
+ .. versionchanged:: 1.2
+ Example
+ -------
>>> # Loading Libraries and File
>>> import gemgis as gg
>>> import geopandas as gpd
>>> 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')
@@ -1576,201 +1929,214 @@ def extract_xyz_array(gdf: gpd.geodataframe.GeoDataFrame,
>>> # 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
- See Also
- ________
+ +----+-----------+------------------------+-------+-------+-------+
+ | 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|
+ +----+-----------+------------------------+-------+-------+-------+
- extract_xyz_rasterio : Extracting X, Y, and Z coordinates from a GeoDataFrame and Digital Elevation Model
+ 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
- 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')
+ 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
+ # 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 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)
@@ -1778,106 +2144,141 @@ 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:
- """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
+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:
+ """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
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
-
- minz : float
- 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``
+ 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, default: ``None``
+ Value defining the minimum elevation of the data that needs to be returned, e.g. ``minz=50``, 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
- _______
-
+ -------
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
- Example
- _______
+ .. versionchanged.: 1.2
+ Example
+ -------
>>> # Loading Libraries and File
>>> import gemgis as gg
>>> import geopandas as gpd
>>> 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')
@@ -1887,74 +2288,86 @@ def extract_xyz(gdf: gpd.geodataframe.GeoDataFrame,
>>> # 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
- See Also
- ________
+ +----+-----------+------------------------+-------+-------+-------+
+ | 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|
+ +----+-----------+------------------------+-------+-------+-------+
- 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
+ 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
"""
-
# 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:
@@ -1962,41 +2375,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:
@@ -2004,84 +2419,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)
@@ -2092,27 +2515,29 @@ def extract_xyz(gdf: gpd.geodataframe.GeoDataFrame,
# Exploding Geometries
###############################################################
-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
- __________
+def explode_linestring(
+ linestring: shapely.geometry.linestring.LineString,
+) -> List[shapely.geometry.point.Point]:
+ """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
- Example
- _______
+ .. versionchanged:: 1.2
+ Example
+ -------
>>> # Loading Libraries and creating LineString
>>> import gemgis as gg
>>> from shapely.geometry import LineString
@@ -2140,23 +2565,21 @@ def explode_linestring(linestring: shapely.geometry.linestring.LineString) -> Li
'POINT (20 20)'
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')
+ 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)]
@@ -2164,29 +2587,28 @@ 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]:
- """Separating a LineString into its single elements and returning a list of LineStrings representing these elements,
- also works for LineStrings with Z components
+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.
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
- Example
- _______
+ .. versionchanged:: 1.2
+ Example
+ -------
>>> # Loading Libraries and creating LineString
>>> import gemgis as gg
>>> from shapely.geometry import LineString
@@ -2209,57 +2631,59 @@ def explode_linestring_to_elements(linestring: shapely.geometry.linestring.LineS
'LINESTRING (10 10, 20 20)'
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')
+ 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]:
- """Exploding a MultiLineString into a list of LineStrings
+def explode_multilinestring(
+ multilinestring: shapely.geometry.multilinestring.MultiLineString,
+) -> List[shapely.geometry.linestring.LineString]:
+ """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
- Example
- _______
+ .. versionchanged:: 1.2
+ Example
+ -------
>>> # Loading Libraries and creating MultiLineString
>>> import gemgis as gg
>>> from shapely.geometry import MultiLineString
@@ -2282,28 +2706,28 @@ def explode_multilinestring(multilinestring: shapely.geometry.multilinestring.Mu
'LINESTRING (-1 0, 1 0)'
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):
- 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)
@@ -2311,94 +2735,128 @@ 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:
- """Exploding Shapely MultiLineStrings stored in a GeoDataFrame to Shapely LineStrings
+def explode_multilinestrings(
+ gdf: gpd.geodataframe.GeoDataFrame,
+ reset_index: bool = True,
+ drop_level0: bool = True,
+ drop_level1: bool = True,
+) -> gpd.geodataframe.GeoDataFrame:
+ """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.
+
+ +----+----------------------------------------+
+ | | geometry |
+ +----+----------------------------------------+
+ | 0 | MULTILINESTRING ((0.0 0.0, 1.0 1.0)) |
+ +----+----------------------------------------+
+ | 1 | MULTILINESTRING ((0.0 0.0, 1.0 1.0)) |
+ +----+----------------------------------------+
- 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``.
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
- Example
- _______
+ .. versionchanged:: 1.2
+ Example
+ -------
>>> # Loading Libraries and File
>>> import gemgis as gg
>>> 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)
- See Also
- ________
+ +----+------------------------------+
+ | 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)|
+ +----+------------------------------+
- explode_multilinestring : Exploding a MultiLineString into a list of single LineStrings
- """
+ See Also
+ --------
+ 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')
+ 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)
@@ -2409,37 +2867,36 @@ 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]:
- """Exploding Shapely Polygon to list of Points
+def explode_polygon(
+ polygon: shapely.geometry.polygon.Polygon,
+) -> List[shapely.geometry.point.Point]:
+ """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
- Example
- _______
+ .. versionchanged:: 1.2
+ Example
+ -------
>>> # Loading Libraries and creating Polygon
>>> import gemgis as gg
>>> from shapely.geometry import Polygon
@@ -2464,119 +2921,141 @@ def explode_polygon(polygon: shapely.geometry.polygon.Polygon) -> List[shapely.g
'POINT (1 1)'
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')
+ 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:
- """Converting a GeoDataFrame containing elements of geom_type Polygons to a GeoDataFrame with LineStrings
+def explode_polygons(
+ gdf: gpd.geodataframe.GeoDataFrame,
+) -> gpd.geodataframe.GeoDataFrame:
+ """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.
- Returns
- _______
+ +----+------------------------------------------------+
+ | | 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
- Example
- _______
+ .. versionchanged:: 1.2
+ Example
+ -------
>>> # Loading Libraries and creating Polygon
>>> import gemgis as gg
>>> 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')
+ 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]:
- """Exploding a Shapely Geometry Collection to a List of Base Geometries
+def explode_geometry_collection(
+ collection: shapely.geometry.collection.GeometryCollection,
+) -> List[shapely.geometry.base.BaseGeometry]:
+ """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
- Example
- _______
+ .. versionchanged:: 1.2
+ Example
+ -------
>>> # Loading Libraries and creating Geometry Collection
>>> import gemgis as gg
>>> from shapely.geometry import LineString
@@ -2601,23 +3080,21 @@ def explode_geometry_collection(collection: shapely.geometry.collection.Geometry
'LINESTRING (0 0, 1 1)'
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')
+ 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)
@@ -2625,47 +3102,71 @@ 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:
- """Exploding Shapely Geometry Collections stored in GeoDataFrames to different Shapely Base Geometries
+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:
+ """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``
-
- drop_level0 : bool
- Variable to drop the level_0 column.
- 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``
-
- remove_points : bool
- Variable to remove points from exploded 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, 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, 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, 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
- Example
- _______
+ .. versionchanged:: 1.2
+ Example
+ -------
>>> # Loading Libraries and creating Geometries
>>> import gemgis as gg
>>> from shapely.geometry import LineString, Polygon
@@ -2678,62 +3179,75 @@ def explode_geometry_collections(gdf: gpd.geodataframe.GeoDataFrame,
>>> # 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...
- See Also
- ________
+ +----+----------------------------------------------------+
+ | | 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...) |
+ +----+----------------------------------------------------+
- explode_geometry_collection : Exploding a Shapely Geometry Collection Object into a list of Base Geometries
+ See Also
+ --------
+ 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')
+ 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:
@@ -2741,13 +3255,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
@@ -2755,77 +3267,110 @@ 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:
- """
- Create LineString from an array or GeoDataFrame containing X, Y, and Z coordinates of 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:
+ """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``.
Returns
- _______
-
+ -------
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
- _______
-
+ -------
>>> # Loading Libraries and creating 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)
>>> 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)):
- 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):
@@ -2840,7 +3385,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]
@@ -2857,128 +3402,168 @@ 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]:
- """Creating LineStrings from a GeoDataFrame containing X, Y, and Z coordinates of vertices of multiple LineStrings
+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]:
+ """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
- _______
-
+ -------
linestrings : Union[List[shapely.geometry.linestring.LineString], gpd.geodataframe.GeoDataFrame]
List of LineStrings or GeoDataFrame containing the LineStrings with Z component
.. 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
- _______
-
+ -------
>>> # Loading Libraries and File
>>> import gemgis as gg
>>> import geopandas as gpd
>>> 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')
+ 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]
@@ -2987,99 +3572,159 @@ 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]:
- """Creating LineStrings from PyVista Contour Lines and save them as list or 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]:
+ """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
-
- return_gdf : bool
- Variable to create GeoDataFrame of the created list of Shapely Objects.
- 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'``
+ 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, 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], 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.
+
+ +----+----------------------------------------------------+---------+
+ | | 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
- Example
- _______
+ .. versionchanged:: 1.2
+ Example
+ -------
>>> # Loading Libraries and File
>>> import gemgis as gg
>>> 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')
+ 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 = []
@@ -3097,7 +3742,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]
@@ -3114,7 +3762,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
@@ -3123,67 +3774,91 @@ 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:
- """Interpolating a raster/digital elevation model from point or line Shape file
+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:
+ """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
- _______
-
+ -------
array : np.ndarray
- Array representing the interpolated raster/digital elevation model
+ Array representing the interpolated raster/digital elevation model.
.. versionadded:: 1.0.x
- Example
- _______
+ .. 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 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')
@@ -3194,42 +3869,45 @@ def interpolate_raster(gdf: gpd.geodataframe.GeoDataFrame,
398.16690286, 400.12027997]])
"""
-
# Trying to import scipy but returning error if scipy is not installed
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:
@@ -3237,11 +3915,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:
@@ -3249,19 +3927,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:
@@ -3277,84 +3957,125 @@ 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:
- """Clipping vector data contained in a GeoDataFrame to a provided bounding box/extent
+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:
+ """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
- Example
- _______
+ .. 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 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)
@@ -3366,203 +4087,251 @@ def clip_by_bbox(gdf: gpd.geodataframe.GeoDataFrame,
>>> # 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)
25
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')
+ 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:
- """Clipping vector data contained in a GeoDataFrame to a provided bounding box/extent
+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:
+ """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
-
- polygon : polygon: shapely.geometry.polygon
+ GeoDataFrame containing vector data that will be clipped to a provided bounding box/extent.
+
+ +----+-----------------------------+
+ | 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
- Example
- _______
+ .. 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 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)
@@ -3577,75 +4346,75 @@ def clip_by_polygon(gdf: gpd.geodataframe.GeoDataFrame,
>>> # 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)
25
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')
+ 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
@@ -3654,31 +4423,30 @@ 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:
- """Creating a buffer around a Shapely LineString or a Point
+def create_buffer(
+ geom_object: shapely.geometry.base.BaseGeometry, distance: Union[float, int]
+) -> shapely.geometry.polygon.Polygon:
+ """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
- Example
- _______
+ .. versionchanged:: 1.2
+ Example
+ -------
>>> # Loading Libraries and creating Point
>>> import gemgis as gg
>>> from shapely.geometry import Point
@@ -3693,19 +4461,19 @@ def create_buffer(geom_object: shapely.geometry.base.BaseGeometry,
-29.02846772544621, 92.38795325112869 -38.26834323650894, 88.19212643483553...))'
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 Shapely Points
"""
-
# 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)
@@ -3713,32 +4481,33 @@ 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:
- """Creating a unified buffer around Shapely LineStrings or Points
+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:
+ """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
- Example
- _______
+ .. versionchanged:: 1.2
+ Example
+ -------
>>> # Loading Libraries and creating Point
>>> import gemgis as gg
>>> from shapely.geometry import Point
@@ -3761,38 +4530,42 @@ def create_unified_buffer(geom_object: Union[gpd.geodataframe.GeoDataFrame,
-2.902846772544621, 9.23879532511287 -3.826834323650894,...)))'
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, (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)
@@ -3800,33 +4573,33 @@ 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:
- """Subtracting Shapely geometry objects from each other and returning the left over object
+def subtract_geom_objects(
+ geom_object1: shapely.geometry.base.BaseGeometry,
+ geom_object2: shapely.geometry.base.BaseGeometry,
+) -> shapely.geometry.base.BaseGeometry:
+ """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
- Example
- _______
+ .. versionchanged:: 1.2
+ Example
+ -------
>>> # Loading Libraries and creating Polygon
>>> import gemgis as gg
>>> from shapely.geometry import Polygon
@@ -3845,14 +4618,17 @@ def subtract_geom_objects(geom_object1: shapely.geometry.base.BaseGeometry,
'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('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
@@ -3860,46 +4636,44 @@ 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]:
- """Removing object from a buffered object by providing a distance
+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]:
+ """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
- Example
- _______
+ .. versionchanged:: 1.2
+ Example
+ -------
>>> # Loading Libraries and creating Point
>>> import gemgis as gg
>>> from shapely.geometry import Point, LineString
@@ -3924,44 +4698,42 @@ def remove_object_within_buffer(buffer_object: shapely.geometry.base.BaseGeometr
'LINESTRING (0 0, 7.071067811865473 7.071067811865473)'
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
"""
-
# 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:
@@ -3976,62 +4748,64 @@ 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]]:
- """Removing objects from a buffered object by providing a distance
+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],
+]:
+ """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
- Example
- _______
+ .. versionchanged:: 1.2
+ Example
+ -------
>>> # Loading Libraries and creating Point
>>> import gemgis as gg
>>> from shapely.geometry import Point, LineString
@@ -4049,7 +4823,7 @@ def remove_objects_within_buffer(buffer_object: shapely.geometry.base.BaseGeomet
>>> 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
@@ -4066,63 +4840,62 @@ def remove_objects_within_buffer(buffer_object: shapely.geometry.base.BaseGeomet
]
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
"""
-
# 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()
@@ -4133,10 +4906,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))]
@@ -4144,12 +4919,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:
@@ -4166,49 +4945,93 @@ 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]:
- """Function to create a buffer around a GeoDataFrame containing fault data and removing interface points
- that are located within this buffer
+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]:
+ """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
+ 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
- Example
- _______
+ .. versionchanged:: 1.2
+ Example
+ -------
>>> # Loading Libraries
>>> import gemgis as gg
>>> import geopandas as gpd
@@ -4226,6 +5049,15 @@ def remove_interfaces_within_fault_buffers(fault_gdf: gpd.geodataframe.GeoDataFr
>>> # 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)])
@@ -4239,87 +5071,106 @@ def remove_interfaces_within_fault_buffers(fault_gdf: gpd.geodataframe.GeoDataFr
>>> # 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
- ________
+ 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')
+ 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
@@ -4330,27 +5181,27 @@ 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
+ """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)])``
+ 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
- Example
- _______
+ .. versionchanged:: 1.2
+ Example
+ -------
>>> # Loading Libraries and creating LineString
>>> import gemgis as gg
>>> from shapely.geometry import LineString
@@ -4364,64 +5215,63 @@ 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)
+ ----
+ 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')
+ 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:
- """Function to calculate the strike direction of a straight Shapely LineString.
- The strike will always be calculated from start to end point
+def calculate_strike_direction_straight_linestring(
+ linestring: shapely.geometry.linestring.LineString,
+) -> float:
+ """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
@@ -4435,44 +5285,50 @@ def calculate_strike_direction_straight_linestring(linestring: shapely.geometry.
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')
+ 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)
@@ -4484,27 +5340,26 @@ def calculate_strike_direction_straight_linestring(linestring: shapely.geometry.
return angle
-def calculate_strike_direction_bent_linestring(linestring: shapely.geometry.linestring.LineString) -> List[float]:
- """Calculating the strike direction of a LineString with multiple elements
+def calculate_strike_direction_bent_linestring(
+ linestring: shapely.geometry.linestring.LineString,
+) -> List[float]:
+ """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
@@ -4518,62 +5373,61 @@ def calculate_strike_direction_bent_linestring(linestring: shapely.geometry.line
[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')
+ 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):
- """Calculating the dipping angle of a LineString digitized on a cross section
+def calculate_dipping_angle_linestring(
+ linestring: shapely.geometry.linestring.LineString,
+):
+ """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
@@ -4587,65 +5441,67 @@ def calculate_dipping_angle_linestring(linestring: shapely.geometry.linestring.L
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')
+ 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]]):
- """Calculating the dipping angles of LineStrings digitized on a cross section
+ linestring_list: Union[
+ gpd.geodataframe.GeoDataFrame, List[shapely.geometry.linestring.LineString]
+ ]
+):
+ """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
@@ -4662,46 +5518,47 @@ 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')
+ 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
@@ -4709,14 +5566,15 @@ 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]]):
- """Calculating the coordinates for one point digitized on a cross section provided as Shapely LineString
- Parameters
- __________
+def calculate_coordinates_for_point_on_cross_section(
+ linestring: shapely.geometry.linestring.LineString,
+ point: Union[shapely.geometry.point.Point, Tuple[float, float]],
+):
+ """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)])``
@@ -4726,16 +5584,14 @@ def calculate_coordinates_for_point_on_cross_section(linestring: shapely.geometr
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
@@ -4754,8 +5610,7 @@ def calculate_coordinates_for_point_on_cross_section(linestring: shapely.geometr
'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
@@ -4764,40 +5619,43 @@ def calculate_coordinates_for_point_on_cross_section(linestring: shapely.geometr
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')
+ 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])
@@ -4805,14 +5663,14 @@ 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):
- """Calculating the coordinates of vertices for a LineString on a straight cross section provided as Shapely
- LineString
+def calculate_coordinates_for_linestring_on_cross_sections(
+ linestring: shapely.geometry.linestring.LineString,
+ interfaces: shapely.geometry.linestring.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)])``
@@ -4822,16 +5680,14 @@ def calculate_coordinates_for_linestring_on_cross_sections(linestring: shapely.g
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
@@ -4859,8 +5715,7 @@ def calculate_coordinates_for_linestring_on_cross_sections(linestring: shapely.g
'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
@@ -4869,49 +5724,49 @@ def calculate_coordinates_for_linestring_on_cross_sections(linestring: shapely.g
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')
+ 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]:
- """Calculating the coordinates of vertices for LineStrings on a straight cross section provided as Shapely
- LineString
+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]:
+ """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)])``
@@ -4920,16 +5775,14 @@ def calculate_coordinates_for_linestrings_on_cross_sections(linestring: shapely.
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
@@ -4970,8 +5823,7 @@ def calculate_coordinates_for_linestrings_on_cross_sections(linestring: shapely.
'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
@@ -4980,31 +5832,36 @@ def calculate_coordinates_for_linestrings_on_cross_sections(linestring: shapely.
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')
+ 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]))]
@@ -5012,15 +5869,15 @@ 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:
- """Extracting coordinates of interfaces digitized on a cross section
+def extract_interfaces_coordinates_from_cross_section(
+ linestring: shapely.geometry.linestring.LineString,
+ interfaces_gdf: gpd.geodataframe.GeoDataFrame,
+ extract_coordinates: bool = True,
+) -> gpd.geodataframe.GeoDataFrame:
+ """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)])``
@@ -5029,16 +5886,14 @@ def extract_interfaces_coordinates_from_cross_section(linestring: shapely.geomet
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
@@ -5069,8 +5924,7 @@ def extract_interfaces_coordinates_from_cross_section(linestring: shapely.geomet
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
@@ -5080,83 +5934,94 @@ def extract_interfaces_coordinates_from_cross_section(linestring: shapely.geomet
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')
+ 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:
- """Extracting X, Y, and Z coordinates from cross sections and digitized interfaces
+def extract_xyz_from_cross_sections(
+ profile_gdf: gpd.geodataframe.GeoDataFrame,
+ interfaces_gdf: gpd.geodataframe.GeoDataFrame,
+ profile_name_column: str = "name",
+) -> gpd.geodataframe.GeoDataFrame:
+ """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
@@ -5167,16 +6032,14 @@ def extract_xyz_from_cross_sections(profile_gdf: gpd.geodataframe.GeoDataFrame,
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
@@ -5216,8 +6079,7 @@ def extract_xyz_from_cross_sections(profile_gdf: gpd.geodataframe.GeoDataFrame,
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
@@ -5227,83 +6089,99 @@ def extract_xyz_from_cross_sections(profile_gdf: gpd.geodataframe.GeoDataFrame,
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')
+ 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:
- """Calculating the midpoint of a LineString with two vertices
+def calculate_midpoint_linestring(
+ linestring: shapely.geometry.linestring.LineString,
+) -> shapely.geometry.point.Point:
+ """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
@@ -5317,38 +6195,34 @@ def calculate_midpoint_linestring(linestring: shapely.geometry.linestring.LineSt
'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')
+ 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])
@@ -5356,28 +6230,27 @@ 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]:
- """Calculating the midpoints of LineStrings with two vertices each
+def calculate_midpoints_linestrings(
+ linestring_gdf: Union[
+ gpd.geodataframe.GeoDataFrame, List[shapely.geometry.linestring.LineString]
+ ]
+) -> List[shapely.geometry.point.Point]:
+ """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
@@ -5403,41 +6276,45 @@ def calculate_midpoints_linestrings(linestring_gdf: Union[gpd.geodataframe.GeoDa
'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('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]
@@ -5449,13 +6326,14 @@ 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:
- """Calculating the orientation for one LineString on one cross sections
+def calculate_orientation_from_cross_section(
+ profile_linestring: shapely.geometry.linestring.LineString,
+ orientation_linestring: shapely.geometry.linestring.LineString,
+) -> list:
+ """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)])``
@@ -5465,16 +6343,14 @@ def calculate_orientation_from_cross_section(profile_linestring: shapely.geometr
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
@@ -5497,55 +6373,59 @@ def calculate_orientation_from_cross_section(profile_linestring: shapely.geometr
'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')
+ 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)
@@ -5554,16 +6434,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:
@@ -5582,14 +6468,14 @@ 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:
- """Calculating the orientation of a LineString on a bent cross section provided as Shapely LineString
+def calculate_orientation_from_bent_cross_section(
+ profile_linestring: shapely.geometry.linestring.LineString,
+ orientation_linestring: shapely.geometry.linestring.LineString,
+) -> list:
+ """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)])``
@@ -5599,16 +6485,14 @@ def calculate_orientation_from_bent_cross_section(profile_linestring: shapely.ge
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
@@ -5631,54 +6515,57 @@ def calculate_orientation_from_bent_cross_section(profile_linestring: shapely.ge
'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')
+ 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
@@ -5690,12 +6577,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
@@ -5705,20 +6598,24 @@ 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:
- """Calculating orientations from a cross sections using multiple LineStrings
+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:
+ """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)])``
@@ -5731,16 +6628,14 @@ def calculate_orientations_from_cross_section(profile_linestring: shapely.geomet
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
@@ -5764,34 +6659,32 @@ def calculate_orientations_from_cross_section(profile_linestring: shapely.geomet
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')
+ 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
@@ -5800,37 +6693,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:
@@ -5839,14 +6745,15 @@ 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:
- """Calculating orientations digitized from cross sections
+def extract_orientations_from_cross_sections(
+ profile_gdf: gpd.geodataframe.GeoDataFrame,
+ orientations_gdf: gpd.geodataframe.GeoDataFrame,
+ profile_name_column: str = "name",
+) -> gpd.geodataframe.GeoDataFrame:
+ """Calculate orientations digitized from cross sections.
Parameters
- __________
-
+ ----------
profile_gdf : gpd.geodataframe.GeoDataFrame
GeoDataFrame containing the different profile traces as LineStrings
@@ -5857,16 +6764,14 @@ def extract_orientations_from_cross_sections(profile_gdf: gpd.geodataframe.GeoDa
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
@@ -5903,91 +6808,107 @@ def extract_orientations_from_cross_sections(profile_gdf: gpd.geodataframe.GeoDa
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')
+ 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:
- """Calculating the orientation for a three point problem
+def calculate_orientation_for_three_point_problem(
+ gdf: gpd.geodataframe.GeoDataFrame,
+) -> gpd.geodataframe.GeoDataFrame:
+ """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
@@ -6005,38 +6926,36 @@ def calculate_orientation_for_three_point_problem(gdf: gpd.geodataframe.GeoDataF
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')
+ 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)
@@ -6051,16 +6970,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
@@ -6069,14 +7008,14 @@ 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) \
- -> Union[shapely.geometry.linestring.LineString, shapely.geometry.polygon.Polygon]:
- """Calculating the intersection between to Shapely Polygons
+def intersect_polygon_polygon(
+ polygon1: shapely.geometry.polygon.Polygon,
+ polygon2: shapely.geometry.polygon.Polygon,
+) -> Union[shapely.geometry.linestring.LineString, shapely.geometry.polygon.Polygon]:
+ """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]])``
@@ -6086,16 +7025,16 @@ def intersection_polygon_polygon(polygon1: shapely.geometry.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
.. versionadded:: 1.0.x
- Example
- _______
+ .. versionchanged:: 1.2
+ Example
+ -------
>>> # Loading Libraries
>>> import gemgis as gg
>>> from shapely.geometry import Polygon
@@ -6109,42 +7048,40 @@ 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
"""
-
# 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)
@@ -6152,15 +7089,16 @@ 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]]) \
- -> List[shapely.geometry.base.BaseGeometry]:
- """Calculating the intersections between one polygon and a list of polygons
+def intersect_polygon_polygons(
+ polygon1: shapely.geometry.polygon.Polygon,
+ polygons2: Union[
+ gpd.geodataframe.GeoDataFrame, List[shapely.geometry.polygon.Polygon]
+ ],
+) -> List[shapely.geometry.base.BaseGeometry]:
+ """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]])``
@@ -6169,16 +7107,16 @@ def intersections_polygon_polygons(polygon1: shapely.geometry.polygon.Polygon,
List of polygons as list or GeoDataFrame to get intersected
Returns
- _______
-
+ -------
intersections : List[shapely.geometry.base.BaseGeometry]
List of intersected geometries
.. versionadded:: 1.0.x
- Example
- _______
+ .. versionchanged:: 1.2
+ Example
+ -------
>>> # Loading Libraries and creating Polygon
>>> import gemgis as gg
>>> from shapely.geometry import Polygon
@@ -6209,29 +7147,27 @@ def intersections_polygon_polygons(polygon1: shapely.geometry.polygon.Polygon,
'LINESTRING (10 0, 10 10)'
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
"""
-
# 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):
@@ -6240,32 +7176,37 @@ def intersections_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 = [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(
- 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
+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]:
+ """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
@@ -6273,16 +7214,16 @@ def intersections_polygons_polygons(
List of Polygons or GeoDataFrame containing Polygons to be intersected
Returns
- _______
-
+ -------
intersections : List[shapely.geometry.base.BaseGeometry]
List of intersected geometries
.. versionadded:: 1.0.x
- Example
- _______
+ .. versionchanged:: 1.2
+ Example
+ -------
>>> # Loading Libraries and creating Polygon
>>> import gemgis as gg
>>> from shapely.geometry import Polygon
@@ -6302,7 +7243,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
[,
,
@@ -6326,17 +7267,15 @@ def intersections_polygons_polygons(
'LINESTRING (10 0, 10 10)'
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
"""
-
# 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):
@@ -6346,19 +7285,19 @@ def intersections_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):
@@ -6368,34 +7307,41 @@ def intersections_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 = [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]))]
+ 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:
- """Calculating the intersections between Polygons; the table must be sorted by stratigraphic age
+def extract_xy_from_polygon_intersections(
+ gdf: gpd.geodataframe.GeoDataFrame,
+ extract_coordinates: bool = False,
+ drop_index: bool = True,
+) -> gpd.geodataframe.GeoDataFrame:
+ """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
@@ -6408,16 +7354,14 @@ def extract_xy_from_polygon_intersections(gdf: gpd.geodataframe.GeoDataFrame,
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
@@ -6446,55 +7390,72 @@ def extract_xy_from_polygon_intersections(gdf: gpd.geodataframe.GeoDataFrame,
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')
+ 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 = [intersections_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
@@ -6502,9 +7463,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
@@ -6513,27 +7473,27 @@ 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]]:
- """Calculating the azimuth for an orientation Geodataframe represented by LineStrings
+def calculate_azimuth(
+ gdf: Union[
+ gpd.geodataframe.GeoDataFrame, List[shapely.geometry.linestring.LineString]
+ ]
+) -> List[Union[float, int]]:
+ """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
@@ -6560,8 +7520,7 @@ def calculate_azimuth(gdf: Union[gpd.geodataframe.GeoDataFrame,
[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
@@ -6569,62 +7528,58 @@ def calculate_azimuth(gdf: Union[gpd.geodataframe.GeoDataFrame,
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')
+ 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:
- """Creating a LineString object from a GeoDataFrame containing surface points at a given altitude and for a given
- formation
+def create_linestring_from_points(
+ gdf: gpd.geodataframe.GeoDataFrame, formation: str, altitude: Union[int, float]
+) -> shapely.geometry.linestring.LineString:
+ """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
@@ -6647,8 +7602,7 @@ def create_linestring_from_points(gdf: gpd.geodataframe.GeoDataFrame,
'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
@@ -6656,37 +7610,36 @@ def create_linestring_from_points(gdf: gpd.geodataframe.GeoDataFrame,
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')
+ 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())
@@ -6694,26 +7647,25 @@ def create_linestring_from_points(gdf: gpd.geodataframe.GeoDataFrame,
return linestring
-def create_linestring_gdf(gdf: gpd.geodataframe.GeoDataFrame) -> gpd.geodataframe.GeoDataFrame:
- """Creating LineStrings from Points
+def create_linestring_gdf(
+ gdf: gpd.geodataframe.GeoDataFrame,
+) -> gpd.geodataframe.GeoDataFrame:
+ """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
@@ -6738,8 +7690,7 @@ def create_linestring_gdf(gdf: gpd.geodataframe.GeoDataFrame) -> gpd.geodatafram
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
@@ -6747,46 +7698,47 @@ def create_linestring_gdf(gdf: gpd.geodataframe.GeoDataFrame) -> gpd.geodatafram
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')
+ 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()
@@ -6794,30 +7746,27 @@ 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:
- """Calculating orientations from LineStrings
+def extract_orientations_from_map(
+ gdf: gpd.geodataframe.GeoDataFrame, dz: str = "dZ"
+) -> gpd.geodataframe.GeoDataFrame:
+ """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
@@ -6847,8 +7796,7 @@ def extract_orientations_from_map(gdf: gpd.geodataframe.GeoDataFrame,
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
@@ -6856,66 +7804,67 @@ def extract_orientations_from_map(gdf: gpd.geodataframe.GeoDataFrame,
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')
+ 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:
- """Calculating the minimal distance between two LineStrings
+def calculate_distance_linestrings(
+ ls1: shapely.geometry.linestring.LineString,
+ ls2: shapely.geometry.linestring.LineString,
+) -> float:
+ """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)])``
@@ -6923,15 +7872,14 @@ def calculate_distance_linestrings(ls1: shapely.geometry.linestring.LineString,
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
@@ -6950,8 +7898,7 @@ def calculate_distance_linestrings(ls1: shapely.geometry.linestring.LineString,
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
@@ -6959,30 +7906,29 @@ def calculate_distance_linestrings(ls1: shapely.geometry.linestring.LineString,
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')
+ 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)
@@ -6990,18 +7936,18 @@ def calculate_distance_linestrings(ls1: shapely.geometry.linestring.LineString,
return distance
-def calculate_orientations_from_strike_lines(gdf: gpd.geodataframe.GeoDataFrame) -> gpd.geodataframe.GeoDataFrame:
- """Calculating orientations based on LineStrings representing strike lines
+def calculate_orientations_from_strike_lines(
+ gdf: gpd.geodataframe.GeoDataFrame,
+) -> gpd.geodataframe.GeoDataFrame:
+ """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
@@ -7011,8 +7957,7 @@ def calculate_orientations_from_strike_lines(gdf: gpd.geodataframe.GeoDataFrame)
Fixing indexing issue.
Example
- _______
-
+ -------
>>> # Loading Libraries and creating LineString
>>> import gemgis as gg
>>> from shapely.geometry import LineString
@@ -7042,8 +7987,7 @@ def calculate_orientations_from_strike_lines(gdf: gpd.geodataframe.GeoDataFrame)
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
@@ -7051,71 +7995,87 @@ def calculate_orientations_from_strike_lines(gdf: gpd.geodataframe.GeoDataFrame)
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')
+ 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
@@ -7127,27 +8087,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
@@ -7155,13 +8118,12 @@ def calculate_orientations_from_strike_lines(gdf: gpd.geodataframe.GeoDataFrame)
# Loading GPX Files
###################
-def load_gpx(path: str,
- layer: Union[int, str] = 'tracks') -> Collection:
- """Loading a GPX file as collection
- Parameters
- __________
+def load_gpx(path: str, layer: Union[int, str] = "tracks") -> Collection:
+ """Load a GPX file as collection.
+ Parameters
+ ----------
path : str
Path to the GPX file, e.g. ``path='file.gpx'``
@@ -7170,16 +8132,14 @@ def load_gpx(path: str,
``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')
@@ -7187,44 +8147,52 @@ def load_gpx(path: str,
See Also
- ________
-
+ --------
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
+ 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')
+ 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:
- """Loading a GPX file as dict
+def load_gpx_as_dict(path: str, layer: Union[int, str] = "tracks") -> Collection:
+ """Load a GPX file as dict.
Parameters
- __________
-
+ ----------
path : str
Path to the GPX file, e.g. ``path='file.gpx'``
@@ -7233,16 +8201,16 @@ def load_gpx_as_dict(path: str,
``tracks``
Returns
- _______
-
+ -------
gpx_dict : dict
Dict containing the GPX data
.. versionadded:: 1.0.x
- Example
- _______
+ .. versionchanged:: 1.2
+ Example
+ -------
>>> # Loading Libraries and File
>>> import gemgis as gg
>>> gpx = gg.vector.load_gpx_as_dict(path='file.gpx', layer='tracks')
@@ -7268,33 +8236,43 @@ def load_gpx_as_dict(path: str,
(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
+ .. versionadded:: 1.0.x
+
+ .. versionchanged:: 1.2
+
"""
+ # 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')
+ 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]
@@ -7302,31 +8280,30 @@ 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:
- """Loading a GPX file as Shapely Geometry
+def load_gpx_as_geometry(
+ path: str, layer: Union[int, str] = "tracks"
+) -> shapely.geometry.base.BaseGeometry:
+ """Load a GPX file as Shapely Geometry.
Parameters
- __________
-
+ ----------
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``
Returns
- _______
-
+ -------
shape : shapely.geometry.base.BaseGeometry
Shapely BaseGeometry containing the geometry data of the GPX file
.. versionadded:: 1.0.x
- Example
- _______
+ .. versionchanged:: 1.2
+ Example
+ -------
>>> # Loading Libraries and File
>>> import gemgis as gg
>>> gpx = gg.vector.load_gpx_as_geometry(path='file.gpx', layer='tracks')
@@ -7335,40 +8312,50 @@ def load_gpx_as_geometry(path: str,
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
+ 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')
+ 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)
@@ -7376,17 +8363,105 @@ def load_gpx_as_geometry(path: str,
return shape
+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
#########################
-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
- __________
+def sort_by_stratigraphy(
+ gdf: gpd.geodataframe.GeoDataFrame,
+ stratigraphy: List[str],
+ formation_column: str = "formation",
+) -> gpd.geodataframe.GeoDataFrame:
+ """Sort a GeoDataFrame by a provided list of Stratigraphic Units.
+ Parameters
+ ----------
gdf : gpd.geodataframe.GeoDataFrame
GeoDataFrame containing the unsorted input polygons
@@ -7397,16 +8472,14 @@ def sort_by_stratigraphy(gdf: gpd.geodataframe.GeoDataFrame,
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
@@ -7439,60 +8512,60 @@ def sort_by_stratigraphy(gdf: gpd.geodataframe.GeoDataFrame,
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')
+ 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
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]``
+ 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
Example
- _______
-
+ -------
>>> # Loading Libraries
>>> import gemgis as gg
@@ -7505,68 +8578,66 @@ 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')
+ 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:
- """Checking and setting the dtypes of the input data 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:
+ """Check and set the dtypes of the input data GeoDataFrame.
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
Example
- _______
-
+ -------
>>> # Loading Libraries and File
>>> import gemgis as gg
>>> import geopandas as gpd
@@ -7576,34 +8647,41 @@ def set_dtype(gdf: gpd.geodataframe.GeoDataFrame,
>>> 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')
+ 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:
@@ -7636,37 +8714,35 @@ 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]:
- """Extracting faces from PyVista PolyData as Shapely Polygons
+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]:
+ """Extract faces from PyVista PolyData as Shapely Polygons.
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
Example
- _______
-
+ -------
>>> # Importing Libraries and File
>>> import gemgis as gg
>>> import pyvista as pv
@@ -7687,26 +8763,26 @@ def create_polygons_from_faces(mesh: pv.core.pointset.PolyData,
>>> # 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):
- 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:]
@@ -7724,92 +8800,97 @@ 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]:
- """Unifying adjacent triangular polygons to form larger objects
+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]:
+ """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
+ 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
Example
- _______
-
+ -------
>>> # Loading Libraries and File
>>> import gemgis as gg
>>> 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
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)
@@ -7822,98 +8903,102 @@ 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]:
- """Unifying adjacent LineStrings to form LineStrings with multiple vertices
+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]:
+ """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
-
+ 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
Example
- _______
-
+ -------
>>> # Loading Libraries and File
>>> import gemgis as gg
>>> 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
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)
@@ -7923,60 +9008,52 @@ 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]):
- """Function to create one hexagon
+def create_hexagon(center: shapely.geometry.Point, radius: Union[int, float]):
+ """Create one hexagon.
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
+ See Also
+ --------
+ create_hexagon_grid : Create 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')
+ 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)
@@ -7986,62 +9063,63 @@ 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):
- """Function to create a grid of hexagons based on a GeoDataFrame containing Polygons and a radius provided for the single hexagons
+def create_hexagon_grid(
+ gdf: gpd.GeoDataFrame, radius: Union[int, float], crop_gdf: bool = True
+):
+ """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
-
+ 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
.. versionchanged:: 1.1.3
Optimized creation of hexagon
- See also
- ________
-
- create_hexagon : Creating one hexagon based on a given center and radius
+ See Also
+ --------
+ create_hexagon : Create 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')
+ 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 = []
@@ -8055,101 +9133,101 @@ 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:
- """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)
-
+def create_voronoi_polygons(
+ gdf: gpd.geodataframe.GeoDataFrame,
+) -> gpd.geodataframe.GeoDataFrame:
+ """Create Voronoi Polygons from Point GeoDataFrame using the SciPy Spatial Voronoi class.
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
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')
+ 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)
# 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))]
# 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
diff --git a/gemgis/visualization.py b/gemgis/visualization.py
index a95719ab..badd13bf 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 (all(gdf.has_z == False)) 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]-1, ((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
@@ -1617,7 +1679,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 +1687,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
@@ -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)) == True and all(pd.isna(geo_model.surfaces.df.edges)) == True:
- 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]
@@ -1714,20 +1787,37 @@ 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
- surfaces_poly[list_surfaces[index]] = [surf, geo_model.structural_frame.elements_colors[index]]
+ # 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
-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
@@ -1780,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)
@@ -1809,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
@@ -1893,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]
@@ -1913,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)
@@ -1928,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
@@ -1936,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
@@ -1967,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]
@@ -2036,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
@@ -2127,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())
@@ -2142,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
@@ -2229,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)
@@ -2250,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
@@ -2324,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
@@ -2413,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
@@ -2525,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)
@@ -2558,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
@@ -2598,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
@@ -2673,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
@@ -2798,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):
@@ -2814,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]
@@ -2850,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)
@@ -2871,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
@@ -2941,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):
@@ -3020,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
@@ -3137,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
@@ -3215,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()]
@@ -3239,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
@@ -3333,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)
@@ -3437,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)
@@ -3516,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
@@ -3548,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()
@@ -3580,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
@@ -3616,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()
@@ -3647,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
@@ -3731,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]
@@ -3777,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))]
@@ -3819,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)
@@ -4083,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()
@@ -4108,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)
@@ -4372,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()
@@ -4397,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)
@@ -4661,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()
@@ -4673,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
@@ -4706,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
@@ -4762,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
@@ -4812,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 = []
@@ -4837,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
@@ -4862,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]
@@ -4881,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
@@ -4915,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)
@@ -4946,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
@@ -4976,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()
diff --git a/gemgis/web.py b/gemgis/web.py
index 80fb94b0..8f9b60b8 100644
--- a/gemgis/web.py
+++ b/gemgis/web.py
@@ -33,23 +33,28 @@
###############################
-def load_wms(url: str): # -> owslib.wms.WebMapService:
- """Loading a WMS Service by URL
+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
_______
wms : owslib.map.wms111.WebMapService
- OWSLib WebMapService Object
+ OWSLib WebMapService Object.
.. versionadded:: 1.0.x
+ .. versionchanged:: 1.2
+
Example
_______
@@ -69,90 +74,102 @@ def load_wms(url: str): # -> owslib.wms.WebMapService:
"""
# Trying to import owslib but returning error if owslib is not installed
try:
- import owslib
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")
# Requesting the WMS Service or returning an error if a module may be missing
try:
- wms = owslib.wms.WebMapService(url)
+ 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,
- 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
+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:
+ """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 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``
-
- create_directory : bool
- 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``.
+ 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
_______
@@ -162,6 +179,8 @@ def load_as_map(url: str,
.. versionadded:: 1.0.x
+ .. versionchanged:: 1.2
+
Example
_______
@@ -178,66 +197,69 @@ def load_as_map(url: str,
load_as_array : Load Map as array from WMS Service
"""
-
# 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(
- '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")
# 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
@@ -256,100 +278,113 @@ 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)
+ 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,
- 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
+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:
+ """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 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
_______
wms_array: np.ndarray
- OWSlib map object loaded as np.ndarray
-
- .. versionadded:: 1.0.x
+ OWSlib map object loaded as np.ndarray.
Example
_______
@@ -372,73 +407,82 @@ 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
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(
- '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")
# 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
@@ -457,24 +501,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,
- 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())
@@ -488,22 +538,24 @@ def load_as_array(url: str,
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
_______
@@ -519,67 +571,69 @@ 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:
- 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(
- '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:
- wfs = owslib.wfs.WebFeatureService(url)
+ wfs = WebFeatureService(url)
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:
- """Requesting data from a WFS Service
+def load_as_gpd(
+ url: str, typename: str = None, outputformat: str = None
+) -> gpd.geodataframe.GeoDataFrame:
+ """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
_______
@@ -587,8 +641,12 @@ def load_as_gpd(url: str,
>>> 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
________
@@ -596,37 +654,35 @@ def load_as_gpd(url: str,
load_wfs : Load WFS Service
"""
-
# 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(
- '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)
@@ -635,19 +691,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)
@@ -660,22 +723,24 @@ def load_as_gpd(url: str,
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
_______
@@ -693,67 +758,69 @@ 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:
- 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(
- '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 = owslib.wcs.WebCoverageService(url)
+ wcs = WebCoverageService(url)
return wcs
-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
+def create_request(
+ wcs_url: str,
+ version: str,
+ identifier: str,
+ form: str,
+ extent: List[Union[float, int]],
+ name: str = "test.tif",
+) -> str:
+ """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
_______
@@ -774,77 +841,102 @@ def create_request(wcs_url: str,
load_as_files : Download WCS data files
"""
-
# 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(
- '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):
- """Executing WCS request and downloading file into specified folder
+def load_as_file(
+ url: str, path: str, overwrite_file: bool = False, create_directory: bool = False
+):
+ """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
_______
@@ -868,32 +960,31 @@ def load_as_file(url: str,
load_as_files : Download WCS data files
"""
-
# 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(
- '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)
@@ -910,58 +1001,65 @@ 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):
- """Executing WCS requests and downloading files into specified folder
+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,
+):
+ """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
_______
@@ -985,49 +1083,49 @@ 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(
- '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]
@@ -1036,41 +1134,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
diff --git a/pyproject.toml b/pyproject.toml
index 4d80497d..f3104f8c 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -22,15 +22,15 @@ 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',
'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',
]
@@ -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 2af1968c..f2d0859a 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,10 +1,16 @@
-# 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 will also install numpy, pandas, shapely, pyogrio, and pyproj
+geopandas>=1.0.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.8
+rasterio>=1.4.3
+affine>=2.4.0
# pyvista also install pooch and matplotlib
-pyvista>=0.42.2
\ No newline at end of file
+pyvista>=0.44.2
+matplotlib>=3.9.3
\ No newline at end of file
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_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
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):