REF: move element-wise geo ops to array class (#993)

This commit is contained in:
Joris Van den Bossche
2019-05-14 12:46:38 +02:00
committed by GitHub
parent 19df6743ae
commit 4db3b1e0c8
4 changed files with 473 additions and 180 deletions
+382
View File
@@ -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
View File
@@ -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
View File
@@ -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):
+8 -7
View File
@@ -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,