Merge pull request #29 from cfarmer/transform

Adds various transformation functions - shift, rotate, scale, skew
This commit is contained in:
Kelsey Jordahl
2013-09-08 19:57:20 -07:00
2 changed files with 183 additions and 9 deletions
+133 -9
View File
@@ -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)
+50
View File
@@ -5,6 +5,7 @@ from pandas import Series
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):
@@ -15,6 +16,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):
@@ -37,6 +45,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 type(self.g1.area) is Series
@@ -196,3 +207,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))))