Source code for eodal.utils.reprojection
"""
Functions for reprojecting vector and raster data from one spatial
coordinate reference system into another one.
Copyright (C) 2022 Samuel Wildhaber and 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 numpy as np
import rasterio as rio
import geopandas as gpd
from collections import namedtuple
from rasterio import Affine
from rasterio.crs import CRS
from shapely.geometry import box, MultiPolygon, Polygon
from pathlib import Path
from typing import NamedTuple, Optional, Tuple, Union
from geopandas import GeoDataFrame
from eodal.core.utils.geometry import read_geometries
UTMZone = namedtuple("Point", "zone hemisphere")
def _infer_utm_zone(shape: Polygon | MultiPolygon) -> NamedTuple:
"""
Infer the UTM zone from a geometry provided in geographic coordinates.
The geometry must implement the centroid attribute.
:param shape:
geometry in geographic coordinates (WGS84) for which to check
the corresponding UTM zone
:returns:
`NamedTuple` with two-digit UTM `zone` number and `hemisphere`
(north or south)
"""
centroid = shape.centroid
lon = centroid.x
lat = centroid.y
if lat > 84 or lat < -80:
raise Exception("UTM Zones only valid within [-80, 84] latitude")
zone = int((lon + 180) / 6 + 1)
hemisphere = "north" if lat > 0 else "south"
return UTMZone(zone, hemisphere)
def _epsg_from_utm_zone(utmzone: UTMZone) -> int:
"""
EPSG code from UTM zone number and hemisphere
:param utmzone:
`NamedTuple` with two-digit UTM `zone` and `hemisphere`
:returns:
integer EPSG code
"""
epsg_str = "32"
if utmzone.hemisphere == "north":
epsg_str += "6"
elif utmzone.hemisphere == "south":
epsg_str += "7"
else:
raise ValueError("Not a valid hemisphere (allowed: north or south)")
epsg_str += str(utmzone.zone)
return int(epsg_str)
[docs]
def infer_utm_zone(shape: Polygon | MultiPolygon) -> int:
"""
Returns the EPSG code of the UTM zone a geometry with geographic coordinates
lies in (i.e., its centroid)
:param shape:
geometry in geographic coordinates (WGS84) for which to check
the corresponding UTM zone
"""
utmzone = _infer_utm_zone(shape)
return _epsg_from_utm_zone(utmzone)
[docs]
def check_aoi_geoms(
in_dataset: Union[Path, gpd.GeoDataFrame],
full_bounding_box_only: bool,
fname_raster: Optional[Path] = None,
raster_crs: Optional[Union[int, CRS]] = None,
) -> GeoDataFrame:
"""
Checks the provided vector file. If necessary it reprojects
the vector data in the reference system of the provided raster
data. If the full bounding box shall be used (e.g., the hull
encompassing all provided vector geometries) it only returns
this geometry (of type Polygon).
NOTE:
Does not check for spatial intersects, overlaps, etc.
:param in_file_aoi:
vector file (e.g., ESRI shapefile or geojson) defining geometry/ies
for which to extract raster data.
:param fname_raster:
raster file to which to map the vector features. Can be ignored if
a ``raster_crs`` is available
:param full_bounding_box_only:
if set to False, will only extract the data for those geometry/ies
defined in in_file_aoi. If set to False, returns the data for the
full extent (hull) of all features (geometries) in in_file_aoi.
:param raster_crs:
spatial reference system of the raster as EPSG code or ``CRS`` object.
Can be ignored if ``fname_sat`` is available.
:return:
GeoDataFrame with one up to many vector geometries
"""
# check for vector features defining AOI
gdf = read_geometries(in_dataset)
# check if the spatial reference systems match
sat_crs = None
if fname_raster is not None:
sat_crs = rio.open(fname_raster).crs
# if the raster has no inherent CRS use the user-defined one
if raster_crs is not None and sat_crs is None:
sat_crs = raster_crs
# reproject vector data if necessary
if gdf.crs != sat_crs:
gdf.to_crs(sat_crs, inplace=True)
# if the the entire bounding box shall be extracted
# we need the hull encompassing all geometries in gdf
if full_bounding_box_only:
bbox = box(*gdf.total_bounds)
gdf = gpd.GeoDataFrame(geometry=gpd.GeoSeries(bbox))
return gdf
[docs]
def reproject_raster_dataset(
raster: Union[Path, np.ndarray], **kwargs
) -> Tuple[Union[Path, np.ndarray], Affine]:
"""
Re-projects a raster dataset into another spatial coordinate reference
system by calling ``rasterio.warp.reproject``.
:param raster:
either a file-path to a raster dataset or a numpy array
containing band data to reproject
:param kwargs:
kwargs required by ``rasterio.warp.reproject``. See rasterio's docs
for more information.
:return:
tuple containing the reprojected raster dataset (file-path or array)
and the ``Affine`` transformation parameters of the reprojected dataset
"""
try:
dst, transform = rio.warp.reproject(source=raster, **kwargs)
except Exception as e:
raise Exception from e
return dst, transform