mirror of
https://github.com/wassname/geopandas.git
synced 2026-09-21 13:00:14 +08:00
REF: move element-wise geo ops to array class (#993)
This commit is contained in:
@@ -0,0 +1,382 @@
|
||||
import warnings
|
||||
|
||||
import numpy as np
|
||||
|
||||
from shapely.geometry.base import BaseGeometry
|
||||
from shapely.ops import unary_union
|
||||
|
||||
import shapely.affinity
|
||||
|
||||
|
||||
def _binary_geo(op, left, right):
|
||||
# type: (str, GeometryArray, [GeometryArray/BaseGeometry]) -> GeometryArray
|
||||
""" Apply geometry-valued operation
|
||||
|
||||
Supports:
|
||||
|
||||
- difference
|
||||
- symmetric_difference
|
||||
- intersection
|
||||
- union
|
||||
|
||||
Parameters
|
||||
----------
|
||||
op: string
|
||||
right: GeometryArray or single shapely BaseGeoemtry
|
||||
"""
|
||||
if isinstance(right, BaseGeometry):
|
||||
# intersection can return empty GeometryCollections, and if the
|
||||
# result are only those, numpy will coerce it to empty 2D array
|
||||
data = np.empty(len(left), dtype=object)
|
||||
data[:] = [getattr(s, op)(right) for s in left.data]
|
||||
return GeometryArray(data)
|
||||
elif isinstance(right, GeometryArray):
|
||||
if len(left) != len(right):
|
||||
msg = (
|
||||
"Lengths of inputs to not match. "
|
||||
"Left: {0}, Right: {1}".format(len(left), len(right)))
|
||||
raise ValueError(msg)
|
||||
data = np.empty(len(left), dtype=object)
|
||||
data[:] = [getattr(this_elem, op)(other_elem)
|
||||
for this_elem, other_elem in zip(left.data, right.data)]
|
||||
return GeometryArray(data)
|
||||
else:
|
||||
raise TypeError(
|
||||
"Type not known: {0} vs {1}".format(type(left), type(right)))
|
||||
|
||||
|
||||
def _binary_op(op, left, right, *args, **kwargs):
|
||||
# type: (str, GeometryArray, GeometryArray/BaseGeometry, args/kwargs)
|
||||
# -> array
|
||||
"""Binary operation on GeometryArray that returns a ndarray"""
|
||||
if op in ['distance', 'project']:
|
||||
null_value = np.nan
|
||||
elif op == 'relate':
|
||||
null_value = None
|
||||
else:
|
||||
null_value = False
|
||||
if op in ['distance', 'project']:
|
||||
dtype = float
|
||||
elif op == 'relate':
|
||||
dtype = object
|
||||
else:
|
||||
dtype = bool
|
||||
|
||||
if isinstance(right, BaseGeometry):
|
||||
data = [
|
||||
getattr(s, op)(right, *args, **kwargs) if s else null_value
|
||||
for s in left.data]
|
||||
return np.array(data, dtype=dtype)
|
||||
elif isinstance(right, GeometryArray):
|
||||
if len(left) != len(right):
|
||||
msg = (
|
||||
"Lengths of inputs to not match. "
|
||||
"Left: {0}, Right: {1}".format(len(left), len(right)))
|
||||
raise ValueError(msg)
|
||||
data = [
|
||||
getattr(this_elem, op)(other_elem, *args, **kwargs)
|
||||
if not this_elem.is_empty | other_elem.is_empty else null_value
|
||||
for this_elem, other_elem in zip(left.data, right.data)]
|
||||
return np.array(data, dtype=dtype)
|
||||
else:
|
||||
raise TypeError(
|
||||
"Type not known: {0} vs {1}".format(type(left), type(right)))
|
||||
|
||||
|
||||
def _unary_geo(op, left, *args, **kwargs):
|
||||
# type: (str, GeometryArray) -> GeometryArray
|
||||
"""Unary operation that returns new geometries"""
|
||||
# ensure 1D output, see note above
|
||||
data = np.empty(len(left), dtype=object)
|
||||
data[:] = [getattr(geom, op) for geom in left.data]
|
||||
return GeometryArray(data)
|
||||
|
||||
|
||||
def _unary_op(op, left, null_value=False):
|
||||
# type: (str, GeometryArray, Any) -> array
|
||||
"""Unary operation that returns a Series"""
|
||||
data = [getattr(geom, op, null_value) for geom in left.data]
|
||||
return np.array(data, dtype=np.dtype(type(null_value)))
|
||||
|
||||
|
||||
def _affinity_method(op, left, *args, **kwargs):
|
||||
# type: (str, GeometryArray, ...) -> GeometryArray
|
||||
data = [getattr(shapely.affinity, op)(s, *args, **kwargs)
|
||||
for s in left.data]
|
||||
return GeometryArray(np.array(data, dtype=object))
|
||||
|
||||
|
||||
class GeometryArray:
|
||||
"""
|
||||
Class wrapping a numpy array of Shapely objects and
|
||||
holding the array-based implementations.
|
||||
"""
|
||||
|
||||
def __init__(self, data):
|
||||
if isinstance(data, self.__class__):
|
||||
data = data.data
|
||||
elif not isinstance(data, np.ndarray):
|
||||
raise ValueError(
|
||||
"'data' should be array of geometry objects. Use from_shapely,"
|
||||
" from_wkb, from_wkt functions to construct a GeometryArray.")
|
||||
elif not data.ndim == 1:
|
||||
raise ValueError(
|
||||
"'data' should be a 1-dimensional array of geometry objects.")
|
||||
self.data = data
|
||||
|
||||
def __len__(self):
|
||||
return len(self.data)
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Geometry related methods
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
@property
|
||||
def is_valid(self):
|
||||
return _unary_op('is_valid', self, null_value=False)
|
||||
|
||||
@property
|
||||
def is_empty(self):
|
||||
return _unary_op('is_empty', self, null_value=False)
|
||||
|
||||
@property
|
||||
def is_simple(self):
|
||||
return _unary_op('is_simple', self, null_value=False)
|
||||
|
||||
@property
|
||||
def is_ring(self):
|
||||
# operates on the exterior, so can't use _unary_op()
|
||||
return np.array(
|
||||
[geom.exterior.is_ring for geom in self.data], dtype=bool)
|
||||
|
||||
@property
|
||||
def is_closed(self):
|
||||
return _unary_op('is_closed', self, null_value=False)
|
||||
|
||||
@property
|
||||
def has_z(self):
|
||||
return _unary_op('has_z', self, null_value=False)
|
||||
|
||||
@property
|
||||
def geom_type(self):
|
||||
return _unary_op('geom_type', self, null_value=None)
|
||||
|
||||
@property
|
||||
def area(self):
|
||||
return _unary_op('area', self, null_value=np.nan)
|
||||
|
||||
@property
|
||||
def length(self):
|
||||
return _unary_op('length', self, null_value=np.nan)
|
||||
|
||||
#
|
||||
# Unary operations that return new geometries
|
||||
#
|
||||
|
||||
@property
|
||||
def boundary(self):
|
||||
return _unary_geo('boundary', self)
|
||||
|
||||
@property
|
||||
def centroid(self):
|
||||
return _unary_geo('centroid', self)
|
||||
|
||||
@property
|
||||
def convex_hull(self):
|
||||
return _unary_geo('convex_hull', self)
|
||||
|
||||
@property
|
||||
def envelope(self):
|
||||
return _unary_geo('envelope', self)
|
||||
|
||||
@property
|
||||
def exterior(self):
|
||||
return _unary_geo('exterior', self)
|
||||
|
||||
@property
|
||||
def interiors(self):
|
||||
has_non_poly = False
|
||||
inner_rings = []
|
||||
for geom in self.data:
|
||||
interior_ring_seq = getattr(geom, 'interiors', None)
|
||||
# polygon case
|
||||
if interior_ring_seq is not None:
|
||||
inner_rings.append(list(interior_ring_seq))
|
||||
# non-polygon case
|
||||
else:
|
||||
has_non_poly = True
|
||||
inner_rings.append(None)
|
||||
if has_non_poly:
|
||||
warnings.warn(
|
||||
"Only Polygon objects have interior rings. For other "
|
||||
"geometry types, None is returned.")
|
||||
|
||||
return np.array(inner_rings, dtype=object)
|
||||
|
||||
def representative_point(self):
|
||||
# method and not a property -> can't use _unary_geo
|
||||
data = np.empty(len(self), dtype=object)
|
||||
data[:] = [geom.representative_point() for geom in self.data]
|
||||
return GeometryArray(data)
|
||||
|
||||
#
|
||||
# Binary predicates
|
||||
#
|
||||
|
||||
def covers(self, other):
|
||||
return _binary_op('covers', self, other)
|
||||
|
||||
def contains(self, other):
|
||||
return _binary_op('contains', self, other)
|
||||
|
||||
def crosses(self, other):
|
||||
return _binary_op('crosses', self, other)
|
||||
|
||||
def disjoint(self, other):
|
||||
return _binary_op('disjoint', self, other)
|
||||
|
||||
def equals(self, other):
|
||||
return _binary_op('equals', self, other)
|
||||
|
||||
def intersects(self, other):
|
||||
return _binary_op('intersects', self, other)
|
||||
|
||||
def overlaps(self, other):
|
||||
return _binary_op('overlaps', self, other)
|
||||
|
||||
def touches(self, other):
|
||||
return _binary_op('touches', self, other)
|
||||
|
||||
def within(self, other):
|
||||
return _binary_op('within', self, other)
|
||||
|
||||
def equals_exact(self, other, tolerance):
|
||||
return _binary_op('equals_exact', self, other, tolerance=tolerance)
|
||||
|
||||
def almost_equals(self, other, decimal):
|
||||
return _binary_op('almost_equals', self, other, decimal=decimal)
|
||||
|
||||
#
|
||||
# Binary operations that return new geometries
|
||||
#
|
||||
|
||||
def difference(self, other):
|
||||
return _binary_geo('difference', self, other)
|
||||
|
||||
def intersection(self, other):
|
||||
return _binary_geo('intersection', self, other)
|
||||
|
||||
def symmetric_difference(self, other):
|
||||
return _binary_geo('symmetric_difference', self, other)
|
||||
|
||||
def union(self, other):
|
||||
return _binary_geo('union', self, other)
|
||||
|
||||
#
|
||||
# Other operations
|
||||
#
|
||||
|
||||
def distance(self, other):
|
||||
return _binary_op('distance', self, other)
|
||||
|
||||
def buffer(self, distance, resolution=16, **kwargs):
|
||||
if isinstance(distance, np.ndarray):
|
||||
if len(distance) != len(self):
|
||||
raise ValueError("Length of distance sequence does not match "
|
||||
"length of the GeoSeries")
|
||||
data = [
|
||||
geom.buffer(dist, resolution, **kwargs)
|
||||
for geom, dist in zip(self.data, distance)]
|
||||
return GeometryArray(np.array(data, dtype=object))
|
||||
|
||||
data = [geom.buffer(distance, resolution, **kwargs)
|
||||
for geom in self.data]
|
||||
return GeometryArray(np.array(data, dtype=object))
|
||||
|
||||
def interpolate(self, distance, normalized=False):
|
||||
if isinstance(distance, np.ndarray):
|
||||
if len(distance) != len(self):
|
||||
raise ValueError("Length of distance sequence does not match "
|
||||
"length of the GeoSeries")
|
||||
data = [
|
||||
geom.interpolate(dist, normalized=normalized)
|
||||
for geom, dist in zip(self.data, distance)]
|
||||
return GeometryArray(np.array(data, dtype=object))
|
||||
|
||||
data = [geom.interpolate(distance, normalized=normalized)
|
||||
for geom in self.data]
|
||||
return GeometryArray(np.array(data, dtype=object))
|
||||
|
||||
def simplify(self, *args, **kwargs):
|
||||
# method and not a property -> can't use _unary_geo
|
||||
data = np.empty(len(self), dtype=object)
|
||||
data[:] = [geom.simplify(*args, **kwargs) for geom in self.data]
|
||||
return GeometryArray(data)
|
||||
|
||||
def project(self, other, normalized=False):
|
||||
return _binary_op('project', self, other, normalized=normalized)
|
||||
|
||||
def relate(self, other):
|
||||
return _binary_op('relate', self, other)
|
||||
|
||||
#
|
||||
# Reduction operations that return a Shapely geometry
|
||||
#
|
||||
|
||||
def unary_union(self):
|
||||
return unary_union(self.data)
|
||||
|
||||
#
|
||||
# Affinity operations
|
||||
#
|
||||
|
||||
def translate(self, xoff=0.0, yoff=0.0, zoff=0.0):
|
||||
return _affinity_method('translate', self, xoff, yoff, zoff)
|
||||
|
||||
def rotate(self, angle, origin='center', use_radians=False):
|
||||
return _affinity_method('rotate', self, angle, origin=origin,
|
||||
use_radians=use_radians)
|
||||
|
||||
def scale(self, xfact=1.0, yfact=1.0, zfact=1.0, origin='center'):
|
||||
return _affinity_method(
|
||||
'scale', self, xfact, yfact, zfact, origin=origin)
|
||||
|
||||
def skew(self, xs=0.0, ys=0.0, origin='center', use_radians=False):
|
||||
return _affinity_method('skew', self, xs, ys, origin=origin,
|
||||
use_radians=use_radians)
|
||||
|
||||
#
|
||||
# Coordinate related properties
|
||||
#
|
||||
|
||||
@property
|
||||
def x(self):
|
||||
"""Return the x location of point geometries in a GeoSeries"""
|
||||
if (self.geom_type == "Point").all():
|
||||
return _unary_op('x', self, null_value=np.nan)
|
||||
else:
|
||||
message = "x attribute access only provided for Point geometries"
|
||||
raise ValueError(message)
|
||||
|
||||
@property
|
||||
def y(self):
|
||||
"""Return the y location of point geometries in a GeoSeries"""
|
||||
if (self.geom_type == "Point").all():
|
||||
return _unary_op('y', self, null_value=np.nan)
|
||||
else:
|
||||
message = "y attribute access only provided for Point geometries"
|
||||
raise ValueError(message)
|
||||
|
||||
@property
|
||||
def bounds(self):
|
||||
# TODO fix for empty / missing geometries
|
||||
bounds = np.array([geom.bounds for geom in self.data])
|
||||
return bounds
|
||||
|
||||
@property
|
||||
def total_bounds(self):
|
||||
b = self.bounds
|
||||
return np.array((b[:, 0].min(), # minx
|
||||
b[:, 1].min(), # miny
|
||||
b[:, 2].max(), # maxx
|
||||
b[:, 3].max())) # maxy
|
||||
+79
-162
@@ -12,6 +12,8 @@ import shapely.affinity as affinity
|
||||
|
||||
import geopandas as gpd
|
||||
|
||||
from .array import GeometryArray
|
||||
|
||||
|
||||
try:
|
||||
from rtree.core import RTreeError
|
||||
@@ -22,91 +24,62 @@ except ImportError:
|
||||
HAS_SINDEX = False
|
||||
|
||||
|
||||
def _binary_geo(op, left, right):
|
||||
def _delegate_binary_method(op, this, other, *args, **kwargs):
|
||||
# type: (str, GeoSeries, GeoSeries) -> GeoSeries/Series
|
||||
this = this.geometry
|
||||
if isinstance(other, GeoPandasBase):
|
||||
this, other = this.align(other.geometry)
|
||||
|
||||
# TODO reenable for all operations once we use pyproj > 2
|
||||
# if this.crs != other.crs:
|
||||
# warn('GeoSeries crs mismatch: {0} and {1}'.format(this.crs,
|
||||
# other.crs))
|
||||
a_this = GeometryArray(this.values)
|
||||
other = GeometryArray(other.values)
|
||||
elif isinstance(other, BaseGeometry):
|
||||
a_this = GeometryArray(this.values)
|
||||
else:
|
||||
raise TypeError(type(this), type(other))
|
||||
|
||||
data = getattr(a_this, op)(other, *args, **kwargs)
|
||||
return data, this.index
|
||||
|
||||
|
||||
def _binary_geo(op, this, other):
|
||||
# type: (str, GeoSeries, GeoSeries) -> GeoSeries
|
||||
"""Binary operation on GeoSeries objects that returns a GeoSeries"""
|
||||
from .geoseries import GeoSeries
|
||||
if isinstance(right, GeoPandasBase):
|
||||
left = left.geometry
|
||||
left, right = left.align(right.geometry)
|
||||
|
||||
if left.crs != right.crs:
|
||||
warn('GeoSeries crs mismatch: {0} and {1}'.format(left.crs,
|
||||
right.crs))
|
||||
|
||||
# intersection can return empty GeometryCollections, and if the result
|
||||
# are only those, numpy will coerce it to empty 2D array
|
||||
data = np.empty(len(left), dtype=object)
|
||||
data[:] = [getattr(this_elem, op)(other_elem)
|
||||
for this_elem, other_elem in zip(left, right)]
|
||||
|
||||
return GeoSeries(data, index=left.index, crs=left.crs)
|
||||
|
||||
elif isinstance(right, BaseGeometry):
|
||||
# ensure 1D output, see note above
|
||||
data = np.empty(len(left), dtype=object)
|
||||
data[:] = [getattr(s, op)(right) for s in left.geometry]
|
||||
return GeoSeries(data, index=left.index, crs=left.crs)
|
||||
else:
|
||||
raise TypeError(type(left), type(right))
|
||||
geoms, index = _delegate_binary_method(op, this, other)
|
||||
return GeoSeries(geoms.data, index=index, crs=this.crs)
|
||||
|
||||
|
||||
def _binary_op(op, this, other, *args, **kwargs):
|
||||
# type: (str, GeoSeries, GeoSeries, args/kwargs) -> Series[bool]
|
||||
"""Binary operation on GeoSeries objects that returns a Series"""
|
||||
if op in ['distance', 'project']:
|
||||
null_value = np.nan
|
||||
elif op == 'relate':
|
||||
null_value = None
|
||||
data, index = _delegate_binary_method(op, this, other, *args, **kwargs)
|
||||
return Series(data, index=index)
|
||||
|
||||
|
||||
def _delegate_property(op, this):
|
||||
# type: (str, GeoSeries) -> GeoSeries/Series
|
||||
a_this = GeometryArray(this.geometry.values)
|
||||
data = getattr(a_this, op)
|
||||
if isinstance(data, GeometryArray):
|
||||
from .geoseries import GeoSeries
|
||||
return GeoSeries(data.data, index=this.index, crs=this.crs)
|
||||
else:
|
||||
null_value = False
|
||||
if op in ['distance', 'project']:
|
||||
dtype = float
|
||||
elif op == 'relate':
|
||||
dtype = object
|
||||
else:
|
||||
dtype = bool
|
||||
|
||||
if isinstance(other, GeoPandasBase):
|
||||
|
||||
this = this.geometry
|
||||
this, other = this.align(other.geometry)
|
||||
|
||||
data = np.array(
|
||||
[getattr(this_elem, op)(other_elem, *args, **kwargs)
|
||||
if not this_elem.is_empty | other_elem.is_empty else null_value
|
||||
for this_elem, other_elem in zip(this, other)],
|
||||
dtype=dtype)
|
||||
|
||||
return Series(data, index=this.index)
|
||||
|
||||
elif isinstance(other, BaseGeometry):
|
||||
data = np.array(
|
||||
[getattr(s, op)(other, *args, **kwargs) if s else null_value
|
||||
for s in this.geometry],
|
||||
dtype=dtype)
|
||||
return Series(data, index=this.index)
|
||||
else:
|
||||
raise TypeError(type(this), type(other))
|
||||
|
||||
|
||||
def _unary_geo(op, this):
|
||||
def _delegate_geo_method(op, this, *args, **kwargs):
|
||||
# type: (str, GeoSeries) -> GeoSeries
|
||||
"""Unary operation that returns a GeoSeries"""
|
||||
from .geoseries import GeoSeries
|
||||
# ensure 1D output, see note above
|
||||
data = np.empty(len(this), dtype=object)
|
||||
data[:] = [getattr(geom, op) for geom in this.geometry]
|
||||
a_this = GeometryArray(this.geometry.values)
|
||||
data = getattr(a_this, op)(*args, **kwargs).data
|
||||
return GeoSeries(data, index=this.index, crs=this.crs)
|
||||
|
||||
|
||||
def _unary_op(op, this, null_value=False):
|
||||
# type: (str, GeoSeries, Any) -> Series
|
||||
"""Unary operation that returns a Series"""
|
||||
return Series([getattr(geom, op, null_value) for geom in this.geometry],
|
||||
index=this.index, dtype=np.dtype(type(null_value)))
|
||||
|
||||
|
||||
class GeoPandasBase(object):
|
||||
_sindex = None
|
||||
_sindex_generated = False
|
||||
@@ -141,13 +114,13 @@ class GeoPandasBase(object):
|
||||
def area(self):
|
||||
"""Returns a ``Series`` containing the area of each geometry in the
|
||||
``GeoSeries``."""
|
||||
return _unary_op('area', self, null_value=np.nan)
|
||||
return _delegate_property('area', self)
|
||||
|
||||
@property
|
||||
def geom_type(self):
|
||||
"""Returns a ``Series`` of strings specifying the `Geometry Type` of each
|
||||
object."""
|
||||
return _unary_op('geom_type', self, null_value=None)
|
||||
return _delegate_property('geom_type', self)
|
||||
|
||||
@property
|
||||
def type(self):
|
||||
@@ -157,19 +130,19 @@ class GeoPandasBase(object):
|
||||
@property
|
||||
def length(self):
|
||||
"""Returns a ``Series`` containing the length of each geometry."""
|
||||
return _unary_op('length', self, null_value=np.nan)
|
||||
return _delegate_property('length', self)
|
||||
|
||||
@property
|
||||
def is_valid(self):
|
||||
"""Returns a ``Series`` of ``dtype('bool')`` with value ``True`` for
|
||||
geometries that are valid."""
|
||||
return _unary_op('is_valid', self, null_value=False)
|
||||
return _delegate_property('is_valid', self)
|
||||
|
||||
@property
|
||||
def is_empty(self):
|
||||
"""Returns a ``Series`` of ``dtype('bool')`` with value ``True`` for
|
||||
empty geometries."""
|
||||
return _unary_op('is_empty', self, null_value=False)
|
||||
return _delegate_property('is_empty', self)
|
||||
|
||||
@property
|
||||
def is_simple(self):
|
||||
@@ -178,21 +151,19 @@ class GeoPandasBase(object):
|
||||
|
||||
This is meaningful only for `LineStrings` and `LinearRings`.
|
||||
"""
|
||||
return _unary_op('is_simple', self, null_value=False)
|
||||
return _delegate_property('is_simple', self)
|
||||
|
||||
@property
|
||||
def is_ring(self):
|
||||
"""Returns a ``Series`` of ``dtype('bool')`` with value ``True`` for
|
||||
features that are closed."""
|
||||
# operates on the exterior, so can't use _unary_op()
|
||||
return Series([geom.exterior.is_ring for geom in self.geometry],
|
||||
index=self.index)
|
||||
return _delegate_property('is_ring', self)
|
||||
|
||||
@property
|
||||
def has_z(self):
|
||||
"""Returns a ``Series`` of ``dtype('bool')`` with value ``True`` for
|
||||
features that have a z-component."""
|
||||
return _unary_op('has_z', self, null_value=False)
|
||||
return _delegate_property('has_z', self)
|
||||
|
||||
#
|
||||
# Unary operations that return a GeoSeries
|
||||
@@ -202,13 +173,13 @@ class GeoPandasBase(object):
|
||||
def boundary(self):
|
||||
"""Returns a ``GeoSeries`` of lower dimensional objects representing
|
||||
each geometries's set-theoretic `boundary`."""
|
||||
return _unary_geo('boundary', self)
|
||||
return _delegate_property('boundary', self)
|
||||
|
||||
@property
|
||||
def centroid(self):
|
||||
"""Returns a ``GeoSeries`` of points representing the centroid of each
|
||||
geometry."""
|
||||
return _unary_geo('centroid', self)
|
||||
return _delegate_property('centroid', self)
|
||||
|
||||
@property
|
||||
def convex_hull(self):
|
||||
@@ -219,7 +190,7 @@ class GeoPandasBase(object):
|
||||
containing all the points in each geometry, unless the number of points
|
||||
in the geometric object is less than three. For two points, the convex
|
||||
hull collapses to a `LineString`; for 1, a `Point`."""
|
||||
return _unary_geo('convex_hull', self)
|
||||
return _delegate_property('convex_hull', self)
|
||||
|
||||
@property
|
||||
def envelope(self):
|
||||
@@ -229,7 +200,7 @@ class GeoPandasBase(object):
|
||||
The envelope of a geometry is the bounding rectangle. That is, the
|
||||
point or smallest rectangular polygon (with sides parallel to the
|
||||
coordinate axes) that contains the geometry."""
|
||||
return _unary_geo('envelope', self)
|
||||
return _delegate_property('envelope', self)
|
||||
|
||||
@property
|
||||
def exterior(self):
|
||||
@@ -239,7 +210,7 @@ class GeoPandasBase(object):
|
||||
Applies to GeoSeries containing only Polygons.
|
||||
"""
|
||||
# TODO: return empty geometry for non-polygons
|
||||
return _unary_geo('exterior', self)
|
||||
return _delegate_property('exterior', self)
|
||||
|
||||
@property
|
||||
def interiors(self):
|
||||
@@ -253,34 +224,13 @@ class GeoPandasBase(object):
|
||||
inner_rings: Series of List
|
||||
Inner rings of each polygon in the GeoSeries.
|
||||
"""
|
||||
|
||||
has_non_poly = False
|
||||
inner_rings = []
|
||||
for geom in self.geometry:
|
||||
interior_ring_seq = getattr(geom, 'interiors', None)
|
||||
# polygon case
|
||||
if interior_ring_seq is not None:
|
||||
inner_rings.append(list(interior_ring_seq))
|
||||
# non-polygon case
|
||||
else:
|
||||
has_non_poly = True
|
||||
inner_rings.append(None)
|
||||
if has_non_poly:
|
||||
warn("Only Polygon objects have interior rings. For other "
|
||||
"geometry types, None is returned.")
|
||||
|
||||
# _unary_op couldn't be used in order to warning to non-polygon and
|
||||
# conversion to list.
|
||||
return Series(inner_rings,
|
||||
index=self.index, dtype=object)
|
||||
return _delegate_property('interiors', self)
|
||||
|
||||
def representative_point(self):
|
||||
"""Returns a ``GeoSeries`` of (cheaply computed) points that are
|
||||
guaranteed to be within each geometry.
|
||||
"""
|
||||
return gpd.GeoSeries([geom.representative_point()
|
||||
for geom in self.geometry],
|
||||
index=self.index)
|
||||
return _delegate_geo_method('representative_point', self)
|
||||
|
||||
#
|
||||
# Reduction operations that return a Shapely geometry
|
||||
@@ -521,7 +471,7 @@ class GeoPandasBase(object):
|
||||
|
||||
See ``GeoSeries.total_bounds`` for the limits of the entire series.
|
||||
"""
|
||||
bounds = np.array([geom.bounds for geom in self.geometry])
|
||||
bounds = GeometryArray(self.geometry.values).bounds
|
||||
return DataFrame(bounds,
|
||||
columns=['minx', 'miny', 'maxx', 'maxy'],
|
||||
index=self.index)
|
||||
@@ -534,11 +484,7 @@ class GeoPandasBase(object):
|
||||
See ``GeoSeries.bounds`` for the bounds of the geometries contained in
|
||||
the series.
|
||||
"""
|
||||
b = self.bounds
|
||||
return np.array((b['minx'].min(),
|
||||
b['miny'].min(),
|
||||
b['maxx'].max(),
|
||||
b['maxy'].max()))
|
||||
return GeometryArray(self.geometry.values).total_bounds
|
||||
|
||||
@property
|
||||
def sindex(self):
|
||||
@@ -561,22 +507,14 @@ class GeoPandasBase(object):
|
||||
resolution: int
|
||||
Optional, the resolution of the buffer around each vertex.
|
||||
"""
|
||||
if isinstance(distance, (np.ndarray, pd.Series)):
|
||||
if len(distance) != len(self.index):
|
||||
raise ValueError("Length of distance sequence does not match "
|
||||
"length of the GeoSeries")
|
||||
if isinstance(distance, pd.Series):
|
||||
if not self.index.equals(distance.index):
|
||||
raise ValueError("Index values of distance sequence does "
|
||||
"not match index values of the GeoSeries")
|
||||
return gpd.GeoSeries(
|
||||
[geom.buffer(dist, resolution, **kwargs)
|
||||
for geom, dist in zip(self.geometry, distance)],
|
||||
index=self.index, crs=self.crs)
|
||||
if isinstance(distance, pd.Series):
|
||||
if not self.index.equals(distance.index):
|
||||
raise ValueError("Index values of distance sequence does "
|
||||
"not match index values of the GeoSeries")
|
||||
distance = np.asarray(distance)
|
||||
|
||||
return gpd.GeoSeries([geom.buffer(distance, resolution, **kwargs)
|
||||
for geom in self.geometry],
|
||||
index=self.index, crs=self.crs)
|
||||
return _delegate_geo_method('buffer', self, distance,
|
||||
resolution=resolution, **kwargs)
|
||||
|
||||
def simplify(self, *args, **kwargs):
|
||||
"""Returns a ``GeoSeries`` containing a simplified representation of
|
||||
@@ -594,9 +532,7 @@ class GeoPandasBase(object):
|
||||
False uses a quicker algorithm, but may produce self-intersecting
|
||||
or otherwise invalid geometries.
|
||||
"""
|
||||
return gpd.GeoSeries(
|
||||
[geom.simplify(*args, **kwargs) for geom in self.geometry],
|
||||
index=self.index, crs=self.crs)
|
||||
return _delegate_geo_method('simplify', self, *args, **kwargs)
|
||||
|
||||
def relate(self, other):
|
||||
"""
|
||||
@@ -630,7 +566,6 @@ class GeoPandasBase(object):
|
||||
|
||||
The project method is the inverse of interpolate.
|
||||
"""
|
||||
|
||||
return _binary_op('project', self, other, normalized=normalized)
|
||||
|
||||
def interpolate(self, distance, normalized=False):
|
||||
@@ -647,22 +582,13 @@ class GeoPandasBase(object):
|
||||
If normalized is True, distance will be interpreted as a fraction
|
||||
of the geometric object's length.
|
||||
"""
|
||||
if isinstance(distance, (np.ndarray, pd.Series)):
|
||||
if len(distance) != len(self.index):
|
||||
raise ValueError("Length of distance sequence does not match "
|
||||
"length of the GeoSeries")
|
||||
if isinstance(distance, pd.Series):
|
||||
if not self.index.equals(distance.index):
|
||||
raise ValueError("Index values of distance sequence does "
|
||||
"not match index values of the GeoSeries")
|
||||
return gpd.GeoSeries(
|
||||
[s.interpolate(dist, normalized=normalized)
|
||||
for (s, dist) in zip(self.geometry, distance)],
|
||||
index=self.index, crs=self.crs)
|
||||
|
||||
return gpd.GeoSeries([s.interpolate(distance, normalized=normalized)
|
||||
for s in self.geometry],
|
||||
index=self.index, crs=self.crs)
|
||||
if isinstance(distance, pd.Series):
|
||||
if not self.index.equals(distance.index):
|
||||
raise ValueError("Index values of distance sequence does "
|
||||
"not match index values of the GeoSeries")
|
||||
distance = np.asarray(distance)
|
||||
return _delegate_geo_method('interpolate', self, distance,
|
||||
normalized=normalized)
|
||||
|
||||
def translate(self, xoff=0.0, yoff=0.0, zoff=0.0):
|
||||
"""Returns a ``GeoSeries`` with translated geometries.
|
||||
@@ -677,9 +603,7 @@ class GeoPandasBase(object):
|
||||
xoff, yoff, and zoff for translation along the x, y, and z
|
||||
dimensions respectively.
|
||||
"""
|
||||
return gpd.GeoSeries([affinity.translate(s, xoff, yoff, zoff)
|
||||
for s in self.geometry],
|
||||
index=self.index, crs=self.crs)
|
||||
return _delegate_geo_method('translate', self, xoff, yoff, zoff)
|
||||
|
||||
def rotate(self, angle, origin='center', use_radians=False):
|
||||
"""Returns a ``GeoSeries`` with rotated geometries.
|
||||
@@ -700,11 +624,8 @@ class GeoPandasBase(object):
|
||||
use_radians : boolean
|
||||
Whether to interpret the angle of rotation as degrees or radians
|
||||
"""
|
||||
return gpd.GeoSeries(
|
||||
[affinity.rotate(s, angle, origin=origin,
|
||||
use_radians=use_radians)
|
||||
for s in self.geometry],
|
||||
index=self.index, crs=self.crs)
|
||||
return _delegate_geo_method('rotate', self, angle, origin=origin,
|
||||
use_radians=use_radians)
|
||||
|
||||
def scale(self, xfact=1.0, yfact=1.0, zfact=1.0, origin='center'):
|
||||
"""Returns a ``GeoSeries`` with scaled geometries.
|
||||
@@ -724,10 +645,8 @@ class GeoPandasBase(object):
|
||||
box center (default), 'centroid' for the geometry's 2D centroid, a
|
||||
Point object or a coordinate tuple (x, y, z).
|
||||
"""
|
||||
return gpd.GeoSeries(
|
||||
[affinity.scale(s, xfact, yfact, zfact, origin=origin)
|
||||
for s in self.geometry],
|
||||
index=self.index, crs=self.crs)
|
||||
return _delegate_geo_method('scale', self, xfact, yfact, zfact,
|
||||
origin=origin)
|
||||
|
||||
def skew(self, xs=0.0, ys=0.0, origin='center', use_radians=False):
|
||||
"""Returns a ``GeoSeries`` with skewed geometries.
|
||||
@@ -750,10 +669,8 @@ class GeoPandasBase(object):
|
||||
use_radians : boolean
|
||||
Whether to interpret the shear angle(s) as degrees or radians
|
||||
"""
|
||||
return gpd.GeoSeries(
|
||||
[affinity.skew(s, xs, ys, origin=origin, use_radians=use_radians)
|
||||
for s in self.geometry],
|
||||
index=self.index, crs=self.crs)
|
||||
return _delegate_geo_method('skew', self, xs, ys, origin=origin,
|
||||
use_radians=use_radians)
|
||||
|
||||
def explode(self):
|
||||
"""
|
||||
|
||||
+4
-11
@@ -10,7 +10,8 @@ from shapely.geometry.base import BaseGeometry
|
||||
from shapely.ops import transform
|
||||
|
||||
from geopandas.plotting import plot_series
|
||||
from geopandas.base import GeoPandasBase, _unary_op, _CoordinateIndexer
|
||||
from geopandas.base import (
|
||||
GeoPandasBase, _delegate_property, _CoordinateIndexer)
|
||||
|
||||
|
||||
_PYPROJ2 = LooseVersion(pyproj.__version__) >= LooseVersion('2.1.0')
|
||||
@@ -58,20 +59,12 @@ class GeoSeries(GeoPandasBase, Series):
|
||||
@property
|
||||
def x(self):
|
||||
"""Return the x location of point geometries in a GeoSeries"""
|
||||
if (self.geom_type == "Point").all():
|
||||
return _unary_op('x', self, null_value=np.nan)
|
||||
else:
|
||||
message = "x attribute access only provided for Point geometries"
|
||||
raise ValueError(message)
|
||||
return _delegate_property('x', self)
|
||||
|
||||
@property
|
||||
def y(self):
|
||||
"""Return the y location of point geometries in a GeoSeries"""
|
||||
if (self.geom_type == "Point").all():
|
||||
return _unary_op('y', self, null_value=np.nan)
|
||||
else:
|
||||
message = "y attribute access only provided for Point geometries"
|
||||
raise ValueError(message)
|
||||
return _delegate_property('y', self)
|
||||
|
||||
@classmethod
|
||||
def from_file(cls, filename, **kwargs):
|
||||
|
||||
@@ -198,13 +198,14 @@ class TestGeomMethods:
|
||||
result = getattr(gdf, op)
|
||||
fcmp(result, expected)
|
||||
|
||||
def test_crs_warning(self):
|
||||
# operations on geometries should warn for different CRS
|
||||
no_crs_g3 = self.g3.copy()
|
||||
no_crs_g3.crs = None
|
||||
with pytest.warns(UserWarning):
|
||||
self._test_binary_topological('intersection', self.g3,
|
||||
self.g3, no_crs_g3)
|
||||
# TODO reenable for all operations once we use pyproj > 2
|
||||
# def test_crs_warning(self):
|
||||
# # operations on geometries should warn for different CRS
|
||||
# no_crs_g3 = self.g3.copy()
|
||||
# no_crs_g3.crs = None
|
||||
# with pytest.warns(UserWarning):
|
||||
# self._test_binary_topological('intersection', self.g3,
|
||||
# self.g3, no_crs_g3)
|
||||
|
||||
def test_intersection(self):
|
||||
self._test_binary_topological('intersection', self.t1,
|
||||
|
||||
Reference in New Issue
Block a user