Source code for eodal.core.utils.geometry
"""
Utils for working with ``shapely.geometry`` and ``geopandas.GeoDataFrame`` like objects.
Copyright (C) 2022 Lukas Valentin Graf
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
from __future__ import annotations
import geopandas as gpd
import warnings
from pathlib import Path
from shapely.geometry import Polygon
from shapely.geometry import MultiPolygon
from typing import Union
from typing import List
from typing import Optional
[docs]
def read_geometries(in_dataset: Union[Path, gpd.GeoDataFrame]) -> gpd.GeoDataFrame:
"""
Returns a geodataframe containing vector features
:param in_dataset:
path-like object ``GeoSeries`` or ``GeoDataFrame``
:returns:
``GeoDataFrame`` representation of vector features
"""
if isinstance(in_dataset, gpd.GeoDataFrame):
return in_dataset.copy()
elif isinstance(in_dataset, gpd.GeoSeries):
return gpd.GeoDataFrame(geometry=in_dataset.copy())
elif isinstance(in_dataset, Path):
try:
return gpd.read_file(in_dataset)
except Exception as e:
raise Exception from e
else:
raise NotImplementedError(
f"Could not read geometries of input type {type(in_dataset)}"
)
[docs]
def check_geometry_types(
in_dataset: Union[Path, gpd.GeoDataFrame],
allowed_geometry_types: List[str],
remove_empty_geoms: Optional[bool] = True,
) -> gpd.GeoDataFrame:
"""
Checks if a ``GeoDataFrame`` contains allowed ``shapely.geometry``
types, only. Raises an error if geometry types other than those allowed are
found.
:param allowed_geometry_types:
list of allowed geometry types
:param in_dataset:
file with vector geometries (e.g., ESRI shapefile or GEOJSON) or geodataframe
to check
:param remove_empty_geoms:
when True (default) removes features with empty (None-type) geometry from
``in_dataset`` before carrying out the type checking.
:return:
``GeoDataFrame`` with checked (and optionally cleaned) vector features.
"""
# read dataset
gdf = read_geometries(in_dataset)
# check for None geometries (might happen when buffering polygons)
if remove_empty_geoms:
num_none_type_geoms = gdf[gdf.geometry == None].shape[0] # noqa: E711
if num_none_type_geoms > 0:
warnings.warn(
f"Ignoring {num_none_type_geoms} records where "
f"geometries are of type None"
)
gdf = gdf.drop(gdf[gdf.geometry == None].index) # noqa: E711
# check for allowed geometry types
gdf_aoi_geoms_types = list(gdf.geom_type.unique())
not_allowed_types = [
x for x in gdf_aoi_geoms_types if x not in allowed_geometry_types
]
if len(not_allowed_types) > 0:
raise ValueError(
"Encounter geometry types not allowed for reading "
f"band data: ({not_allowed_types})"
)
return gdf
[docs]
def convert_3D_2D(geometry: gpd.GeoSeries) -> gpd.GeoSeries:
"""
Takes a GeoSeries of 3D Multi/Polygons (has_z) and returns a list of
2D Multi/Polygons.
Snippet taken from https://gist.github.com/rmania/8c88377a5c902dfbc134795a7af538d8
(accessed latest Jan 18th 2021)
:param geometry:
``GeoSeries`` from ``GeoDataFrame``
:returns:
updated ``GeoSeries`` without third dimension (z)
"""
new_geo = []
for p in geometry:
if p.has_z:
if p.geom_type == "Polygon":
lines = [xy[:2] for xy in list(p.exterior.coords)]
new_p = Polygon(lines)
new_geo.append(new_p)
elif p.geom_type == "MultiPolygon":
new_multi_p = []
for ap in p:
lines = [xy[:2] for xy in list(ap.exterior.coords)]
new_p = Polygon(lines)
new_multi_p.append(new_p)
new_geo.append(MultiPolygon(new_multi_p))
else:
new_geo = geometry
break
return new_geo
[docs]
def multi_to_single_points(point_features: gpd.GeoDataFrame | Path) -> gpd.GeoDataFrame:
"""
Casts MultiPoint geometries to single point geometries by calling
`gpd.GeoDataFrame.explode()`
:param point_features:
point features to cast
:returns:
casted point features or input if all geometries are already single parted
"""
gdf = check_geometry_types(
in_dataset=point_features, allowed_geometry_types=["Point", "MultiPoint"]
)
if (gdf.geometry.type == "MultiPoint").any():
gdf = gdf.explode(index_parts=False)
return gdf