Source code for eodal.mapper.feature

"""
Module defining geographic features for mapping.

.. versionadded:: 0.1.1

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 pandas as pd

from shapely import wkt
from shapely.errors import ShapelyError
from shapely.geometry import MultiPoint, MultiPolygon, Point, Polygon
from typing import Any, Dict, Optional

allowed_geom_types = [MultiPoint, MultiPolygon, Point, Polygon]


[docs] class Feature: """ Generic class for a geographic feature :attrib name: name of the feature (used for identification) :attrib geometry: `shapely` geometry of the feature in a spatial reference system :attrib epgs: spatial coordinate reference system of the feature as EPSG code :attrib attributes: optional attributes of the feature """
[docs] def __init__( self, name: str, geometry: MultiPoint | MultiPolygon | Point | Polygon, epsg: int, attributes: Optional[Dict[str, Any] | pd.Series] = {}, ): """ Class constructor :param name: name of the feature (used for identification) :param geometry: `shapely` geometry of the feature in a spatial reference system :param epgs: spatial coordinate reference system of the feature as EPSG code :param attributes: optional attributes of the feature """ # check inputs if name == "": raise ValueError("Empty feature names are not allowed") if type(geometry) not in allowed_geom_types: raise ValueError(f"geometry must of type {allowed_geom_types}") if type(epsg) != int or epsg <= 0: raise ValueError("EPSG code must be a positive integer value") if not isinstance(attributes, pd.Series) and not isinstance(attributes, dict): raise ValueError("Attributes must pd.Series or dictionary") self._name = name self._geometry = geometry self._epsg = epsg self._attributes = attributes
def __repr__(self) -> str: return ( f"Name\t\t{self.name}\nGeometry\t" + f"{self.geometry}\nEPSG Code\t{self.epsg}" + f"\nAttributes\t{self.attributes}" ) @property def attributes(self) -> Dict: """feature attributes""" if isinstance(self._attributes, pd.Series): return self._attributes.to_dict() else: return self._attributes @property def epsg(self) -> int: """the feature coordinate reference system as EPSG code""" return self._epsg @property def geometry(self) -> MultiPoint | MultiPolygon | Point | Polygon: """the feature geometry""" return self._geometry @property def name(self) -> str: """the feature name""" return self._name
[docs] @classmethod def from_geoseries(cls, gds: gpd.GeoSeries): """ Feature object from `GeoSeries` :param gds: `GeoSeries` to cast to Feature :returns: Feature instance created from input `GeoSeries` """ return cls( name=gds.name, geometry=gds.geometry.values[0], epsg=gds.crs.to_epsg(), attributes=gds.attrs, )
[docs] @classmethod def from_dict(cls, dictionary: Dict[str, Any]): """ Feature object from Python dictionary :param dictionary: Python dictionary object to cast to Feature :returns: Feature instance created from input dictionary """ try: return cls( name=dictionary["name"], geometry=wkt.loads(dictionary["geometry"]), epsg=int(dictionary["epsg"]), attributes=dictionary["attributes"], ) except KeyError: raise ValueError( "Dictionary does not have fields required to instantiate a new Feature" ) except ShapelyError as e: raise ValueError(f"Invalid Geometry: {e}")
[docs] def to_epsg(self, epsg: int): """ Projects the feature into a different spatial reference system identified by an EPSG code. Returns a copy of the Feature with transformed coordinates. :param epsg: EPSG code of the reference system the feature is project to :returns: new Feature instance in the target spatial reference system """ gds = self.to_geoseries() gds_projected = gds.to_crs(epsg=epsg) return Feature.from_geoseries(gds_projected)
[docs] def to_geoseries(self) -> gpd.GeoSeries: """ Casts the feature to a GeoSeries object :returns: Feature object casted as `GeoSeries` """ gds = gpd.GeoSeries([self.geometry], crs=f"EPSG:{self.epsg}") # add attributes from Feature gds.attrs = self.attributes gds.name = self.name return gds
[docs] def to_dict(self) -> Dict[str, Any]: """ Casts feature to a pure Python dictionary :returns: Feature object as pure Python dictionary """ feature_dict = {} feature_dict["name"] = self.name feature_dict["epsg"] = self.epsg feature_dict["geometry"] = self.geometry.wkt feature_dict["attributes"] = self.attributes return feature_dict