[python-affine] 01/05: Imported Upstream version 1.0.1
Johan Van de Wauw
johanvdw-guest at moszumanska.debian.org
Mon Oct 27 21:36:29 UTC 2014
This is an automated email from the git hooks/post-receive script.
johanvdw-guest pushed a commit to branch master
in repository python-affine.
commit e4d3a85f5f44e4ec47b21ccfac857389eee21813
Author: Johan Van de Wauw <johan.vandewauw at gmail.com>
Date: Sun Oct 26 20:23:13 2014 +0100
Imported Upstream version 1.0.1
---
.gitignore | 54 ++++++
AUTHORS.txt | 6 +
CHANGES.txt | 13 ++
LICENSE.txt | 26 +++
README.rst | 90 +++++++++
affine/__init__.py | 428 +++++++++++++++++++++++++++++++++++++++++
affine/tests/__init__.py | 0
affine/tests/test_transform.py | 367 +++++++++++++++++++++++++++++++++++
setup.py | 35 ++++
9 files changed, 1019 insertions(+)
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..51cbe85
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,54 @@
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*.py[cod]
+
+# C extensions
+*.so
+
+# Distribution / packaging
+.Python
+env/
+bin/
+build/
+develop-eggs/
+dist/
+eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+*.egg-info/
+.installed.cfg
+*.egg
+
+# Installer logs
+pip-log.txt
+pip-delete-this-directory.txt
+
+# Unit test / coverage reports
+htmlcov/
+.tox/
+.coverage
+.cache
+nosetests.xml
+coverage.xml
+
+# Translations
+*.mo
+
+# Mr Developer
+.mr.developer.cfg
+.project
+.pydevproject
+
+# Rope
+.ropeproject
+
+# Django stuff:
+*.log
+*.pot
+
+# Sphinx documentation
+docs/_build/
+
diff --git a/AUTHORS.txt b/AUTHORS.txt
new file mode 100644
index 0000000..3c9e19e
--- /dev/null
+++ b/AUTHORS.txt
@@ -0,0 +1,6 @@
+Authors
+=======
+
+- Sean Gillies <sean.gillies at gmail.com>
+- Sean Gillies <sean at mapbox.com> (same as ^^)
+- Steven Ring <smr at southsky.com.au>
diff --git a/CHANGES.txt b/CHANGES.txt
new file mode 100644
index 0000000..d4a4667
--- /dev/null
+++ b/CHANGES.txt
@@ -0,0 +1,13 @@
+CHANGES
+=======
+
+1.0.1 (2014-10-20)
+------------------
+- set_epsilon() now actually sets module EPSILON (#4).
+- add AUTHORS.txt.
+
+1.0 (2014-05-27)
+----------------
+- Code ported from Casey Duncan's Planar package.
+- from_gdal() class method added.
+
diff --git a/LICENSE.txt b/LICENSE.txt
new file mode 100644
index 0000000..1fa75b5
--- /dev/null
+++ b/LICENSE.txt
@@ -0,0 +1,26 @@
+Copyright (c) 2014, Sean C. Gillies
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+ * Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+ * Neither the name of Sean C. Gillies nor the names of
+ its contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGE.
diff --git a/README.rst b/README.rst
new file mode 100644
index 0000000..5f1a1cf
--- /dev/null
+++ b/README.rst
@@ -0,0 +1,90 @@
+Affine
+======
+
+Matrices describing affine transformation of the plane
+
+The Affine package is derived from Casey Duncan's Planar package. Please see
+the copyright statement in `affine/__init__.py] <affine/__init__.py>`__.
+
+Usage
+-----
+
+The 3x3 augmented affine transformation matrix for transformations in two
+dimensions is illustrated below.
+
+.. ::
+
+ | x' | | a b c | | x |
+ | y' | = | d e f | | y |
+ | 1 | | 0 0 1 | | 1 |
+
+Matrices can be created by passing the values ``a, b, c, d, e, f`` to the
+``affine.Affine`` constructor or by using its ``identity()``,
+``translation()``, ``scale()``, ``shear()``, and ``rotation()`` class methods.
+
+.. code-block:: pycon
+
+ >>> from affine import Affine
+ >>> Affine.identity()
+ Affine(1.0, 0.0, 0.0,
+ 0.0, 1.0, 0.0)
+ >>> Affine.translation(1.0, 5.0)
+ Affine(1.0, 0.0, 1.0,
+ 0.0, 1.0, 5.0)
+ >>> Affine.scale(2.0)
+ Affine(2.0, 0.0, 0.0,
+ 0.0, 2.0, 0.0)
+ >>> Affine.shear(45.0, 45.0) # decimal degrees
+ Affine(1.0, 0.9999999999999999, 0.0,
+ 0.9999999999999999, 1.0, 0.0)
+ >>> Affine.rotation(45.0) # decimal degrees
+ Affine(0.7071067811865476, 0.7071067811865475, 0.0,
+ -0.7071067811865475, 0.7071067811865476, 0.0)
+
+These matrices can be applied to `(x, y)` tuples to obtain transformed
+coordinates `(x', y')`.
+
+.. code-block:: pycon
+
+ >>> Affine.translation(1.0, 5.0) * (1.0, 1.0)
+ (2.0, 6.0)
+ >>> Affine.rotation(45.0) * (1.0, 1.0)
+ (1.1102230246251565e-16, 1.414213562373095)
+
+They may also be multiplied together to combine transformations.
+
+.. code-block:: pycon
+
+ >>> Affine.translation(1.0, 5.0) * Affine.rotation(45.0)
+ Affine(0.7071067811865476, 0.7071067811865475, 1.0,
+ -0.7071067811865475, 0.7071067811865476, 5.0)
+
+Usage with GIS data packages
+----------------------------
+
+Georeferenced raster datasets use affine transformations to map from image
+coordinates to world coordinates. The ``affine.Affine.from_gdal()`` class
+method helps convert `GDAL geotransforms
+<http://www.gdal.org/classGDALDataset.html#af9593cc241e7d140f5f3c4798a43a668>`__,
+sequences of 6 numbers in which the first and fourth are the x and y offsets
+and the second and sixth are the x and y pixel sizes.
+
+Using a GDAL dataset transformation matrix, the world coordinates ``x, y``
+corresponding to the top left corner of the pixel 100 rows down from the
+origin can be easily computed.
+
+.. code-block:: pycon
+
+ >>> fwd = Affine.from_gdal(-237481.5, 425.0, 0.0, 237536.4, 0.0, -425.0)
+ >>> col, row = 0, 100
+ >>> fwd * (col, row)
+ (-237481.5, 195036.4)
+
+The reverse transformation is obained using the `~` operator.
+
+.. code-block:: pycon
+
+ >>> rev = ~fwd
+ >>> rev * fwd * (col, row)
+ (0.0, 99.99999999999999)
+
diff --git a/affine/__init__.py b/affine/__init__.py
new file mode 100644
index 0000000..30f1247
--- /dev/null
+++ b/affine/__init__.py
@@ -0,0 +1,428 @@
+"""Affine transformation matrices
+
+The 3x3 augmented affine transformation matrix for transformations in two
+dimensions is illustrated below.
+
+ | x' | | a b c | | x |
+ | y' | = | d e f | | y |
+ | 1 | | 0 0 1 | | 1 |
+
+The Affine package is derived from Casey Duncan's Planar package. See the
+copyright statement below.
+"""
+
+#############################################################################
+# Copyright (c) 2010 by Casey Duncan
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+#
+# * Redistributions of source code must retain the above copyright notice,
+# this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+# * Neither the name(s) of the copyright holders nor the names of its
+# contributors may be used to endorse or promote products derived from this
+# software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AS IS AND ANY EXPRESS OR
+# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
+# EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY DIRECT, INDIRECT,
+# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
+# OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
+# EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+#############################################################################
+
+from __future__ import division
+
+from collections import namedtuple
+import math
+
+
+__all__ = ['Affine']
+__author__ = "Sean Gillies"
+__version__ = "1.0.1"
+
+EPSILON=1e-5
+EPSILON2=EPSILON**2
+
+def set_epsilon(epsilon):
+ """Set the global absolute error value and rounding limit for approximate
+ floating point comparison operations. This value is accessible via the
+ :attr:`planar.EPSILON` global variable.
+
+ The default value of ``0.00001`` is suitable for values
+ that are in the "countable range". You may need a larger
+ epsilon when using large absolute values, and a smaller value
+ for very small values close to zero. Otherwise approximate
+ comparison operations will not behave as expected.
+ """
+ global EPSILON, EPSILON2
+ EPSILON = float(epsilon)
+ EPSILON2 = EPSILON**2
+
+
+class TransformNotInvertibleError(Exception):
+ """The transform could not be inverted"""
+
+# Define assert_unorderable() depending on the language
+# implicit ordering rules. This keeps things consistent
+# across major Python versions
+try:
+ 3 > ""
+except TypeError: # pragma: no cover
+ # No implicit ordering (newer Python)
+ def assert_unorderable(a, b):
+ """Assert that a and b are unorderable"""
+ return NotImplemented
+else: # pragma: no cover
+ # Implicit ordering by default (older Python)
+ # We must raise an exception ourselves
+ # To prevent nonsensical ordering
+ def assert_unorderable(a, b):
+ """Assert that a and b are unorderable"""
+ raise TypeError("unorderable types: %s and %s"
+ % (type(a).__name__, type(b).__name__))
+
+def cached_property(func):
+ """Special property decorator that caches the computed
+ property value in the object's instance dict the first
+ time it is accessed.
+ """
+ name = func.__name__
+ doc = func.__doc__
+ def getter(self, name=name):
+ try:
+ return self.__dict__[name]
+ except KeyError:
+ self.__dict__[name] = value = func(self)
+ return value
+ getter.func_name = name
+ return property(getter, doc=doc)
+
+def cos_sin_deg(deg):
+ """Return the cosine and sin for the given angle
+ in degrees, with special-case handling of multiples
+ of 90 for perfect right angles
+ """
+ deg = deg % 360.0
+ if deg == 90.0:
+ return 0.0, 1.0
+ elif deg == 180.0:
+ return -1.0, 0
+ elif deg == 270.0:
+ return 0, -1.0
+ rad = math.radians(deg)
+ return math.cos(rad), math.sin(rad)
+
+
+class Affine(
+ namedtuple('Affine', ('a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i'))):
+ """Two dimensional affine transform for linear mapping from 2D coordinates
+ to other 2D coordinates. Parallel lines are preserved by these
+ transforms. Affine transforms can perform any combination of translations,
+ scales/flips, shears, and rotations. Class methods are provided to
+ conveniently compose transforms from these operations.
+
+ Internally the transform is stored as a 3x3 transformation matrix. The
+ transform may be constructed directly by specifying the first two rows of
+ matrix values as 6 floats. Since the matrix is an affine transform, the
+ last row is always ``(0, 0, 1)``.
+
+ :param members: 6 floats for the first two matrix rows.
+ :type members: float
+ """
+
+ def __new__(self, *members):
+ if len(members) == 6:
+ mat3x3 = [x * 1.0 for x in members] + [0.0, 0.0, 1.0]
+ return tuple.__new__(Affine, mat3x3)
+ else:
+ raise TypeError(
+ "Expected 6 number args, got %s" % len(members))
+
+ @classmethod
+ def from_gdal(cls, c, a, b, f, d, e):
+ members = [a, b, c, d, e, f]
+ mat3x3 = [x * 1.0 for x in members] + [0.0, 0.0, 1.0]
+ return tuple.__new__(Affine, mat3x3)
+
+ @classmethod
+ def identity(cls):
+ """Return the identity transform.
+
+ :rtype: Affine
+ """
+ return identity
+
+ @classmethod
+ def translation(cls, xoff, yoff):
+ """Create a translation transform from an offset vector.
+
+ :param xoff: Translation x offset.
+ :type xoff: float
+ :param yoff: Translation y offset.
+ :type yoff: float
+ :rtype: Affine
+ """
+ return tuple.__new__(cls,
+ (1.0, 0.0, xoff,
+ 0.0, 1.0, yoff,
+ 0.0, 0.0, 1.0))
+
+ @classmethod
+ def scale(cls, *scaling):
+ """Create a scaling transform from a scalar or vector.
+
+ :param scaling: The scaling factor. A scalar value will
+ scale in both dimensions equally. A vector scaling
+ value scales the dimensions independently.
+ :type scaling: float or sequence
+ :rtype: Affine
+ """
+ if len(scaling) == 1:
+ sx = sy = float(scaling[0])
+ else:
+ sx, sy = scaling
+ return tuple.__new__(cls,
+ (sx, 0.0, 0.0,
+ 0.0, sy, 0.0,
+ 0.0, 0.0, 1.0))
+
+ @classmethod
+ def shear(cls, x_angle=0, y_angle=0):
+ """Create a shear transform along one or both axes.
+
+ :param x_angle: Angle in degrees to shear along the x-axis.
+ :type x_angle: float
+ :param y_angle: Angle in degrees to shear along the y-axis.
+ :type y_angle: float
+ :rtype: Affine
+ """
+ sx = math.tan(math.radians(x_angle))
+ sy = math.tan(math.radians(y_angle))
+ return tuple.__new__(cls,
+ (1.0, sy, 0.0,
+ sx, 1.0, 0.0,
+ 0.0, 0.0, 1.0))
+
+ @classmethod
+ def rotation(cls, angle, pivot=None):
+ """Create a rotation transform at the specified angle,
+ optionally about the specified pivot point.
+
+ :param angle: Rotation angle in degrees
+ :type angle: float
+ :param pivot: Point to rotate about, if omitted the
+ rotation is about the origin.
+ :type pivot: sequence
+ :rtype: Affine
+ """
+ ca, sa = cos_sin_deg(angle)
+ if pivot is None:
+ return tuple.__new__(cls,
+ (ca, sa, 0.0,
+ -sa, ca, 0.0,
+ 0.0, 0.0, 1.0))
+ else:
+ px, py = pivot
+ return tuple.__new__(cls,
+ (ca, sa, px - px*ca + py*sa,
+ -sa, ca, py - px*sa - py*ca,
+ 0.0, 0.0, 1.0))
+
+ def __str__(self):
+ """Concise string representation."""
+ return ("|% .2f,% .2f,% .2f|\n"
+ "|% .2f,% .2f,% .2f|\n"
+ "|% .2f,% .2f,% .2f|") % self
+
+ def __repr__(self):
+ """Precise string representation."""
+ return ("Affine(%r, %r, %r,\n"
+ " %r, %r, %r)") % self[:6]
+
+ def to_gdal(self):
+ return (self.c, self.a, self.b, self.f, self.d, self.e)
+
+ @property
+ def xoff(self):
+ return self.c
+
+ @property
+ def yoff(self):
+ return self.f
+
+ @cached_property
+ def determinant(self):
+ """The determinant of the transform matrix. This value
+ is equal to the area scaling factor when the transform
+ is applied to a shape.
+ """
+ a, b, c, d, e, f, g, h, i = self
+ return a*e - b*d
+
+ @cached_property
+ def is_identity(self):
+ """True if this transform equals the identity matrix,
+ within rounding limits.
+ """
+ return self is identity or self.almost_equals(identity)
+
+ @cached_property
+ def is_rectilinear(self):
+ """True if the transform is rectilinear, i.e., whether a shape would
+ remain axis-aligned, within rounding limits, after applying the
+ transform.
+ """
+ a, b, c, d, e, f, g, h, i = self
+ return ((abs(a) < EPSILON and abs(e) < EPSILON)
+ or (abs(d) < EPSILON and abs(b) < EPSILON))
+
+ @cached_property
+ def is_conformal(self):
+ """True if the transform is conformal, i.e., if angles between points
+ are preserved after applying the transform, within rounding limits.
+ This implies that the transform has no effective shear.
+ """
+ a, b, c, d, e, f, g, h, i = self
+ return abs(a*b + d*e) < EPSILON
+
+ @cached_property
+ def is_orthonormal(self):
+ """True if the transform is orthonormal, which means that the
+ transform represents a rigid motion, which has no effective scaling or
+ shear. Mathematically, this means that the axis vectors of the
+ transform matrix are perpendicular and unit-length. Applying an
+ orthonormal transform to a shape always results in a congruent shape.
+ """
+ a, b, c, d, e, f, g, h, i = self
+ return (self.is_conformal
+ and abs(1.0 - (a*a + d*d)) < EPSILON
+ and abs(1.0 - (b*b + e*e)) < EPSILON)
+
+ @cached_property
+ def is_degenerate(self):
+ """True if this transform is degenerate, which means that it will
+ collapse a shape to an effective area of zero. Degenerate transforms
+ cannot be inverted.
+ """
+ return abs(self.determinant) < EPSILON
+
+ @property
+ def column_vectors(self):
+ """The values of the transform as three 2D column vectors"""
+ a, b, c, d, e, f, _, _, _ = self
+ return (a, d), (b, e), (c, f)
+
+ def almost_equals(self, other):
+ """Compare transforms for approximate equality.
+
+ :param other: Transform being compared.
+ :type other: Affine
+ :return: True if absolute difference between each element
+ of each respective tranform matrix < ``EPSILON``.
+ """
+ for i in (0, 1, 2, 3, 4, 5):
+ if abs(self[i] - other[i]) >= EPSILON:
+ return False
+ return True
+
+ def __gt__(self, other):
+ return assert_unorderable(self, other)
+
+ __ge__ = __lt__ = __le__ = __gt__
+
+ # Override from base class. We do not support entrywise
+ # addition, subtraction or scalar multiplication because
+ # the result is not an affine transform
+
+ def __add__(self, other):
+ raise TypeError("Operation not supported")
+
+ __iadd__ = __add__
+
+ def __mul__(self, other):
+ """Apply the transform using matrix multiplication, creating a
+ resulting object of the same type. A transform may be applied to
+ another transform, a vector, vector array, or shape.
+
+ :param other: The object to transform.
+ :type other: Affine, :class:`~planar.Vec2`,
+ :class:`~planar.Vec2Array`, :class:`~planar.Shape`
+ :rtype: Same as ``other``
+ """
+ sa, sb, sc, sd, se, sf, _, _, _ = self
+ if isinstance(other, Affine):
+ oa, ob, oc, od, oe, of, _, _, _ = other
+ return tuple.__new__(Affine,
+ (sa*oa + sb*od, sa*ob + sb*oe, sa*oc + sb*of + sc,
+ sd*oa + se*od, sd*ob + se*oe, sd*oc + se*of + sf,
+ 0.0, 0.0, 1.0))
+ else:
+ try:
+ vx, vy = other
+ except Exception:
+ return NotImplemented
+ return (vx*sa + vy*sd + sc, vx*sb + vy*se + sf)
+
+ def __rmul__(self, other):
+ # We should not be called if other is an affine instance
+ # This is just a guarantee, since we would potentially
+ # return the wrong answer in that case
+ assert not isinstance(other, Affine)
+ return self.__mul__(other)
+
+ def __imul__(self, other):
+ if isinstance(other, Affine) or isinstance(other, tuple):
+ return self.__mul__(other)
+ else:
+ return NotImplemented
+
+ def itransform(self, seq):
+ """Transform a sequence of points or vectors in place.
+
+ :param seq: Mutable sequence of :class:`~planar.Vec2` to be
+ transformed.
+ :returns: None, the input sequence is mutated in place.
+ """
+ if self is not identity and self != identity:
+ sa, sb, sc, sd, se, sf, _, _, _ = self
+ for i, (x, y) in enumerate(seq):
+ seq[i] = (x*sa + y*sd + sc, x*sb + y*se + sf)
+
+ def __invert__(self):
+ """Return the inverse transform.
+
+ :raises: :except:`TransformNotInvertible` if the transform
+ is degenerate.
+ """
+ if self.is_degenerate:
+ raise TransformNotInvertibleError(
+ "Cannot invert degenerate transform")
+ idet = 1.0 / self.determinant
+ sa, sb, sc, sd, se, sf, _, _, _ = self
+ ra = se * idet
+ rb = -sb * idet
+ rd = -sd * idet
+ re = sa * idet
+ return tuple.__new__(Affine,
+ (ra, rb, -sc*ra - sf*rb,
+ rd, re, -sc*rd - sf*re,
+ 0.0, 0.0, 1.0))
+
+ __hash__ = tuple.__hash__ # hash is not inherited in Py 3
+
+
+identity = Affine(1, 0, 0, 0, 1, 0)
+"""The identity transform"""
+
+
+# vim: ai ts=4 sts=4 et sw=4 tw=78
+
diff --git a/affine/tests/__init__.py b/affine/tests/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/affine/tests/test_transform.py b/affine/tests/test_transform.py
new file mode 100644
index 0000000..e731e02
--- /dev/null
+++ b/affine/tests/test_transform.py
@@ -0,0 +1,367 @@
+#############################################################################
+# Planar is Copyright (c) 2010 by Casey Duncan
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+#
+# * Redistributions of source code must retain the above copyright notice,
+# this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+# * Neither the name(s) of the copyright holders nor the names of its
+# contributors may be used to endorse or promote products derived from this
+# software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AS IS AND ANY EXPRESS OR
+# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
+# EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY DIRECT, INDIRECT,
+# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
+# OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
+# EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+#############################################################################
+
+"""Transform unit tests"""
+
+from __future__ import division
+import sys
+import math
+import unittest
+from nose.tools import assert_equal, assert_almost_equal, raises
+
+from affine import Affine
+
+
+def seq_almost_equal(t1, t2, error=0.00001):
+ assert len(t1) == len(t2), "%r != %r" % (t1, t2)
+ for m1, m2 in zip(t1, t2):
+ assert abs(m1 - m2) <= error, "%r != %r" % (t1, t2)
+
+
+class PyAffineTestCase(unittest.TestCase):
+
+ @raises(TypeError)
+ def test_zero_args(self):
+ Affine()
+
+ @raises(TypeError)
+ def test_wrong_arg_type(self):
+ Affine(None)
+
+ @raises(TypeError)
+ def test_args_too_few(self):
+ Affine(1, 2)
+
+ @raises(TypeError)
+ def test_args_too_many(self):
+ Affine(*range(10))
+
+ @raises(TypeError)
+ def test_args_members_wrong_type(self):
+ Affine(0, 2, 3, None, None, "")
+
+ def test_len(self):
+ t = Affine(1, 2, 3, 4, 5, 6)
+ assert_equal(len(t), 9)
+
+ def test_slice_last_row(self):
+ t = Affine(1, 2, 3, 4, 5, 6)
+ assert_equal(t[-3:], (0, 0, 1))
+
+ def test_members_are_floats(self):
+ t = Affine(1, 2, 3, 4, 5, 6)
+ for m in t:
+ assert isinstance(m, float), repr(m)
+
+ def test_getitem(self):
+ t = Affine(1, 2, 3, 4, 5, 6)
+ assert_equal(t[0], 1)
+ assert_equal(t[1], 2)
+ assert_equal(t[2], 3)
+ assert_equal(t[3], 4)
+ assert_equal(t[4], 5)
+ assert_equal(t[5], 6)
+ assert_equal(t[6], 0)
+ assert_equal(t[7], 0)
+ assert_equal(t[8], 1)
+ assert_equal(t[-1], 1)
+
+ @raises(TypeError)
+ def test_getitem_wrong_type(self):
+ t = Affine(1, 2, 3, 4, 5, 6)
+ t['foobar']
+
+ def test_str(self):
+ assert_equal(
+ str(Affine(1.111, 2.222, 3.333, -4.444, -5.555, 6.666)),
+ "| 1.11, 2.22, 3.33|\n|-4.44,-5.55, 6.67|\n| 0.00, 0.00, 1.00|")
+
+ def test_repr(self):
+ assert_equal(
+ repr(Affine(1.111, 2.222, 3.456, 4.444, 5.5, 6.25)),
+ ("Affine(1.111, 2.222, 3.456,\n"
+ " 4.444, 5.5, 6.25)"))
+
+ def test_identity_constructor(self):
+ ident = Affine.identity()
+ assert isinstance(ident, Affine)
+ assert_equal(tuple(ident), (1,0,0, 0,1,0, 0,0,1))
+ assert ident.is_identity
+
+ def test_translation_constructor(self):
+ trans = Affine.translation(2, -5)
+ assert isinstance(trans, Affine)
+ assert_equal(tuple(trans), (1,0,2, 0,1,-5, 0,0,1))
+
+ def test_scale_constructor(self):
+ scale = Affine.scale(5)
+ assert isinstance(scale, Affine)
+ assert_equal(tuple(scale), (5,0,0, 0,5,0, 0,0,1))
+ scale = Affine.scale(-1, 2)
+ assert_equal(tuple(scale), (-1,0,0, 0,2,0, 0,0,1))
+ assert_equal(tuple(Affine.scale(1)),
+ tuple(Affine.identity()))
+
+ def test_shear_constructor(self):
+ shear = Affine.shear(30)
+ assert isinstance(shear, Affine)
+ sx = math.tan(math.radians(30))
+ seq_almost_equal(tuple(shear), (1,0,0, sx,1,0, 0,0,1))
+ shear = Affine.shear(-15, 60)
+ sx = math.tan(math.radians(-15))
+ sy = math.tan(math.radians(60))
+ seq_almost_equal(tuple(shear), (1,sy,0, sx,1,0, 0,0,1))
+ shear = Affine.shear(y_angle=45)
+ seq_almost_equal(tuple(shear), (1,1,0, 0,1,0, 0,0,1))
+
+ def test_rotation_constructor(self):
+ rot = Affine.rotation(60)
+ assert isinstance(rot, Affine)
+ r = math.radians(60)
+ s, c = math.sin(r), math.cos(r)
+ assert_equal(tuple(rot), (c,s,0, -s,c,0, 0,0,1))
+ rot = Affine.rotation(337)
+ r = math.radians(337)
+ s, c = math.sin(r), math.cos(r)
+ seq_almost_equal(tuple(rot), (c,s,0, -s,c,0, 0,0,1))
+ assert_equal(tuple(Affine.rotation(0)),
+ tuple(Affine.identity()))
+
+ def test_rotation_constructor_quadrants(self):
+ assert_equal(tuple(Affine.rotation(0)), (1,0,0, 0,1,0, 0,0,1))
+ assert_equal(tuple(Affine.rotation(90)), (0,1,0, -1,0,0, 0,0,1))
+ assert_equal(tuple(Affine.rotation(180)), (-1,0,0, 0,-1,0, 0,0,1))
+ assert_equal(tuple(Affine.rotation(-180)), (-1,0,0, 0,-1,0, 0,0,1))
+ assert_equal(tuple(Affine.rotation(270)), (0,-1,0, 1,0,0, 0,0,1))
+ assert_equal(tuple(Affine.rotation(-90)), (0,-1,0, 1,0,0, 0,0,1))
+ assert_equal(tuple(Affine.rotation(360)), (1,0,0, 0,1,0, 0,0,1))
+ assert_equal(tuple(Affine.rotation(450)), (0,1,0, -1,0,0, 0,0,1))
+ assert_equal(tuple(Affine.rotation(-450)), (0,-1,0, 1,0,0, 0,0,1))
+
+ def test_rotation_constructor_with_pivot(self):
+ assert_equal(tuple(Affine.rotation(60)),
+ tuple(Affine.rotation(60, pivot=(0,0))))
+ rot = Affine.rotation(27, pivot=(2,-4))
+ r = math.radians(27)
+ s, c = math.sin(r), math.cos(r)
+ assert_equal(tuple(rot),
+ (c,s,2 - 2*c - 4*s, -s,c,-4 - 2*s + 4*c, 0,0,1))
+ assert_equal(tuple(Affine.rotation(0, (-3, 2))),
+ tuple(Affine.identity()))
+
+ @raises(TypeError)
+ def test_rotation_contructor_wrong_arg_types(self):
+ Affine.rotation(1,1)
+
+ def test_determinant(self):
+ assert_equal(Affine.identity().determinant, 1)
+ assert_equal(Affine.scale(2).determinant, 4)
+ assert_equal(Affine.scale(0).determinant, 0)
+ assert_equal(Affine.scale(5,1).determinant, 5)
+ assert_equal(Affine.scale(-1,1).determinant, -1)
+ assert_equal(Affine.scale(-1,0).determinant, 0)
+ assert_almost_equal(Affine.rotation(77).determinant, 1)
+ assert_almost_equal(Affine.translation(32, -47).determinant, 1)
+
+ def test_is_rectilinear(self):
+ assert Affine.identity().is_rectilinear
+ assert Affine.scale(2.5, 6.1).is_rectilinear
+ assert Affine.translation(4, -1).is_rectilinear
+ assert Affine.rotation(90).is_rectilinear
+ assert not Affine.shear(4, -1).is_rectilinear
+ assert not Affine.rotation(-26).is_rectilinear
+
+ def test_is_conformal(self):
+ assert Affine.identity().is_conformal
+ assert Affine.scale(2.5, 6.1).is_conformal
+ assert Affine.translation(4, -1).is_conformal
+ assert Affine.rotation(90).is_conformal
+ assert Affine.rotation(-26).is_conformal
+ assert not Affine.shear(4, -1).is_conformal
+
+ def test_is_orthonormal(self):
+ assert Affine.identity().is_orthonormal
+ assert Affine.translation(4, -1).is_orthonormal
+ assert Affine.rotation(90).is_orthonormal
+ assert Affine.rotation(-26).is_orthonormal
+ assert not Affine.scale(2.5, 6.1).is_orthonormal
+ assert not Affine.scale(.5, 2).is_orthonormal
+ assert not Affine.shear(4, -1).is_orthonormal
+
+ def test_is_degenerate(self):
+ from affine import EPSILON
+ assert not Affine.identity().is_degenerate
+ assert not Affine.translation(2, -1).is_degenerate
+ assert not Affine.shear(0, -22.5).is_degenerate
+ assert not Affine.rotation(88.7).is_degenerate
+ assert not Affine.scale(0.5).is_degenerate
+ assert Affine.scale(0).is_degenerate
+ assert Affine.scale(-10, 0).is_degenerate
+ assert Affine.scale(0, 300).is_degenerate
+ assert Affine.scale(0).is_degenerate
+ assert Affine.scale(0).is_degenerate
+ assert Affine.scale(EPSILON).is_degenerate
+
+ def test_column_vectors(self):
+ import affine
+ a, b, c = Affine(2, 3, 4, 5, 6, 7).column_vectors
+ assert isinstance(a, tuple)
+ assert isinstance(b, tuple)
+ assert isinstance(c, tuple)
+ assert_equal(a, (2, 5))
+ assert_equal(b, (3, 6))
+ assert_equal(c, (4, 7))
+
+ def test_almost_equals(self):
+ from affine import EPSILON
+ assert EPSILON != 0, EPSILON
+ E = EPSILON * 0.5
+ t = Affine(1.0, E, 0, -E, 1.0+E, E)
+ assert t.almost_equals(Affine.identity())
+ assert Affine.identity().almost_equals(t)
+ assert t.almost_equals(t)
+ t = Affine(1.0, 0, 0, -EPSILON, 1.0, 0)
+ assert not t.almost_equals(Affine.identity())
+ assert not Affine.identity().almost_equals(t)
+ assert t.almost_equals(t)
+
+ def test_equality(self):
+ t1 = Affine(1, 2, 3, 4, 5, 6)
+ t2 = Affine(6, 5, 4, 3, 2, 1)
+ t3 = Affine(1, 2, 3, 4, 5, 6)
+ assert t1 == t3
+ assert not t1 == t2
+ assert t2 == t2
+ assert not t1 != t3
+ assert not t2 != t2
+ assert t1 != t2
+ assert not t1 == 1
+ assert t1 != 1
+
+ @raises(TypeError)
+ def test_gt(self):
+ Affine(1,2,3,4,5,6) > Affine(6,5,4,3,2,1)
+
+ @raises(TypeError)
+ def test_lt(self):
+ Affine(1,2,3,4,5,6) < Affine(6,5,4,3,2,1)
+
+ @raises(TypeError)
+ def test_add(self):
+ Affine(1,2,3,4,5,6) + Affine(6,5,4,3,2,1)
+
+ @raises(TypeError)
+ def test_sub(self):
+ Affine(1,2,3,4,5,6) - Affine(6,5,4,3,2,1)
+
+ def test_mul_by_identity(self):
+ t = Affine(1,2,3,4,5,6)
+ assert_equal(tuple(t * Affine.identity()), tuple(t))
+
+ def test_mul_transform(self):
+ t = Affine.rotation(5) * Affine.rotation(29)
+ assert isinstance(t, Affine)
+ seq_almost_equal(t, Affine.rotation(34))
+ t = Affine.scale(3, 5) * Affine.scale(2)
+ seq_almost_equal(t, Affine.scale(6, 10))
+
+ def test_itransform(self):
+ pts = [(4,1), (-1,0), (3,2)]
+ r = Affine.scale(-2).itransform(pts)
+ assert r is None, r
+ assert_equal(pts, [(-8, -2), (2,0), (-6,-4)])
+
+ @raises(TypeError)
+ def test_mul_wrong_type(self):
+ Affine(1,2,3,4,5,6) * None
+
+ @raises(TypeError)
+ def test_mul_sequence_wrong_member_types(self):
+ class NotPtSeq:
+ @classmethod
+ def from_points(cls, points):
+ list(points)
+ def __iter__(self):
+ yield 0
+ Affine(1,2,3,4,5,6) * NotPtSeq()
+
+ def test_imul_transform(self):
+ t = Affine.translation(3, 5)
+ t *= Affine.translation(-2, 3.5)
+ assert isinstance(t, Affine)
+ seq_almost_equal(t, Affine.translation(1, 8.5))
+
+ def test_inverse(self):
+ seq_almost_equal(~Affine.identity(), Affine.identity())
+ seq_almost_equal(
+ ~Affine.translation(2, -3), Affine.translation(-2, 3))
+ seq_almost_equal(
+ ~Affine.rotation(-33.3), Affine.rotation(33.3))
+ t = Affine(1,2,3,4,5,6)
+ seq_almost_equal(~t * t, Affine.identity())
+
+ def test_cant_invert_degenerate(self):
+ from affine import TransformNotInvertibleError
+ t = Affine.scale(0)
+ self.assertRaises(TransformNotInvertibleError, lambda: ~t)
+
+ def test_set_epsilon(self):
+ import affine
+
+ old_epsilon = affine.EPSILON
+ old_epsilon2 = affine.EPSILON2
+
+ try:
+ affine.set_epsilon(123)
+ assert_equal(123, affine.EPSILON)
+ assert_equal(123*123, affine.EPSILON2)
+ finally:
+ affine.set_epsilon(old_epsilon)
+
+
+
+
+
+def test_gdal():
+ t = Affine.from_gdal(-237481.5, 425.0, 0.0, 237536.4, 0.0, -425.0)
+ assert t.c == t.xoff == -237481.5
+ assert t.a == 425.0
+ assert t.b == 0.0
+ assert t.f == t.yoff == 237536.4
+ assert t.d == 0.0
+ assert t.e == -425.0
+ assert tuple(t) == (425.0, 0.0, -237481.5, 0.0, -425.0, 237536.4, 0, 0, 1.0)
+ assert t.to_gdal() == (-237481.5, 425.0, 0.0, 237536.4, 0.0, -425.0)
+
+
+if __name__ == '__main__':
+ unittest.main()
+
+
+# vim: ai ts=4 sts=4 et sw=4 tw=78
+
diff --git a/setup.py b/setup.py
new file mode 100644
index 0000000..4915923
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,35 @@
+from setuptools import setup, find_packages
+import sys, os
+
+# Parse the version from the fiona module.
+with open('affine/__init__.py') as f:
+ for line in f:
+ if line.find("__version__") >= 0:
+ version = line.split("=")[1].strip()
+ version = version.strip('"')
+ version = version.strip("'")
+ continue
+
+readme = open('README.rst').read()
+
+setup(name='affine',
+ version=version,
+ description="Matrices describing affine transformation of the plane",
+ long_description=readme,
+ classifiers=[],
+ keywords='',
+ author='Sean Gillies',
+ author_email='sean at mapbox.com',
+ url='https://github.com/sgillies/affine',
+ license='BSD',
+ package_dir={'': '.'},
+ packages=['affine'],
+ include_package_data=True,
+ zip_safe=False,
+ install_requires=[
+ # -*- Extra requirements: -*-
+ ],
+ entry_points="""
+ # -*- Entry points: -*-
+ """,
+ )
--
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/pkg-grass/python-affine.git
More information about the Pkg-grass-devel
mailing list