From fd58437ea311b6ea106ce8299f31d6017ff8a686 Mon Sep 17 00:00:00 2001 From: Carson Farmer Date: Sun, 8 Sep 2013 21:33:25 -0400 Subject: [PATCH] adds various transformation functions - shift, rotate, scale, skew, and a few other minor adjustments/fixes --- geopandas/geoseries.py | 142 +++++++++++++++++++++++++++++++++++++--- tests/test_geoseries.py | 50 ++++++++++++++ 2 files changed, 183 insertions(+), 9 deletions(-) diff --git a/geopandas/geoseries.py b/geopandas/geoseries.py index 6f95b08..5b4066e 100644 --- a/geopandas/geoseries.py +++ b/geopandas/geoseries.py @@ -9,6 +9,7 @@ from shapely.geometry import shape, Polygon, Point from shapely.geometry.collection import GeometryCollection from shapely.geometry.base import BaseGeometry from shapely.ops import cascaded_union, unary_union, transform +import shapely.affinity as affinity import fiona from fiona.crs import from_epsg @@ -311,20 +312,140 @@ class GeoSeries(Series): def buffer(self, distance, resolution=16): return GeoSeries([geom.buffer(distance, resolution) for geom in self], - index=self.index) + index=self.index, crs=self.crs) def simplify(self, *args, **kwargs): - return Series([geom.simplify(*args, **kwargs) for geom in self], - index=self.index) - - def interpolate(self): - raise NotImplementedError + return GeoSeries([geom.simplify(*args, **kwargs) for geom in self], + index=self.index, crs=self.crs) def relate(self, other): raise NotImplementedError - def project(self, *args, **kwargs): - raise NotImplementedError + def project(self, other, normalized=False): + """ + Return the distance along each geometry nearest to *other* + + Parameters + ---------- + other : BaseGeometry or GeoSeries + The *other* geometry to computed projected point from. + normalized : boolean + If normalized is True, return the distance normalized to + the length of the object. + + The project method is the inverse of interpolate. + """ + + return self._series_op(other, 'project', normalized=normalized) + + def interpolate(self, distance, normalized=False): + """ + Return a point at the specified distance along each geometry + + Parameters + ---------- + distance : float or Series of floats + Distance(s) along the geometries at which a point should be returned + normalized : boolean + If normalized is True, distance will be interpreted as a fraction + of the geometric object's length. + """ + + return GeoSeries([s.interpolate(distance, normalized) for s in self], + index=self.index, crs=self.crs) + + def translate(self, xoff=0.0, yoff=0.0, zoff=0.0): + """ + Shift the coordinates of the GeoSeries. + + Parameters + ---------- + xoff, yoff, zoff : float, float, float + Amount of offset along each dimension. + xoff, yoff, and zoff for translation along the x, y, and z + dimensions respectively. + + See shapely manual for more information: + http://toblerity.org/shapely/manual.html#affine-transformations + """ + + return GeoSeries([affinity.translate(s, xoff, yoff, zoff) for s in self], + index=self.index, crs=self.crs) + + # Shift is simply an alias for translate + shift = translate + + def rotate(self, angle, origin='center', use_radians=False): + """ + Rotate the coordinates of the GeoSeries. + + Parameters + ---------- + angle : float + The angle of rotation can be specified in either degrees (default) + or radians by setting use_radians=True. Positive angles are + counter-clockwise and negative are clockwise rotations. + origin : string, Point, or tuple (x, y) + The point of origin can be a keyword 'center' for the bounding box + center (default), 'centroid' for the geometry's centroid, a Point + object or a coordinate tuple (x, y). + use_radians : boolean + Whether to interpret the angle of rotation as degrees or radians + + See shapely manual for more information: + http://toblerity.org/shapely/manual.html#affine-transformations + """ + + return GeoSeries([affinity.rotate(s, angle, origin=origin, + use_radians=use_radians) for s in self], index=self.index, + crs=self.crs) + + def scale(self, xfact=1.0, yfact=1.0, zfact=1.0, origin='center'): + """ + Scale the geometries of the GeoSeries along each (x, y, z) dimension. + + Parameters + ---------- + xfact, yfact, zfact : float, float, float + Scaling factors for the x, y, and z dimensions respectively. + origin : string, Point, or tuple + The point of origin can be a keyword 'center' for the 2D bounding + box center (default), 'centroid' for the geometry's 2D centroid, a + Point object or a coordinate tuple (x, y, z). + + Note: Negative scale factors will mirror or reflect coordinates. + + See shapely manual for more information: + http://toblerity.org/shapely/manual.html#affine-transformations + """ + + return GeoSeries([affinity.scale(s, xfact, yfact, zfact, + origin=origin) for s in self], index=self.index, crs=self.crs) + + def skew(self, xs=0.0, ys=0.0, origin='center', use_radians=False): + """ + Shear/Skew the geometries of the GeoSeries by angles along x and y dimensions. + + Parameters + ---------- + xs, ys : float, float + The shear angle(s) for the x and y axes respectively. These can be + specified in either degrees (default) or radians by setting + use_radians=True. + origin : string, Point, or tuple (x, y) + The point of origin can be a keyword 'center' for the bounding box + center (default), 'centroid' for the geometry's centroid, a Point + object or a coordinate tuple (x, y). + use_radians : boolean + Whether to interpret the shear angle(s) as degrees or radians + + See shapely manual for more information: + http://toblerity.org/shapely/manual.html#affine-transformations + """ + + return GeoSeries([affinity.skew(s, xs, ys, origin=origin, + use_radians=use_radians) for s in self], index=self.index, + crs=self.crs) # # Implement standard operators for GeoSeries @@ -432,7 +553,10 @@ class GeoSeries(Series): fill_value=fill_value, method=method, limit=limit) - return GeoSeries(left), GeoSeries(right) + if isinstance(other, GeoSeries): + return GeoSeries(left), GeoSeries(right) + else: # It is probably a Series, let's keep it that way + return GeoSeries(left), right def plot(self, *args, **kwargs): return plot_series(self, *args, **kwargs) diff --git a/tests/test_geoseries.py b/tests/test_geoseries.py index 7d9b4ab..b39960a 100644 --- a/tests/test_geoseries.py +++ b/tests/test_geoseries.py @@ -4,6 +4,7 @@ from numpy.testing import assert_array_equal from shapely.geometry import Polygon, Point, LineString from shapely.geometry.base import BaseGeometry from geopandas import GeoSeries +from pandas import Series def geom_equals(this, that): @@ -14,6 +15,13 @@ def geom_equals(this, that): eq = this.equals(that) return np.all(np.logical_or(eq, empty)) +def geom_almost_equals(this, that): + """ + Test for geometric equality, allowing all empty geometries to be considered almost equal + """ + empty = np.logical_and(this.is_empty, that.is_empty) + eq = this.almost_equals(that) + return np.all(np.logical_or(eq, empty)) class TestSeries(unittest.TestCase): @@ -36,6 +44,9 @@ class TestSeries(unittest.TestCase): self.sol = Point(-74.0446, 40.6893) self.landmarks = GeoSeries([self.esb, self.sol], crs={'init': 'epsg:4326', 'no_defs': True}) + self.l1 = LineString([(0, 0), (0, 1), (1, 1)]) + self.l2 = LineString([(0, 0), (1, 0), (1, 1), (0, 1)]) + self.g5 = GeoSeries([self.l1, self.l2]) def test_area(self): assert_array_equal(self.g1.area.values, np.array([0.5, 1.0])) @@ -194,3 +205,42 @@ class TestSeries(unittest.TestCase): na = self.na_none.fillna() self.assertTrue(isinstance(na[2], BaseGeometry)) self.assertTrue(na[2].is_empty) + + def test_interpolate(self): + res = self.g5.interpolate(0.75, normalized=True) + geom_equals(res, GeoSeries([Point(0.5, 1.0), Point(0.75, 1.0)])) + res = self.g5.interpolate(1.5) + geom_equals(res, GeoSeries([Point(0.5, 1.0), Point(1.0, 0.5)])) + + def test_project(self): + res = self.g5.project(Point(1.0, 0.5)) + assert_array_equal(res, [2.0, 1.5]) + res = self.g5.project(Point(1.0, 0.5), normalized=True) + assert_array_equal(res, [1.0, 0.5]) + + def test_translate_tuple(self): + trans = self.sol.x - self.esb.x, self.sol.y - self.esb.y + self.assertTrue(self.landmarks.translate(*trans)[0].equals(self.sol)) + + def test_rotate(self): + angle = 98 + res = self.g4.rotate(angle, origin=Point(0,0)) + self.assertTrue(geom_almost_equals(self.g4, res.rotate(-angle, + origin=Point(0,0)))) + + def test_scale(self): + scale = 2., 1. + inv = tuple(1./i for i in scale) + res = self.g4.scale(*scale, origin=Point(0,0)) + self.assertTrue(geom_almost_equals(self.g4, res.scale(*inv, + origin=Point(0,0)))) + + def test_skew(self): + skew = 45. + res = self.g4.skew(xs=skew, origin=Point(0,0)) + self.assertTrue(geom_almost_equals(self.g4, res.skew(xs=-skew, + origin=Point(0,0)))) + res = self.g4.skew(ys=skew, origin=Point(0,0)) + self.assertTrue(geom_almost_equals(self.g4, res.skew(ys=-skew, + origin=Point(0,0)))) +