[python-shapely] 01/02: Imported Upstream version 1.5.9

Sebastiaan Couwenberg sebastic at moszumanska.debian.org
Thu Aug 20 17:42:14 UTC 2015


This is an automated email from the git hooks/post-receive script.

sebastic pushed a commit to branch upstream
in repository python-shapely.

commit 57f2ccd201f5e531ed203a1857939286d805c9b4
Author: Johan Van de Wauw <johan.vandewauw at gmail.com>
Date:   Fri Jun 26 20:47:56 2015 +0200

    Imported Upstream version 1.5.9
---
 .travis.yml                         |   3 +-
 CHANGES.txt                         |  54 ++++++++++
 CREDITS.txt                         |   1 +
 MANIFEST.in                         |  16 +--
 README.rst                          |  79 ++++++---------
 build-scripts/macosx-10.6-intel.sh  |   8 ++
 build-wheels.sh                     |  21 ++++
 docs/manual.rst                     |  79 ++++++++++-----
 setup.py                            |   8 +-
 shapely/__init__.py                 |   2 +-
 shapely/_geos.pxi                   |   2 +-
 shapely/coords.py                   |  11 ++-
 shapely/ctypes_declarations.py      |  35 +++++--
 shapely/geometry/base.py            | 114 +++++++++++++++------
 shapely/geometry/collection.py      |  38 ++++++-
 shapely/geometry/linestring.py      |  33 ++++---
 shapely/geometry/multilinestring.py |  90 +++++++----------
 shapely/geometry/multipoint.py      |  38 ++++---
 shapely/geometry/multipolygon.py    |  39 ++++----
 shapely/geometry/point.py           |  36 +++----
 shapely/geometry/polygon.py         |  78 ++++++++++-----
 shapely/geos.py                     |  89 ++++++++++++-----
 shapely/impl.py                     |   6 ++
 shapely/iterops.py                  |  48 ++++-----
 shapely/ops.py                      |  28 +++++-
 shapely/predicates.py               |  16 +--
 shapely/prepared.py                 |  36 ++++++-
 shapely/speedups/__init__.py        |   9 ++
 shapely/speedups/_speedups.pyx      | 163 +++++++++++++++++++++++++-----
 shapely/topology.py                 |  38 +++++--
 tests/test_affinity.py              |   6 +-
 tests/test_coords.py                |  40 ++++++++
 tests/test_geos_err_handler.py      |  33 +++++++
 tests/test_hash.py                  |  21 ++++
 tests/test_iterops.py               |  26 +++++
 tests/test_operators.py             |  49 ++++++++-
 tests/test_predicates.py            |  14 ++-
 tests/test_prepared.py              |  20 ++++
 tests/test_snap.py                  |  32 ++++++
 tests/test_svg.py                   | 192 ++++++++++++++++++++++++++++++++++++
 40 files changed, 1262 insertions(+), 389 deletions(-)

diff --git a/.travis.yml b/.travis.yml
index f92f690..5d36913 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -13,8 +13,9 @@ env:
 before_install:
   - sudo add-apt-repository -y ppa:ubuntugis/ppa
   - sudo apt-get update -qq
-  - sudo apt-get install -qq libgeos-dev python-numpy cython
+  - sudo apt-get install -qq libgeos-dev python-numpy
   - if [[ $TRAVIS_PYTHON_VERSION == "2.6" ]]; then pip install unittest2; fi
+  - pip install --install-option="--no-cython-compile" cython
   - pip install -r requirements-dev.txt
 
 install:
diff --git a/CHANGES.txt b/CHANGES.txt
index 72f8245..93f3772 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -1,6 +1,60 @@
 Changes
 =======
 
+1.5.9 (2015-05-27)
+------------------
+- Fix for 64 bit speedups compatibility (#274).
+
+1.5.8 (2015-04-29)
+------------------
+- Setup file encoding bug fix (#254).
+- Support for pyinstaller (#261).
+- Major prepared geometry operation fix for Windows (#268, #269).
+- Major fix for OS X binary wheel (#262).
+
+1.5.7 (2015-03-16)
+------------------
+- Test and fix buggy error and notice handlers (#249).
+
+1.5.6 (2015-02-02)
+------------------
+- Fix setup regression (#232, #234).
+- SVG representation improvements (#233, #237).
+
+1.5.5 (2015-01-20)
+------------------
+- MANIFEST changes to restore _geox.pxi (#231).
+
+1.5.4 (2015-01-19)
+------------------
+- Fixed OS X binary wheel library load path (#224).
+
+1.5.3 (2015-01-12)
+------------------
+- Fixed ownership and potential memory leak in polygonize (#223).
+- Wider release of binary wheels for OS X.
+
+1.5.2 (2015-01-04)
+------------------
+- Fail installation if GEOS dependency is not met, preventing update breakage
+  (#218, #219).
+
+1.5.1 (2014-12-04)
+------------------
+- Restore geometry hashing (#209).
+
+1.5.0 (2014-12-02)
+------------------
+- Affine transformation speedups (#197).
+- New `==` rich comparison (#195).
+- Geometry collection constructor (#200).
+- ops.snap() backed by GEOSSnap (#201).
+- Clearer exceptions in cases of topological invalidity (#203).
+
+1.4.4 (2014-11-02)
+------------------
+- Proper conversion of numpy float32 vals to coords (#186).
+
 1.4.3 (2014-10-01)
 ------------------
 - Fix for endianness bug in WKB writer (#174).
diff --git a/CREDITS.txt b/CREDITS.txt
index 3433530..b328cf5 100644
--- a/CREDITS.txt
+++ b/CREDITS.txt
@@ -12,6 +12,7 @@ Patches contributed by:
 
 * Allan Adair (https://github.com/allanadair)
 * Joshua Arnott (https://github.com/snorfalorpagus)
+* David Baumgold (https://github.com/singingwolfboy)
 * Howard Butler
 * Gabi Davar (https://github.com/mindw)
 * Phil Elson (https://github.com/pelson)
diff --git a/MANIFEST.in b/MANIFEST.in
index 86e4a78..f542522 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -1,12 +1,14 @@
+prune manual
+prune debian
+prune docs
+prune DLLs_AMD64
+prune DLLs_x86
 exclude *.txt
-recursive-exclude manual *
-recursive-exclude debian *
-recursive-exclude docs *
-recursive-exclude DLLs_AMD64 *
-recursive-exclude DLLs_x86 *
+exclude MANIFEST.in
 include CHANGES.txt CREDITS.txt LICENSE.txt README.rst VERSION.txt
 recursive-include tests *.py *.txt
 recursive-include shapely/examples *.py
-recursive-exclude shapely/speedups *.pyx
+recursive-include shapely/speedups *.pyx
+recursive-include shapely/vectorized *.pyx
+include shapely/_geos.pxi
 include docs/*.rst
-exclude MANIFEST.in
diff --git a/README.rst b/README.rst
index 5c2005f..ea892c0 100644
--- a/README.rst
+++ b/README.rst
@@ -25,22 +25,45 @@ but can be readily integrated with packages that are. For more details, see:
 Requirements
 ============
 
-Shapely 1.4 requires
+Shapely 1.5.x requires
 
 * Python >=2.6 (including Python 3.x)
-* libgeos_c >=3.1 (3.0 and below have not been tested, YMMV)
+* GEOS >=3.3 (Shapely 1.2.x requires only GEOS 3.1 but YMMV)
 
-Installation
-============
+Installing Shapely
+==================
+
+Windows users should download an executable installer from
+http://www.lfd.uci.edu/~gohlke/pythonlibs/#shapely or PyPI (if available).
 
-Windows users should use the executable installer, which contains the required
-GEOS DLL. Other users should acquire libgeos_c by any means, make sure that it
-is on the system library path, and install from the Python package index.
+On other systems, acquire the GEOS by any means (`brew install geos` on OS X or
+`apt-get install libgeos-dev` on Debian/Ubuntu), make sure that it is on the
+system library path, and install Shapely from the Python package index.
 
 .. code-block:: console
 
     $ pip install shapely
 
+If you've installed GEOS to a non-standard location, you can use the
+geos-config program to find compiler and linker options.
+
+.. code-block:: console
+
+    $ CFLAGS=`geos-config --cflags` LDFLAGS=`geos-config --clibs` pip install shapely
+
+If your system's GEOS version is < 3.3.0 you cannot use Shapely 1.3+ and must
+stick to 1.2.x as shown below.
+
+.. code-block:: console
+
+    $ pip install shapely<1.3
+
+Or, if you're using pip 6+
+
+.. code-block:: console
+
+    $ pip install shapely~=1.2
+
 Shapely is also provided by popular Python distributions like Canopy (Enthought)
 and Anaconda (Continuum Analytics).
 
@@ -75,46 +98,8 @@ modules provide dumpers and loaders inspired by Python's pickle module.
     >>> dumps(loads('POINT (0 0)'))
     'POINT (0.0000000000000000 0.0000000000000000)'
 
-All linear objects, such as the rings of a polygon (like ``patch`` above),
-provide the Numpy array interface.
-
-.. code-block:: pycon
-
-    >>> import numpy as np
-    >>> np.array(patch.exterior)
-    array([[  1.00000000e+01,   0.00000000e+00],
-           [  9.95184727e+00,  -9.80171403e-01],
-           [  9.80785280e+00,  -1.95090322e+00],
-           ...
-           [  1.00000000e+01,   0.00000000e+00]])
-
-That yields a Numpy array of ``[x, y]`` arrays. This is not always exactly what one
-wants for plotting shapes with Matplotlib (for example), so Shapely adds
-a ``xy`` property for obtaining separate arrays of coordinate x and y values.
-
-.. code-block:: pycon
-
-    >>> x, y = patch.exterior.xy
-    >>> np.array(x)
-    array([  1.00000000e+01,   9.95184727e+00,   9.80785280e+00,  ...])
-
-Numpy arrays of ``[x, y]`` arrays can also be adapted to Shapely linestrings.
-
-.. code-block:: pycon
-
-    >>> from shapely.geometry import LineString
-    >>> LineString(np.array(patch.exterior)).length
-    62.806623139095073
-
-Numpy arrays of x and y must be transposed.
-
-.. code-block:: pycon
-
-    >>> LineString(np.transpose(np.array(patch.exterior.xy))).length
-    62.80662313909507
-
-Shapely can also integrate with other Python GIS packages using data modeled
-after GeoJSON.
+Shapely can also integrate with other Python GIS packages using GeoJSON-like
+dicts.
 
 .. code-block:: pycon
 
diff --git a/build-scripts/macosx-10.6-intel.sh b/build-scripts/macosx-10.6-intel.sh
new file mode 100644
index 0000000..e240488
--- /dev/null
+++ b/build-scripts/macosx-10.6-intel.sh
@@ -0,0 +1,8 @@
+#!/bin/bash
+
+# Dependent on the Kyngchaos Frameworks:
+# http://www.kyngchaos.com/software/frameworks
+
+export GEOS_CONFIG="/Library/Frameworks/GEOS.framework/Versions/3/unix/bin/geos-config"
+CFLAGS="`$GEOS_CONFIG --cflags`" LDFLAGS="`$GEOS_CONFIG --clibs`" python setup.py bdist_wheel
+delocate-wheel -w fixed_wheels --require-archs=intel -v dist/Shapely-1.5.2-cp27-none-macosx_10_6_intel.whl
diff --git a/build-wheels.sh b/build-wheels.sh
new file mode 100644
index 0000000..4595f93
--- /dev/null
+++ b/build-wheels.sh
@@ -0,0 +1,21 @@
+#!/bin/bash
+
+# Automation of this is a TODO. For now, it depends on manually built libraries
+# as detailed in https://gist.github.com/sgillies/a8a2fb910a98a8566d0a.
+
+export MACOSX_DEPLOYMENT_TARGET=10.6
+export GEOS_CONFIG="/usr/local/bin/geos-config"
+
+VERSION=$1
+
+source $HOME/envs/pydotorg27/bin/activate
+touch shapely/speedups/*.pyx
+touch shapely/vectorized/*.pyx
+CFLAGS="`$GEOS_CONFIG --cflags`" LDFLAGS="`$GEOS_CONFIG --libs`" python setup.py bdist_wheel -d wheels/$VERSION
+source $HOME/envs/pydotorg34/bin/activate
+touch shapely/speedups/*.pyx
+touch shapely/vectorized/*.pyx
+CFLAGS="`$GEOS_CONFIG --cflags`" LDFLAGS="`$GEOS_CONFIG --libs`" python setup.py bdist_wheel -d wheels/$VERSION
+
+parallel delocate-wheel -w fixed_wheels/$VERSION --require-archs=intel -v {} ::: wheels/$VERSION/*.whl
+parallel cp {} {.}.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl ::: fixed_wheels/$VERSION/*.whl
diff --git a/docs/manual.rst b/docs/manual.rst
index 87933db..90b0a3d 100644
--- a/docs/manual.rst
+++ b/docs/manual.rst
@@ -1018,13 +1018,40 @@ evaluate topological, set-theoretic relationships. In a few cases the results
 may not be what one might expect starting from different assumptions. All take
 another geometric object as argument and return ``True`` or ``False``.
 
+.. method:: object.__eq__(other)
+
+  Returns ``True`` if the two objects are of the same geometric type, and
+  the coordinates of the two objects match precisely.
+
+.. method:: object.equals(other)
+
+  Returns ``True`` if the set-theoretic `boundary`, `interior`, and `exterior`
+  of the object coincide with those of the other.
+
+The coordinates passed to the object constructors are of these sets, and
+determine them, but are not the entirety of the sets. This is a potential
+"gotcha" for new users.  Equivalent lines, for example, can be constructed
+differently.
+
+.. code-block:: pycon
+
+  >>> a = LineString([(0, 0), (1, 1)])
+  >>> b = LineString([(0, 0), (0.5, 0.5), (1, 1)])
+  >>> c = LineString([(0, 0), (0, 0), (1, 1)])
+  >>> a.equals(b)
+  True
+  >>> a == b
+  False
+  >>> b.equals(c)
+  True
+  >>> b == c
+  False
+
 .. method:: object.almost_equals(other[, decimal=6])
 
   Returns ``True`` if the object is approximately equal to the `other` at all
   points to specified `decimal` place precision.
 
-See also :meth:`equals`.
-
 .. method:: object.contains(other)
 
   Returns ``True`` if the object's `interior` contains the `boundary` and
@@ -1092,29 +1119,6 @@ A line does not cross a point that it contains.
 
 This predicate applies to all types and is the inverse of :meth:`intersects`.
 
-.. method:: object.equals(other)
-
-  Returns ``True`` if the set-theoretic `boundary`, `interior`, and `exterior`
-  of the object coincide with those of the other.
-
-The coordinates passed to the object constructors are of these sets, and
-determine them, but are not the entirety of the sets. This is a potential
-"gotcha" for new users.  Equivalent lines, for example, can be constructed
-differently.
-
-.. code-block:: pycon
-
-  >>> a = LineString([(0, 0), (1, 1)])
-  >>> b = LineString([(0, 0), (0.5, 0.5), (1, 1)])
-  >>> c = LineString([(0, 0), (0, 0), (1, 1)])
-  >>> a.equals(b)
-  True
-  >>> b.equals(c)
-  True
-
-This predicate should not be mistaken for Python's ``==`` or ``is``
-constructions.
-
 .. method:: object.intersects(other)
 
   Returns ``True`` if the `boundary` and `interior` of the object intersect in
@@ -2019,6 +2023,31 @@ the nearest points in a pair of geometries.
 
 Note that the nearest points may not be existing vertices in the geometries.
 
+Snapping
+--------
+
+The :func:`~shapely.ops.snap` function in `shapely.ops` snaps the vertices in
+one geometry to the vertices in a second geometry with a given tolerance.
+
+.. function:: shapely.ops.snap(geom1, geom2, tolerance)
+
+   Snaps vertices in `geom1` to vertices in the `geom2`. A copy of the snapped
+   geometry is returned. The input geometries are not modified.
+
+   The `tolerance` argument specifies the minimum distance between vertices for
+   them to be snapped.
+
+   `New in version 1.4.5`
+
+.. code-block:: pycon
+
+  >>> from shapely.ops import snap
+  >>> square = Polygon([(1,1), (2, 1), (2, 2), (1, 2), (1, 1)])
+  >>> line = LineString([(0,0), (0.8, 0.8), (1.8, 0.95), (2.6, 0.5)])
+  >>> result = snap(line, square, 0.5)
+  >>> result.wkt
+  'LINESTRING (0 0, 1 1, 2 1, 2.6 0.5)'
+
 Prepared Geometry Operations
 ----------------------------
 
diff --git a/setup.py b/setup.py
index 1602923..9748470 100755
--- a/setup.py
+++ b/setup.py
@@ -36,7 +36,7 @@ if version is None:
 
 # Handle UTF-8 encoding of certain text files.
 open_kwds = {}
-if sys.version_info > (3,):
+if sys.version_info >= (3,):
     open_kwds['encoding'] = 'utf-8'
 
 with open('VERSION.txt', 'w', **open_kwds) as fp:
@@ -184,12 +184,13 @@ ext_modules = [
         include_dirs=[get_config_var('INCLUDEDIR')],),
 ]
 
+cmd_classes = setup_args.setdefault('cmdclass', {})
+
 try:
     import numpy as np
     from Cython.Distutils import build_ext as cython_build_ext
     from distutils.extension import Extension as DistutilsExtension
 
-    cmd_classes = setup_args.setdefault('cmdclass', {})
     if 'build_ext' in cmd_classes:
         raise ValueError('We need to put the Cython build_ext in '
                          'cmd_classes, but it is already defined.')
@@ -222,6 +223,9 @@ except BuildFailed as ex:
     print("Failure information, if any, is above.")
     print("I'm retrying the build without the C extension now.")
 
+    if 'build_ext' in cmd_classes:
+        del cmd_classes['build_ext']
+
     setup(**setup_args)
 
     print(BUILD_EXT_WARNING)
diff --git a/shapely/__init__.py b/shapely/__init__.py
index aa56ed4..424ebc4 100644
--- a/shapely/__init__.py
+++ b/shapely/__init__.py
@@ -1 +1 @@
-__version__ = "1.4.3"
+__version__ = "1.5.9"
diff --git a/shapely/_geos.pxi b/shapely/_geos.pxi
index fc22a57..d7b0fa5 100644
--- a/shapely/_geos.pxi
+++ b/shapely/_geos.pxi
@@ -13,7 +13,7 @@ cdef extern from "geos_c.h":
     GEOSCoordSequence *GEOSCoordSeq_create_r(GEOSContextHandle_t, unsigned int, unsigned int) nogil
     GEOSCoordSequence *GEOSGeom_getCoordSeq_r(GEOSContextHandle_t, GEOSGeometry *) nogil
 
-    int GEOSCoordSeq_getSize_r(GEOSContextHandle_t, GEOSCoordSequence *, int *) nogil
+    int GEOSCoordSeq_getSize_r(GEOSContextHandle_t, GEOSCoordSequence *, unsigned int *) nogil
     int GEOSCoordSeq_setX_r(GEOSContextHandle_t, GEOSCoordSequence *, int, double) nogil
     int GEOSCoordSeq_setY_r(GEOSContextHandle_t, GEOSCoordSequence *, int, double) nogil
     int GEOSCoordSeq_setZ_r(GEOSContextHandle_t, GEOSCoordSequence *, int, double) nogil
diff --git a/shapely/coords.py b/shapely/coords.py
index c37ffc0..bfac400 100644
--- a/shapely/coords.py
+++ b/shapely/coords.py
@@ -21,13 +21,14 @@ def required(ob):
     """Return an object that meets Shapely requirements for self-owned
     C-continguous data, copying if necessary, or just return the original
     object."""
-    if (hasattr(ob, '__array_interface__')
-            and ob.__array_interface__.get('strides')):
-        if has_numpy:
-            return numpy.require(ob, numpy.float64, ["C", "OWNDATA"])
-        else:
+    if hasattr(ob, '__array_interface__'):
+        if ob.__array_interface__.get('strides') and not has_numpy:
             # raise an error if strided. See issue #52.
             raise ValueError("C-contiguous data is required")
+        else:
+            # numpy.require will just return (ob) if it is already
+            # float64 and well-behaved.
+            return numpy.require(ob, numpy.float64, ["C", "OWNDATA"])
     else:
         return ob
 
diff --git a/shapely/ctypes_declarations.py b/shapely/ctypes_declarations.py
index ffe9912..3ee5141 100644
--- a/shapely/ctypes_declarations.py
+++ b/shapely/ctypes_declarations.py
@@ -14,7 +14,7 @@ class allocated_c_char_p(c_char_p):
     '''char pointer return type'''
     pass
 
-EXCEPTION_HANDLER_FUNCTYPE = CFUNCTYPE(None, c_char_p, c_char_p)
+EXCEPTION_HANDLER_FUNCTYPE = CFUNCTYPE(None, c_char_p, c_void_p)
 
 
 def prototype(lgeos, geos_version):
@@ -249,6 +249,9 @@ def prototype(lgeos, geos_version):
     lgeos.GEOSOverlaps.restype = c_byte
     lgeos.GEOSOverlaps.argtypes = [c_void_p, c_void_p]
 
+    lgeos.GEOSCovers.restype = c_byte
+    lgeos.GEOSCovers.argtypes = [c_void_p, c_void_p]
+
     lgeos.GEOSEquals.restype = c_byte
     lgeos.GEOSEquals.argtypes = [c_void_p, c_void_p]
 
@@ -306,17 +309,33 @@ def prototype(lgeos, geos_version):
         lgeos.GEOSPreparedGeom_destroy.restype = None
         lgeos.GEOSPreparedGeom_destroy.argtypes = [c_void_p]
 
-        lgeos.GEOSPreparedContains.restype = c_int
+        lgeos.GEOSPreparedDisjoint.restype = c_byte
+        lgeos.GEOSPreparedDisjoint.argtypes = [c_void_p, c_void_p]
+
+        lgeos.GEOSPreparedTouches.restype = c_byte
+        lgeos.GEOSPreparedTouches.argtypes = [c_void_p, c_void_p]
+
+        lgeos.GEOSPreparedIntersects.restype = c_byte
+        lgeos.GEOSPreparedIntersects.argtypes = [c_void_p, c_void_p]
+
+        lgeos.GEOSPreparedCrosses.restype = c_byte
+        lgeos.GEOSPreparedCrosses.argtypes = [c_void_p, c_void_p]
+
+        lgeos.GEOSPreparedWithin.restype = c_byte
+        lgeos.GEOSPreparedWithin.argtypes = [c_void_p, c_void_p]
+
+        lgeos.GEOSPreparedContains.restype = c_byte
         lgeos.GEOSPreparedContains.argtypes = [c_void_p, c_void_p]
 
-        lgeos.GEOSPreparedContainsProperly.restype = c_int
+        lgeos.GEOSPreparedContainsProperly.restype = c_byte
         lgeos.GEOSPreparedContainsProperly.argtypes = [c_void_p, c_void_p]
 
-        lgeos.GEOSPreparedCovers.restype = c_int
+        lgeos.GEOSPreparedOverlaps.restype = c_byte
+        lgeos.GEOSPreparedOverlaps.argtypes = [c_void_p, c_void_p]
+
+        lgeos.GEOSPreparedCovers.restype = c_byte
         lgeos.GEOSPreparedCovers.argtypes = [c_void_p, c_void_p]
 
-        lgeos.GEOSPreparedIntersects.restype = c_int
-        lgeos.GEOSPreparedIntersects.argtypes = [c_void_p, c_void_p]
 
     '''
     Geometry info
@@ -466,6 +485,10 @@ def prototype(lgeos, geos_version):
         lgeos.GEOSFree.restype = None
         lgeos.GEOSFree.argtypes = [c_void_p]
 
+    if geos_version >= (3, 3, 0):
+        lgeos.GEOSSnap.restype = c_void_p
+        lgeos.GEOSSnap.argtypes = [c_void_p, c_void_p, c_double]
+
     if geos_version >= (3, 4, 0):
         lgeos.GEOSNearestPoints.restype = c_void_p
         lgeos.GEOSNearestPoints.argtypes = [c_void_p, c_void_p]
diff --git a/shapely/geometry/base.py b/shapely/geometry/base.py
index 0a34998..69d8405 100644
--- a/shapely/geometry/base.py
+++ b/shapely/geometry/base.py
@@ -252,6 +252,17 @@ class BaseGeometry(object):
     def __xor__(self, other):
         return self.symmetric_difference(other)
 
+    def __eq__(self, other):
+        return (
+            isinstance(other, self.__class__) and
+            tuple(self.coords) == tuple(other.coords)
+        )
+
+    def __ne__(self, other):
+        return not self.__eq__(other)
+
+    __hash__ = object.__hash__
+
     # Array and ctypes interfaces
     # ---------------------------
 
@@ -342,35 +353,47 @@ class BaseGeometry(object):
         """WKB hex representation of the geometry"""
         return WKBWriter(lgeos).write_hex(self)
 
-    def svg(self, scale_factor=1.):
-        """
-        SVG representation of the geometry. Scale factor is multiplied by
-        the size of the SVG symbol so it can be scaled consistently for a
-        consistent appearance based on the canvas size.
-        """
+    def svg(self, scale_factor=1., **kwargs):
+        """Raises NotImplementedError"""
         raise NotImplementedError
 
     def _repr_svg_(self):
         """SVG representation for iPython notebook"""
-        #Pick an arbitrary size for the SVG canvas
-
-
-        xmin, ymin, xmax, ymax = self.buffer(1).bounds
-        x_size = min([max([100., xmax - xmin]), 300])
-        y_size = min([max([100., ymax - ymin]), 300])
-        try:
-            scale_factor = max([xmax - xmin, ymax - ymin]) / max([x_size, y_size])
-        except ZeroDivisionError:
-            scale_factor = 1
-        buffered_box = "{0} {1} {2} {3}".format(xmin, ymin, xmax - xmin, ymax - ymin)
-        return """<svg
-            preserveAspectRatio="xMinYMin meet"
-            viewBox="{0}"
-            width="{1}"
-            height="{2}"
-            transform="translate(0, {1}),scale(1, -1)">
-            {3}
-            </svg>""".format(buffered_box, x_size, y_size, self.svg(scale_factor))
+        svg_top = '<svg xmlns="http://www.w3.org/2000/svg" ' \
+            'xmlns:xlink="http://www.w3.org/1999/xlink" '
+        if self.is_empty:
+            return svg_top + '/>'
+        else:
+            # Establish SVG canvas that will fit all the data + small space
+            xmin, ymin, xmax, ymax = self.bounds
+            if xmin == xmax and ymin == ymax:
+                # This is a point; buffer using an arbitrary size
+                xmin, ymin, xmax, ymax = self.buffer(1).bounds
+            else:
+                # Expand bounds by a fraction of the data ranges
+                expand = 0.04  # or 4%, same as R plots
+                widest_part = max([xmax - xmin, ymax - ymin])
+                expand_amount = widest_part * expand
+                xmin -= expand_amount
+                ymin -= expand_amount
+                xmax += expand_amount
+                ymax += expand_amount
+            dx = xmax - xmin
+            dy = ymax - ymin
+            width = min([max([100., dx]), 300])
+            height = min([max([100., dy]), 300])
+            try:
+                scale_factor = max([dx, dy]) / max([width, height])
+            except ZeroDivisionError:
+                scale_factor = 1.
+            view_box = "{0} {1} {2} {3}".format(xmin, ymin, dx, dy)
+            transform = "matrix(1,0,0,-1,0,{0})".format(ymax + ymin)
+            return svg_top + (
+                'width="{1}" height="{2}" viewBox="{0}" '
+                'preserveAspectRatio="xMinYMin meet">'
+                '<g transform="{3}">{4}</g></svg>'
+                ).format(view_box, width, height, transform,
+                         self.svg(scale_factor))
 
     @property
     def geom_type(self):
@@ -561,7 +584,7 @@ class BaseGeometry(object):
     @property
     def is_closed(self):
         """True if the geometry is closed, else False
-        
+
         Applicable only to 1-D geometries."""
         if self.geom_type == 'LinearRing':
             return True
@@ -593,6 +616,10 @@ class BaseGeometry(object):
         (string)"""
         return self.impl['relate'](self, other)
 
+    def covers(self, other):
+        """Returns True if the geometry covers the other, else False"""
+        return bool(self.impl['covers'](self, other))
+
     def contains(self, other):
         """Returns True if the geometry contains the other, else False"""
         return bool(self.impl['contains'](self, other))
@@ -721,13 +748,36 @@ class BaseMultipartGeometry(BaseGeometry):
         else:
             return ()[index]
 
-    def svg(self, scale_factor=1.):
-        """
-        SVG representation of the geometry. Scale factor is multiplied by
-        the size of the SVG symbol so it can be scaled consistently for a
-        consistent appearance based on the canvas size.
+    def __eq__(self, other):
+        return (
+            isinstance(other, self.__class__) and
+            len(self) == len(other) and
+            all(x == y for x, y in zip(self, other))
+        )
+
+    def __ne__(self, other):
+        return not self.__eq__(other)
+
+    __hash__ = object.__hash__
+
+    def svg(self, scale_factor=1., color=None):
+        """Returns a group of SVG elements for the multipart geometry.
+
+        Parameters
+        ==========
+        scale_factor : float
+            Multiplication factor for the SVG stroke-width.  Default is 1.
+        color : str, optional
+            Hex string for stroke or fill color. Default is to use "#66cc99"
+            if geometry is valid, and "#ff3333" if invalid.
         """
-        return "\n".join([g.svg(scale_factor) for g in self])
+        if self.is_empty:
+            return '<g />'
+        if color is None:
+            color = "#66cc99" if self.is_valid else "#ff3333"
+        return '<g>' + \
+            ''.join(p.svg(scale_factor, color) for p in self) + \
+            '</g>'
 
 
 class GeometrySequence(object):
diff --git a/shapely/geometry/collection.py b/shapely/geometry/collection.py
index 6cc1df1..3c9d333 100644
--- a/shapely/geometry/collection.py
+++ b/shapely/geometry/collection.py
@@ -1,8 +1,13 @@
 """Multi-part collections of geometries
 """
 
+from ctypes import c_void_p
+
+from shapely.geos import lgeos
+from shapely.geometry.base import BaseGeometry
 from shapely.geometry.base import BaseMultipartGeometry
 from shapely.geometry.base import HeterogeneousGeometrySequence
+from shapely.geometry.base import geos_geom_from_py
 
 
 class GeometryCollection(BaseMultipartGeometry):
@@ -15,8 +20,26 @@ class GeometryCollection(BaseMultipartGeometry):
         A sequence of Shapely geometry instances
     """
 
-    def __init__(self):
+    def __init__(self, geoms=None):
+        """
+        Parameters
+        ----------
+        geoms : list
+            A list of shapely geometry instances, which may be heterogenous.
+        
+        Example
+        -------
+        Create a GeometryCollection with a Point and a LineString
+        
+          >>> p = Point(51, -1)
+          >>> l = LineString([(52, -1), (49, 2)])
+          >>> gc = GeometryCollection([p, l])
+        """
         BaseMultipartGeometry.__init__(self)
+        if not geoms:
+            pass
+        else:
+            self._geom, self._ndim = geos_geometrycollection_from_py(geoms)
 
     @property
     def __geo_interface__(self):
@@ -31,6 +54,19 @@ class GeometryCollection(BaseMultipartGeometry):
             return []
         return HeterogeneousGeometrySequence(self)
 
+def geos_geometrycollection_from_py(ob):
+    """Creates a GEOS GeometryCollection from a list of geometries"""
+    L = len(ob)
+    N = 2
+    subs = (c_void_p * L)()
+    for l in range(L):
+        assert(isinstance(ob[l], BaseGeometry))
+        if ob[l].has_z:
+            N = 3
+        geom, n = geos_geom_from_py(ob[l])
+        subs[l] = geom
+    
+    return (lgeos.GEOSGeom_createCollection(7, subs, L), N)
 
 # Test runner
 def _test():
diff --git a/shapely/geometry/linestring.py b/shapely/geometry/linestring.py
index dc39d8b..7cb5cf1 100644
--- a/shapely/geometry/linestring.py
+++ b/shapely/geometry/linestring.py
@@ -55,23 +55,26 @@ class LineString(BaseGeometry):
             'coordinates': tuple(self.coords)
             }
 
-    def svg(self, scale_factor=1.):
-        """
-        SVG representation of the geometry. Scale factor is multiplied by
-        the size of the SVG symbol so it can be scaled consistently for a
-        consistent appearance based on the canvas size.
+    def svg(self, scale_factor=1., stroke_color=None):
+        """Returns SVG polyline element for the LineString geometry.
+
+        Parameters
+        ==========
+        scale_factor : float
+            Multiplication factor for the SVG stroke-width.  Default is 1.
+        stroke_color : str, optional
+            Hex string for stroke color. Default is to use "#66cc99" if
+            geometry is valid, and "#ff3333" if invalid.
         """
+        if self.is_empty:
+            return '<g />'
+        if stroke_color is None:
+            stroke_color = "#66cc99" if self.is_valid else "#ff3333"
         pnt_format = " ".join(["{0},{1}".format(*c) for c in self.coords])
-        return """<polyline
-            fill="none"
-            stroke="{2}"
-            stroke-width={1}
-            points="{0}"
-            opacity=".8"
-            />""".format(
-                pnt_format,
-                2.*scale_factor, 
-                "#66cc99" if self.is_valid else "#ff3333")
+        return (
+            '<polyline fill="none" stroke="{2}" stroke-width="{1}" '
+            'points="{0}" opacity="0.8" />'
+            ).format(pnt_format, 2. * scale_factor, stroke_color)
 
     @property
     def ctypes(self):
diff --git a/shapely/geometry/multilinestring.py b/shapely/geometry/multilinestring.py
index 497a405..39b94fb 100644
--- a/shapely/geometry/multilinestring.py
+++ b/shapely/geometry/multilinestring.py
@@ -61,26 +61,24 @@ class MultiLineString(BaseMultipartGeometry):
             'coordinates': tuple(tuple(c for c in g.coords) for g in self.geoms)
             }
 
-    def svg(self, scale_factor=1.):
-        """
-        SVG representation of the geometry. Scale factor is multiplied by
-        the size of the SVG symbol so it can be scaled consistently for a
-        consistent appearance based on the canvas size.
+    def svg(self, scale_factor=1., stroke_color=None):
+        """Returns a group of SVG polyline elements for the LineString geometry.
+
+        Parameters
+        ==========
+        scale_factor : float
+            Multiplication factor for the SVG stroke-width.  Default is 1.
+        stroke_color : str, optional
+            Hex string for stroke color. Default is to use "#66cc99" if
+            geometry is valid, and "#ff3333" if invalid.
         """
-        parts = []
-        for part in self.geoms:
-            pnt_format = " ".join(["{0},{1}".format(*c) for c in part.coords])
-            parts.append("""<polyline
-                fill="none"
-                stroke="{2}"
-                stroke-width={1}
-                points="{0}"
-                opacity=".8"
-                />""".format(
-                    pnt_format,
-                    2.*scale_factor,
-                    "#66cc99" if self.is_valid else "#ff3333"))
-        return "\n".join(parts)
+        if self.is_empty:
+            return '<g />'
+        if stroke_color is None:
+            stroke_color = "#66cc99" if self.is_valid else "#ff3333"
+        return '<g>' + \
+            ''.join(p.svg(scale_factor, stroke_color) for p in self) + \
+            '</g>'
 
 
 class MultiLineStringAdapter(CachingGeometryProxy, MultiLineString):
@@ -117,46 +115,28 @@ def geos_multilinestring_from_py(ob):
     if isinstance(ob, MultiLineString):
          return geos_geom_from_py(ob)
 
+    obs = getattr(ob, 'geoms', ob)
+    L = len(obs)
+    assert L >= 1
+    exemplar = obs[0]
     try:
-        # From array protocol
-        array = ob.__array_interface__
-        assert len(array['shape']) == 1
-        L = array['shape'][0]
-        assert L >= 1
-
-        # Array of pointers to sub-geometries
-        subs = (c_void_p * L)()
-
-        for l in range(L):
-            geom, ndims = linestring.geos_linestring_from_py(array['data'][l])
-            subs[i] = cast(geom, c_void_p)
-
-        if lgeos.GEOSHasZ(subs[0]):
-            N = 3
-        else:
-            N = 2
-
-    except (NotImplementedError, AttributeError):
-        obs = getattr(ob, 'geoms', ob)
-        L = len(obs)
-        exemplar = obs[0]
-        try:
-            N = len(exemplar[0])
-        except TypeError:
-            N = exemplar._ndim
-        assert L >= 1
-        assert N == 2 or N == 3
-
-        # Array of pointers to point geometries
-        subs = (c_void_p * L)()
-        
-        # add to coordinate sequence
-        for l in range(L):
-            geom, ndims = linestring.geos_linestring_from_py(obs[l])
-            subs[l] = cast(geom, c_void_p)
+        N = len(exemplar[0])
+    except TypeError:
+        N = exemplar._ndim
+    if N not in (2, 3):
+        raise ValueError("Invalid coordinate dimensionality")
+
+    # Array of pointers to point geometries
+    subs = (c_void_p * L)()
+    
+    # add to coordinate sequence
+    for l in range(L):
+        geom, ndims = linestring.geos_linestring_from_py(obs[l])
+        subs[l] = cast(geom, c_void_p)
             
     return (lgeos.GEOSGeom_createCollection(5, subs, L), N)
 
+
 # Test runner
 def _test():
     import doctest
diff --git a/shapely/geometry/multipoint.py b/shapely/geometry/multipoint.py
index 633c65b..9ea8a6e 100644
--- a/shapely/geometry/multipoint.py
+++ b/shapely/geometry/multipoint.py
@@ -69,28 +69,24 @@ class MultiPoint(BaseMultipartGeometry):
             'coordinates': tuple([g.coords[0] for g in self.geoms])
             }
 
-    def svg(self, scale_factor=1.):
-        """
-        SVG representation of the geometry. Scale factor is multiplied by
-        the size of the SVG symbol so it can be scaled consistently for a
-        consistent appearance based on the canvas size.
+    def svg(self, scale_factor=1., fill_color=None):
+        """Returns a group of SVG circle elements for the MultiPoint geometry.
+
+        Parameters
+        ==========
+        scale_factor : float
+            Multiplication factor for the SVG circle diameters.  Default is 1.
+        fill_color : str, optional
+            Hex string for fill color. Default is to use "#66cc99" if
+            geometry is valid, and "#ff3333" if invalid.
         """
-        
-        parts = []
-        for part in self.geoms:
-            parts.append("""<circle
-            cx="{0.x}"
-            cy="{0.y}"
-            r="{1}"
-            stroke="#555555"
-            stroke-width="{2}"
-            fill="{3}"
-            opacity=".6"
-            />""".format(
-                part,
-                3*scale_factor,
-                1*scale_factor, "#66cc99" if self.is_valid else "#ff3333"))
-        return "\n".join(parts)
+        if self.is_empty:
+            return '<g />'
+        if fill_color is None:
+            fill_color = "#66cc99" if self.is_valid else "#ff3333"
+        return '<g>' + \
+            ''.join(p.svg(scale_factor, fill_color) for p in self) + \
+            '</g>'
 
     @property
     @exceptNull
diff --git a/shapely/geometry/multipolygon.py b/shapely/geometry/multipolygon.py
index 3e645de..eedd410 100644
--- a/shapely/geometry/multipolygon.py
+++ b/shapely/geometry/multipolygon.py
@@ -80,29 +80,24 @@ class MultiPolygon(BaseMultipartGeometry):
             'coordinates': allcoords
             }
 
-    def svg(self, scale_factor=1.):
-        """
-        SVG representation of the geometry. Scale factor is multiplied by
-        the size of the SVG symbol so it can be scaled consistently for a
-        consistent appearance based on the canvas size.
+    def svg(self, scale_factor=1., fill_color=None):
+        """Returns group of SVG path elements for the MultiPolygon geometry.
+
+        Parameters
+        ==========
+        scale_factor : float
+            Multiplication factor for the SVG stroke-width.  Default is 1.
+        fill_color : str, optional
+            Hex string for fill color. Default is to use "#66cc99" if
+            geometry is valid, and "#ff3333" if invalid.
         """
-        parts = []
-        for part in self.geoms:
-            exterior_coords = [["{0},{1}".format(*c) for c in part.exterior.coords]]
-            interior_coords = [
-                ["{0},{1}".format(*c) for c in interior.coords]
-                for interior in part.interiors ]
-            path = " ".join([
-                "M {0} L {1} z".format(coords[0], " L ".join(coords[1:]))
-                for coords in exterior_coords + interior_coords ])
-            parts.append(
-                """<g fill-rule="evenodd" fill="{2}" stroke="#555555"
-                stroke-width="{0}" opacity="0.6">
-                <path d="{1}" /></g>""".format(
-                    2. * scale_factor,
-                    path,
-                    "#66cc99" if self.is_valid else "#ff3333"))
-        return "\n".join(parts)
+        if self.is_empty:
+            return '<g />'
+        if fill_color is None:
+            fill_color = "#66cc99" if self.is_valid else "#ff3333"
+        return '<g>' + \
+            ''.join(p.svg(scale_factor, fill_color) for p in self) + \
+            '</g>'
 
 
 class MultiPolygonAdapter(CachingGeometryProxy, MultiPolygon):
diff --git a/shapely/geometry/point.py b/shapely/geometry/point.py
index d7660d6..e355839 100644
--- a/shapely/geometry/point.py
+++ b/shapely/geometry/point.py
@@ -74,25 +74,25 @@ class Point(BaseGeometry):
             'coordinates': self.coords[0]
             }
 
-    def svg(self, scale_factor=1.):
-        """
-        SVG representation of the geometry. Scale factor is multiplied by
-        the size of the SVG symbol so it can be scaled consistently for a
-        consistent appearance based on the canvas size.
+    def svg(self, scale_factor=1., fill_color=None):
+        """Returns SVG circle element for the Point geometry.
+
+        Parameters
+        ==========
+        scale_factor : float
+            Multiplication factor for the SVG circle diameter.  Default is 1.
+        fill_color : str, optional
+            Hex string for fill color. Default is to use "#66cc99" if
+            geometry is valid, and "#ff3333" if invalid.
         """
-        return """<circle
-            cx="{0.x}"
-            cy="{0.y}"
-            r="{1}"
-            stroke="#555555"
-            stroke-width="{2}"
-            fill="{3}"
-            opacity=".6"
-            />""".format(
-                self,
-                3 * scale_factor,
-                1 * scale_factor,
-                "#66cc99" if self.is_valid else "#ff3333")
+        if self.is_empty:
+            return '<g />'
+        if fill_color is None:
+            fill_color = "#66cc99" if self.is_valid else "#ff3333"
+        return (
+            '<circle cx="{0.x}" cy="{0.y}" r="{1}" '
+            'stroke="#555555" stroke-width="{2}" fill="{3}" opacity="0.6" />'
+            ).format(self, 3. * scale_factor, 1. * scale_factor, fill_color)
 
     @property
     def ctypes(self):
diff --git a/shapely/geometry/polygon.py b/shapely/geometry/polygon.py
index 4e6355b..167f4d9 100644
--- a/shapely/geometry/polygon.py
+++ b/shapely/geometry/polygon.py
@@ -27,7 +27,7 @@ class LinearRing(LineString):
     A LinearRing that crosses itself or touches itself at a single point is
     invalid and operations on it may fail.
     """
-    
+
     def __init__(self, coordinates=None):
         """
         Parameters
@@ -131,7 +131,7 @@ class InteriorRingSequence(object):
             self._index += 1
             return ring
         else:
-            raise StopIteration 
+            raise StopIteration
 
     if sys.version_info[0] < 3:
         next = __next__
@@ -182,7 +182,7 @@ class InteriorRingSequence(object):
             ring._ndim = self._ndim
             self.__rings__[i] = weakref.ref(ring)
         return self.__rings__[i]()
-        
+
 
 class Polygon(BaseGeometry):
     """
@@ -248,6 +248,24 @@ class Polygon(BaseGeometry):
             return []
         return InteriorRingSequence(self)
 
+    def __eq__(self, other):
+        if not isinstance(other, Polygon):
+            return False
+        my_coords = [
+            tuple(self.exterior.coords),
+            [tuple(interior.coords) for interior in self.interiors]
+        ]
+        other_coords = [
+            tuple(other.exterior.coords),
+            [tuple(interior.coords) for interior in other.interiors]
+        ]
+        return my_coords == other_coords
+
+    def __ne__(self, other):
+        return not self.__eq__(other)
+
+    __hash__ = object.__hash__
+
     @property
     def ctypes(self):
         if not self._ctypes_data:
@@ -282,29 +300,37 @@ class Polygon(BaseGeometry):
             'coordinates': tuple(coords)
             }
 
-    def svg(self, scale_factor=1.):
-        """
-        SVG representation of the geometry. Scale factor is multiplied by
-        the size of the SVG symbol so it can be scaled consistently for a
-        consistent appearance based on the canvas size.
+    def svg(self, scale_factor=1., fill_color=None):
+        """Returns SVG path element for the Polygon geometry.
+
+        Parameters
+        ==========
+        scale_factor : float
+            Multiplication factor for the SVG stroke-width.  Default is 1.
+        fill_color : str, optional
+            Hex string for fill color. Default is to use "#66cc99" if
+            geometry is valid, and "#ff3333" if invalid.
         """
-        exterior_coords = [["{0},{1}".format(*c) for c in self.exterior.coords]]
+        if self.is_empty:
+            return '<g />'
+        if fill_color is None:
+            fill_color = "#66cc99" if self.is_valid else "#ff3333"
+        exterior_coords = [
+            ["{0},{1}".format(*c) for c in self.exterior.coords]]
         interior_coords = [
             ["{0},{1}".format(*c) for c in interior.coords]
-            for interior in self.interiors ]
+            for interior in self.interiors]
         path = " ".join([
             "M {0} L {1} z".format(coords[0], " L ".join(coords[1:]))
-            for coords in exterior_coords + interior_coords ])
-        return """
-            <g fill-rule="evenodd" fill="{2}" stroke="#555555" 
-            stroke-width="{0}" opacity="0.6">
-            <path d="{1}" />
-            </g>""".format(
-                2.*scale_factor, path, "#66cc99" if self.is_valid else "#ff3333")
+            for coords in exterior_coords + interior_coords])
+        return (
+            '<path fill-rule="evenodd" fill="{2}" stroke="#555555" '
+            'stroke-width="{0}" opacity="0.6" d="{1}" />'
+            ).format(2. * scale_factor, path, fill_color)
 
 
 class PolygonAdapter(PolygonProxy, Polygon):
-    
+
     def __init__(self, shell, holes=None):
         self.shell = shell
         self.holes = holes
@@ -392,7 +418,7 @@ def geos_linearring_from_py(ob, update_geom=None, update_ndim=0):
 
         # add to coordinate sequence
         for i in range(m):
-            # Because of a bug in the GEOS C API, 
+            # Because of a bug in the GEOS C API,
             # always set X before Y
             lgeos.GEOSCoordSeq_setX(cs, i, cp[n*i])
             lgeos.GEOSCoordSeq_setY(cs, i, cp[n*i+1])
@@ -400,14 +426,14 @@ def geos_linearring_from_py(ob, update_geom=None, update_ndim=0):
                 lgeos.GEOSCoordSeq_setZ(cs, i, cp[n*i+2])
 
         # Add closing coordinates to sequence?
-        if M > m:        
-            # Because of a bug in the GEOS C API, 
+        if M > m:
+            # Because of a bug in the GEOS C API,
             # always set X before Y
             lgeos.GEOSCoordSeq_setX(cs, M-1, cp[0])
             lgeos.GEOSCoordSeq_setY(cs, M-1, cp[1])
             if n == 3:
                 lgeos.GEOSCoordSeq_setZ(cs, M-1, cp[2])
-            
+
     except AttributeError:
         # Fall back on list
         try:
@@ -437,11 +463,11 @@ def geos_linearring_from_py(ob, update_geom=None, update_ndim=0):
                 % update_ndim)
         else:
             cs = lgeos.GEOSCoordSeq_create(M, n)
-        
+
         # add to coordinate sequence
         for i in range(m):
             coords = ob[i]
-            # Because of a bug in the GEOS C API, 
+            # Because of a bug in the GEOS C API,
             # always set X before Y
             lgeos.GEOSCoordSeq_setX(cs, i, coords[0])
             lgeos.GEOSCoordSeq_setY(cs, i, coords[1])
@@ -454,7 +480,7 @@ def geos_linearring_from_py(ob, update_geom=None, update_ndim=0):
         # Add closing coordinates to sequence?
         if M > m:
             coords = ob[0]
-            # Because of a bug in the GEOS C API, 
+            # Because of a bug in the GEOS C API,
             # always set X before Y
             lgeos.GEOSCoordSeq_setX(cs, M-1, coords[0])
             lgeos.GEOSCoordSeq_setY(cs, M-1, coords[1])
@@ -490,7 +516,7 @@ def geos_polygon_from_py(shell, holes=None):
 
             # Array of pointers to ring geometries
             geos_holes = (c_void_p * L)()
-    
+
             # add to coordinate sequence
             for l in range(L):
                 geom, ndim = geos_linearring_from_py(ob[l])
diff --git a/shapely/geos.py b/shapely/geos.py
index 564f3cc..a00aa26 100644
--- a/shapely/geos.py
+++ b/shapely/geos.py
@@ -8,7 +8,8 @@ import sys
 import atexit
 import logging
 import threading
-from ctypes import CDLL, cdll, pointer, c_void_p, c_size_t, c_char_p, string_at
+from ctypes import CDLL, cdll, pointer, string_at, cast, POINTER
+from ctypes import c_void_p, c_size_t, c_char_p, c_int, c_float
 from ctypes.util import find_library
 
 from . import ftools
@@ -60,18 +61,29 @@ if sys.platform.startswith('linux'):
     free.restype = None
 
 elif sys.platform == 'darwin':
-    if hasattr(sys, 'frozen'):
-        # .app file from py2app
-        alt_paths = [os.path.join(os.environ['RESOURCEPATH'],
-                     '..', 'Frameworks', 'libgeos_c.dylib')]
+    # First test to see if this is a delocated wheel with a GEOS dylib.
+    geos_whl_dylib = os.path.abspath(
+            os.path.join(os.path.dirname(__file__), '.dylibs/libgeos_c.1.dylib'))
+    if os.path.exists(geos_whl_dylib):
+        _lgeos = CDLL(geos_whl_dylib)
     else:
-        alt_paths = [
-            # The Framework build from Kyng Chaos:
-            "/Library/Frameworks/GEOS.framework/Versions/Current/GEOS",
-            # macports
-            '/opt/local/lib/libgeos_c.dylib',
-        ]
-    _lgeos = load_dll('geos_c', fallbacks=alt_paths)
+        if hasattr(sys, 'frozen'):
+            try:
+                # .app file from py2app
+                alt_paths = [os.path.join(os.environ['RESOURCEPATH'],
+                            '..', 'Frameworks', 'libgeos_c.dylib')]
+            except KeyError:
+                # binary from pyinstaller
+                alt_paths = [os.path.join(sys.executable, 'libgeos_c.dylib')]
+        else:
+            alt_paths = [
+                # The Framework build from Kyng Chaos
+                "/Library/Frameworks/GEOS.framework/Versions/Current/GEOS",
+                # macports
+                '/opt/local/lib/libgeos_c.dylib',
+            ]
+        _lgeos = load_dll('geos_c', fallbacks=alt_paths)
+
     free = load_dll('c').free
     free.argtypes = [c_void_p]
     free.restype = None
@@ -177,19 +189,33 @@ class TopologicalError(Exception):
 class PredicateError(Exception):
     pass
 
-
-def error_handler(fmt, *args):
-    if sys.version_info[0] >= 3:
+# While this function can take any number of positional arguments when
+# called from Python and GEOS expects its error handler to accept any
+# number of arguments (like printf), I'm unable to get ctypes to make
+# a callback object from this function that will accept any number of
+# arguments.
+#
+# At the moment, functions in the GEOS C API only pass 0 or 1 arguments
+# to the error handler. We can deal with this, but when if that changes,
+# Shapely may break.
+
+def handler(level):
+    def callback(fmt, *args):
         fmt = fmt.decode('ascii')
-        args = [arg.decode('ascii') for arg in args]
-    LOG.error(fmt, *args)
-
+        conversions = re.findall(r'%.', fmt)
+        log_vals = []
+        for spec, arg in zip(conversions, args):
+            if spec == '%s' and arg is not None:
+                log_vals.append(string_at(arg).decode('ascii'))
+            else:
+                LOG.error("An error occurred, but the format string "
+                          "'%s' could not be converted.", fmt)
+                return
+        getattr(LOG, level)(fmt, *log_vals)
+    return callback
 
-def notice_handler(fmt, args):
-    if sys.version_info[0] >= 3:
-        fmt = fmt.decode('ascii')
-        args = args.decode('ascii')
-    LOG.warning(fmt, args)
+error_handler = handler('error')
+notice_handler = handler('warning')
 
 error_h = EXCEPTION_HANDLER_FUNCTYPE(error_handler)
 notice_h = EXCEPTION_HANDLER_FUNCTYPE(notice_handler)
@@ -620,8 +646,18 @@ class LGEOS310(LGEOSBase):
                 self.GEOSWithin,
                 self.GEOSContains,
                 self.GEOSOverlaps,
+                self.GEOSCovers,
                 self.GEOSEquals,
                 self.GEOSEqualsExact,
+                self.GEOSPreparedDisjoint,
+                self.GEOSPreparedTouches,
+                self.GEOSPreparedCrosses,
+                self.GEOSPreparedWithin,
+                self.GEOSPreparedOverlaps,
+                self.GEOSPreparedContains,
+                self.GEOSPreparedContainsProperly,
+                self.GEOSPreparedCovers,
+                self.GEOSPreparedIntersects,
                 self.GEOSisEmpty,
                 self.GEOSisValid,
                 self.GEOSisSimple,
@@ -652,6 +688,7 @@ class LGEOS310(LGEOSBase):
         self.methods['within'] = self.GEOSWithin
         self.methods['contains'] = self.GEOSContains
         self.methods['overlaps'] = self.GEOSOverlaps
+        self.methods['covers'] = self.GEOSCovers
         self.methods['equals'] = self.GEOSEquals
         self.methods['equals_exact'] = self.GEOSEqualsExact
         self.methods['relate'] = self.GEOSRelate
@@ -659,10 +696,15 @@ class LGEOS310(LGEOSBase):
         self.methods['symmetric_difference'] = self.GEOSSymDifference
         self.methods['union'] = self.GEOSUnion
         self.methods['intersection'] = self.GEOSIntersection
+        self.methods['prepared_disjoint'] = self.GEOSPreparedDisjoint
+        self.methods['prepared_touches'] = self.GEOSPreparedTouches
         self.methods['prepared_intersects'] = self.GEOSPreparedIntersects
+        self.methods['prepared_crosses'] = self.GEOSPreparedCrosses
+        self.methods['prepared_within'] = self.GEOSPreparedWithin
         self.methods['prepared_contains'] = self.GEOSPreparedContains
         self.methods['prepared_contains_properly'] = \
             self.GEOSPreparedContainsProperly
+        self.methods['prepared_overlaps'] = self.GEOSPreparedOverlaps
         self.methods['prepared_covers'] = self.GEOSPreparedCovers
         self.methods['simplify'] = self.GEOSSimplify
         self.methods['topology_preserve_simplify'] = \
@@ -721,6 +763,7 @@ class LGEOS330(LGEOS320):
         self.methods['unary_union'] = self.GEOSUnaryUnion
         self.methods['is_closed'] = self.GEOSisClosed
         self.methods['cascaded_union'] = self.methods['unary_union']
+        self.methods['snap'] = self.GEOSSnap
 
 
 class LGEOS340(LGEOS330):
diff --git a/shapely/impl.py b/shapely/impl.py
index 2d670cf..fda6281 100644
--- a/shapely/impl.py
+++ b/shapely/impl.py
@@ -83,6 +83,7 @@ IMPL300 = {
     'overlaps': (BinaryPredicate, 'overlaps'),
     'touches': (BinaryPredicate, 'touches'),
     'within': (BinaryPredicate, 'within'),
+    'covers': (BinaryPredicate, 'covers'),
     'equals_exact': (BinaryPredicate, 'equals_exact'),
 
     # First pure Python implementation
@@ -93,6 +94,11 @@ IMPL310 = {
     'simplify': (UnaryTopologicalOp, 'simplify'),
     'topology_preserve_simplify':
         (UnaryTopologicalOp, 'topology_preserve_simplify'),
+    'prepared_disjoint': (BinaryPredicate, 'prepared_disjoint'),
+    'prepared_touches': (BinaryPredicate, 'prepared_touches'),
+    'prepared_crosses': (BinaryPredicate, 'prepared_crosses'),
+    'prepared_within': (BinaryPredicate, 'prepared_within'),
+    'prepared_overlaps': (BinaryPredicate, 'prepared_overlaps'),
     'prepared_intersects': (BinaryPredicate, 'prepared_intersects'),
     'prepared_contains': (BinaryPredicate, 'prepared_contains'),
     'prepared_contains_properly':
diff --git a/shapely/iterops.py b/shapely/iterops.py
index 70faef0..4b15b84 100644
--- a/shapely/iterops.py
+++ b/shapely/iterops.py
@@ -1,29 +1,16 @@
 """
 Iterative forms of operations
 """
-from warnings import warn
-from ctypes import c_char_p, c_size_t
-from shapely.geos import lgeos, PredicateError
 
+from shapely.geos import PredicateError
+from shapely.topology import Delegating
 
-def geos_from_geometry(geom):
-    warn("`geos_from_geometry` is deprecated. Use geometry's `wkb` property "
-         "instead.", DeprecationWarning)
-    data = geom.to_wkb()
-    return lgeos.GEOSGeomFromWKB_buf(
-                        c_char_p(data),
-                        c_size_t(len(data))
-                        )
 
+class IterOp(Delegating):
 
-class IterOp(object):
-    
     """A generating non-data descriptor.
     """
-    
-    def __init__(self, fn):
-        self.fn = fn
-    
+
     def __call__(self, context, iterator, value=True):
         if context._geom is None:
             raise ValueError("Null geometry supports no operations")
@@ -35,21 +22,20 @@ class IterOp(object):
                 ob = this_geom
             if not this_geom._geom:
                 raise ValueError("Null geometry supports no operations")
-            retval = self.fn(context._geom, this_geom._geom)
-            if retval == 2:
-                raise PredicateError(
-                    "Failed to evaluate %s" % repr(self.fn))
-            elif bool(retval) == value:
+            try:
+                retval = self.fn(context._geom, this_geom._geom)
+            except Exception as err:
+                self._check_topology(err, context, this_geom)
+            if bool(retval) == value:
                 yield ob
 
 
 # utilities
-disjoint = IterOp(lgeos.GEOSDisjoint)
-touches = IterOp(lgeos.GEOSTouches)
-intersects = IterOp(lgeos.GEOSIntersects)
-crosses = IterOp(lgeos.GEOSCrosses)
-within = IterOp(lgeos.GEOSWithin)
-contains = IterOp(lgeos.GEOSContains)
-overlaps = IterOp(lgeos.GEOSOverlaps)
-equals = IterOp(lgeos.GEOSEquals)
-
+disjoint = IterOp('disjoint')
+touches = IterOp('touches')
+intersects = IterOp('intersects')
+crosses = IterOp('crosses')
+within = IterOp('within')
+contains = IterOp('contains')
+overlaps = IterOp('overlaps')
+equals = IterOp('equals')
diff --git a/shapely/ops.py b/shapely/ops.py
index fa5be14..b1aff95 100644
--- a/shapely/ops.py
+++ b/shapely/ops.py
@@ -51,7 +51,7 @@ class CollectionOperator(object):
         for g in collection.geoms:
             clone = lgeos.GEOSGeom_clone(g._geom)
             g = geom_factory(clone)
-            g._owned = False
+            g._other_owned = False
             yield g
 
     def polygonize_full(self, lines):
@@ -275,3 +275,29 @@ def nearest_points(g1, g2):
     p1 = Point(x1.value, y1.value)
     p2 = Point(x2.value, y2.value)
     return (p1, p2)
+
+def snap(g1, g2, tolerance):
+    """Snap one geometry to another with a given tolerance
+
+    Vertices of the first geometry are snapped to vertices of the second
+    geometry. The resulting snapped geometry is returned. The input geometries
+    are not modified.
+
+    Parameters
+    ----------
+    g1 : geometry
+        The first geometry
+    g2 : geometry
+        The second geometry
+    tolerence : float
+        The snapping tolerance
+
+    Example
+    -------
+    >>> square = Polygon([(1,1), (2, 1), (2, 2), (1, 2), (1, 1)])
+    >>> line = LineString([(0,0), (0.8, 0.8), (1.8, 0.95), (2.6, 0.5)])
+    >>> result = snap(line, square, 0.5)
+    >>> result.wkt
+    'LINESTRING (0 0, 1 1, 2 1, 2.6 0.5)'
+    """
+    return(geom_factory(lgeos.methods['snap'](g1._geom, g2._geom, tolerance)))
diff --git a/shapely/predicates.py b/shapely/predicates.py
index 25a4711..eff42c9 100644
--- a/shapely/predicates.py
+++ b/shapely/predicates.py
@@ -2,22 +2,24 @@
 Support for GEOS spatial predicates
 """
 
+from shapely.geos import PredicateError, TopologicalError
 from shapely.topology import Delegating
 
+
 class BinaryPredicate(Delegating):
+
     def __call__(self, this, other, *args):
         self._validate(this)
         self._validate(other, stop_prepared=True)
-        return self.fn(this._geom, other._geom, *args)
+        try:
+            return self.fn(this._geom, other._geom, *args)
+        except PredicateError as err:
+            # Dig deeper into causes of errors.
+            self._check_topology(err, this, other)
 
-class RelateOp(Delegating):
-    def __call__(self, this, other):
-        self._validate(this)
-        self._validate(other, stop_prepared=True)
-        return self.fn(this._geom, other._geom)
 
 class UnaryPredicate(Delegating):
+
     def __call__(self, this):
         self._validate(this)
         return self.fn(this._geom)
-
diff --git a/shapely/prepared.py b/shapely/prepared.py
index 5a3bf22..fb9080a 100644
--- a/shapely/prepared.py
+++ b/shapely/prepared.py
@@ -37,23 +37,51 @@ class PreparedGeometry(object):
     @property
     def _geom(self):
         return self.__geom__
-   
-    @delegated
-    def intersects(self, other):
-        return bool(self.impl['prepared_intersects'](self, other))
 
     @delegated
     def contains(self, other):
+        """Returns True if the geometry contains the other, else False"""
         return bool(self.impl['prepared_contains'](self, other))
 
     @delegated
     def contains_properly(self, other):
+        """Returns True if the geometry properly contains the other, else False"""
         return bool(self.impl['prepared_contains_properly'](self, other))
 
     @delegated
     def covers(self, other):
+        """Returns True if the geometry covers the other, else False"""
         return bool(self.impl['prepared_covers'](self, other))
 
+    @delegated
+    def crosses(self, other):
+        """Returns True if the geometries cross, else False"""
+        return bool(self.impl['prepared_crosses'](self, other))
+
+    @delegated
+    def disjoint(self, other):
+        """Returns True if geometries are disjoint, else False"""
+        return bool(self.impl['prepared_disjoint'](self, other))
+
+    @delegated
+    def intersects(self, other):
+        """Returns True if geometries intersect, else False"""
+        return bool(self.impl['prepared_intersects'](self, other))
+
+    @delegated
+    def overlaps(self, other):
+        """Returns True if geometries overlap, else False"""
+        return bool(self.impl['prepared_overlaps'](self, other))
+
+    @delegated
+    def touches(self, other):
+        """Returns True if geometries touch, else False"""
+        return bool(self.impl['prepared_touches'](self, other))
+
+    @delegated
+    def within(self, other):
+        """Returns True if geometry is within the other, else False"""
+        return bool(self.impl['prepared_within'](self, other))
 
 def prep(ob):
     """Creates and returns a prepared geometric object."""
diff --git a/shapely/speedups/__init__.py b/shapely/speedups/__init__.py
index a472aaf..60d9d0b 100644
--- a/shapely/speedups/__init__.py
+++ b/shapely/speedups/__init__.py
@@ -2,6 +2,7 @@ import warnings
 
 from shapely.geometry import linestring, polygon
 from shapely import coords
+import shapely.affinity
 
 try:
     from shapely.speedups import _speedups
@@ -41,6 +42,13 @@ def enable():
 
     _orig['geos_linearring_from_py']  = polygon.geos_linearring_from_py
     polygon.geos_linearring_from_py = _speedups.geos_linearring_from_py
+    
+    _orig['affine_transform'] = shapely.affinity.affine_transform
+    # copy docstring from original function
+    def affine_transform(geom, matrix):
+        return _speedups.affine_transform(geom, matrix)
+    affine_transform.__doc__ = shapely.affinity.affine_transform.__doc__
+    shapely.affinity.affine_transform = affine_transform
 
 def disable():
     if not _orig:
@@ -50,4 +58,5 @@ def disable():
     coords.CoordinateSequence.__iter__ = _orig['CoordinateSequence.__iter__']
     linestring.geos_linestring_from_py = _orig['geos_linestring_from_py']
     polygon.geos_linearring_from_py = _orig['geos_linearring_from_py']
+    affinity.affine_transform = _orig['affine_transform']
     _orig.clear()
diff --git a/shapely/speedups/_speedups.pyx b/shapely/speedups/_speedups.pyx
index bf3d505..b5e9a6d 100644
--- a/shapely/speedups/_speedups.pyx
+++ b/shapely/speedups/_speedups.pyx
@@ -6,24 +6,32 @@
 # Transcription to cython: Copyright (c) 2011, Oliver Tonnhofer
 
 import ctypes
+
+from shapely.coords import required
 from shapely.geos import lgeos
 from shapely.geometry import Point, LineString, LinearRing
+from shapely.geometry.base import geom_factory
 
 include "../_geos.pxi"
-    
 
-cdef inline GEOSGeometry *cast_geom(unsigned long geom_addr):
+from libc.stdint cimport uintptr_t
+
+cdef inline GEOSGeometry *cast_geom(uintptr_t geom_addr):
     return <GEOSGeometry *>geom_addr
 
-cdef inline GEOSContextHandle_t cast_handle(unsigned long handle_addr):
+
+cdef inline GEOSContextHandle_t cast_handle(uintptr_t handle_addr):
     return <GEOSContextHandle_t>handle_addr
 
-cdef inline GEOSCoordSequence *cast_seq(unsigned long handle_addr):
+
+cdef inline GEOSCoordSequence *cast_seq(uintptr_t handle_addr):
     return <GEOSCoordSequence *>handle_addr
 
+
 def destroy(geom):
     GEOSGeom_destroy_r(cast_handle(lgeos.geos_handle), cast_geom(geom))
 
+
 def geos_linestring_from_py(ob, update_geom=None, update_ndim=0):
     cdef double *cp
     cdef GEOSContextHandle_t handle = cast_handle(lgeos.geos_handle)
@@ -42,12 +50,15 @@ def geos_linestring_from_py(ob, update_geom=None, update_ndim=0):
             n = 2
 
         if type(ob) == LineString:
-            return <unsigned long>GEOSGeom_clone_r(handle, g), n
+            return <uintptr_t>GEOSGeom_clone_r(handle, g), n
         else:
-            cs = GEOSGeom_getCoordSeq_r(handle, g)
+            cs = <GEOSCoordSequence*>GEOSGeom_getCoordSeq_r(handle, g)
             cs = GEOSCoordSeq_clone_r(handle, cs)
-            return <unsigned long>GEOSGeom_createLineString_r(handle, cs), n
+            return <uintptr_t>GEOSGeom_createLineString_r(handle, cs), n
 
+    # If numpy is present, we use numpy.require to ensure that we have a
+    # C-continguous array that owns its data. View data will be copied.
+    ob = required(ob)
     try:
         # From array protocol
         array = ob.__array_interface__
@@ -65,9 +76,9 @@ def geos_linestring_from_py(ob, update_geom=None, update_ndim=0):
 
         # Make pointer to the coordinate array
         if isinstance(array['data'], ctypes.Array):
-            cp = <double *><unsigned long>ctypes.addressof(array['data'])
+            cp = <double *><uintptr_t>ctypes.addressof(array['data'])
         else:
-            cp = <double *><unsigned long>array['data'][0]
+            cp = <double *><uintptr_t>array['data'][0]
 
         # Use strides to properly index into cp
         # ob[i, j] == cp[sm*i + sn*j]
@@ -82,7 +93,7 @@ def geos_linestring_from_py(ob, update_geom=None, update_ndim=0):
 
         # Create a coordinate sequence
         if update_geom is not None:
-            cs = GEOSGeom_getCoordSeq_r(handle, cast_geom(update_geom))
+            cs = <GEOSCoordSequence*>GEOSGeom_getCoordSeq_r(handle, cast_geom(update_geom))
             if n != update_ndim:
                 raise ValueError(
                 "Wrong coordinate dimensions; this geometry has dimensions: %d" \
@@ -131,7 +142,7 @@ def geos_linestring_from_py(ob, update_geom=None, update_ndim=0):
 
         # Create a coordinate sequence
         if update_geom is not None:
-            cs = GEOSGeom_getCoordSeq_r(handle, cast_geom(update_geom))
+            cs = <GEOSCoordSequence*>GEOSGeom_getCoordSeq_r(handle, cast_geom(update_geom))
             if n != update_ndim:
                 raise ValueError(
                 "Wrong coordinate dimensions; this geometry has dimensions: %d" \
@@ -160,7 +171,7 @@ def geos_linestring_from_py(ob, update_geom=None, update_ndim=0):
     if update_geom is not None:
         return None
     else:
-        return <unsigned long>GEOSGeom_createLineString_r(handle, cs), n
+        return <uintptr_t>GEOSGeom_createLineString_r(handle, cs), n
 
 
 def geos_linearring_from_py(ob, update_geom=None, update_ndim=0):
@@ -169,7 +180,8 @@ def geos_linearring_from_py(ob, update_geom=None, update_ndim=0):
     cdef GEOSGeometry *g
     cdef GEOSCoordSequence *cs
     cdef double dx, dy, dz
-    cdef int i, n, m, M, sm, sn
+    cdef unsigned int m
+    cdef int i, n, M, sm, sn
 
     # If a LinearRing is passed in, just clone it and return
     # If a LineString is passed in, clone the coord seq and return a LinearRing
@@ -181,14 +193,17 @@ def geos_linearring_from_py(ob, update_geom=None, update_ndim=0):
             n = 2
 
         if type(ob) == LinearRing:
-            return <unsigned long>GEOSGeom_clone_r(handle, g), n
+            return <uintptr_t>GEOSGeom_clone_r(handle, g), n
         else:
-            cs = GEOSGeom_getCoordSeq_r(handle, g)
+            cs = <GEOSCoordSequence*>GEOSGeom_getCoordSeq_r(handle, g)
             GEOSCoordSeq_getSize_r(handle, cs, &m)
             if GEOSisClosed_r(handle, g) and m >= 4:
                 cs = GEOSCoordSeq_clone_r(handle, cs)
-                return <unsigned long>GEOSGeom_createLinearRing_r(handle, cs), n
+                return <uintptr_t>GEOSGeom_createLinearRing_r(handle, cs), n
 
+    # If numpy is present, we use numpy.require to ensure that we have a
+    # C-continguous array that owns its data. View data will be copied.
+    ob = required(ob)
     try:
         # From array protocol
         array = ob.__array_interface__
@@ -202,9 +217,9 @@ def geos_linearring_from_py(ob, update_geom=None, update_ndim=0):
 
         # Make pointer to the coordinate array
         if isinstance(array['data'], ctypes.Array):
-            cp = <double *><unsigned long>ctypes.addressof(array['data'])
+            cp = <double *><uintptr_t>ctypes.addressof(array['data'])
         else:
-            cp = <double *><unsigned long>array['data'][0]
+            cp = <double *><uintptr_t>array['data'][0]
 
         # Use strides to properly index into cp
         # ob[i, j] == cp[sm*i + sn*j]
@@ -227,7 +242,7 @@ def geos_linearring_from_py(ob, update_geom=None, update_ndim=0):
 
         # Create a coordinate sequence
         if update_geom is not None:
-            cs = GEOSGeom_getCoordSeq_r(handle, cast_geom(update_geom))
+            cs = <GEOSCoordSequence*>GEOSGeom_getCoordSeq_r(handle, cast_geom(update_geom))
             if n != update_ndim:
                 raise ValueError(
                 "Wrong coordinate dimensions; this geometry has dimensions: %d" \
@@ -286,7 +301,7 @@ def geos_linearring_from_py(ob, update_geom=None, update_ndim=0):
 
         # Create a coordinate sequence
         if update_geom is not None:
-            cs = GEOSGeom_getCoordSeq_r(handle, cast_geom(update_geom))
+            cs = <GEOSCoordSequence*>GEOSGeom_getCoordSeq_r(handle, cast_geom(update_geom))
             if n != update_ndim:
                 raise ValueError(
                 "Wrong coordinate dimensions; this geometry has dimensions: %d" \
@@ -329,7 +344,7 @@ def geos_linearring_from_py(ob, update_geom=None, update_ndim=0):
     if update_geom is not None:
         return None
     else:
-        return <unsigned long>GEOSGeom_createLinearRing_r(handle, cs), n
+        return <uintptr_t>GEOSGeom_createLinearRing_r(handle, cs), n
 
 
 def coordseq_ctypes(self):
@@ -345,7 +360,7 @@ def coordseq_ctypes(self):
     data = array_type()
     
     cs = cast_seq(self._cseq)
-    data_p = <double *><unsigned long>ctypes.addressof(data)
+    data_p = <double *><uintptr_t>ctypes.addressof(data)
     
     for i in xrange(m):
         GEOSCoordSeq_getX_r(handle, cs, i, &temp)
@@ -379,3 +394,107 @@ def coordseq_iter(self):
             yield (dx, dy, dz)
         else:
             yield (dx, dy)
+
+cdef GEOSCoordSequence* transform(GEOSCoordSequence* cs,
+                                  int ndim,
+                                  double a,
+                                  double b,
+                                  double c,
+                                  double d,
+                                  double e,
+                                  double f,
+                                  double g,
+                                  double h,
+                                  double i,
+                                  double xoff,
+                                  double yoff,
+                                  double zoff):
+    """Performs an affine transformation on a GEOSCoordSequence
+    
+    Returns the transformed coordinate sequence
+    """
+    cdef GEOSContextHandle_t handle = cast_handle(lgeos.geos_handle)
+    cdef unsigned int m
+    cdef GEOSCoordSequence *cs_t
+    cdef double x, y, z
+    cdef double x_t, y_t, z_t
+    
+    # create a new coordinate sequence with the same size and dimensions
+    GEOSCoordSeq_getSize_r(handle, cs, &m)
+    cs_t = GEOSCoordSeq_create_r(handle, m, ndim)
+    
+    # perform the transform
+    for n in range(0, m):
+        GEOSCoordSeq_getX_r(handle, cs, n, &x)
+        GEOSCoordSeq_getY_r(handle, cs, n, &y)
+        x_t = a * x + b * y + xoff
+        y_t = d * x + e * y + yoff
+        GEOSCoordSeq_setX_r(handle, cs_t, n, x_t)
+        GEOSCoordSeq_setY_r(handle, cs_t, n, y_t)
+    if ndim == 3:
+        for n in range(0, m):
+            GEOSCoordSeq_getZ_r(handle, cs, n, &z)
+            z_t = g * x + h * y + i * z + zoff
+            GEOSCoordSeq_setZ_r(handle, cs_t, n, z_t)
+    
+    return cs_t
+
+cpdef affine_transform(geom, matrix):
+    cdef double a, b, c, d, e, f, g, h, i, xoff, yoff, zoff
+    if geom.is_empty:
+        return geom
+    if len(matrix) == 6:
+        ndim = 2
+        a, b, d, e, xoff, yoff = matrix
+        if geom.has_z:
+            ndim = 3
+            i = 1.0
+            c = f = g = h = zoff = 0.0
+            matrix = a, b, c, d, e, f, g, h, i, xoff, yoff, zoff
+    elif len(matrix) == 12:
+        ndim = 3
+        a, b, c, d, e, f, g, h, i, xoff, yoff, zoff = matrix
+        if not geom.has_z:
+            ndim = 2
+            matrix = a, b, d, e, xoff, yoff
+    else:
+        raise ValueError("'matrix' expects either 6 or 12 coefficients")
+    
+    cdef GEOSContextHandle_t handle = cast_handle(lgeos.geos_handle)
+    cdef GEOSCoordSequence *cs
+    cdef GEOSCoordSequence *cs_t
+    cdef GEOSGeometry *the_geom
+    cdef GEOSGeometry *the_geom_t
+    cdef int m, n
+    cdef double x, y, z
+    cdef double x_t, y_t, z_t
+    
+    # Process coordinates from each supported geometry type
+    if geom.type in ('Point', 'LineString', 'LinearRing'):
+        the_geom = cast_geom(geom._geom)
+        cs = <GEOSCoordSequence*>GEOSGeom_getCoordSeq_r(handle, the_geom)
+        
+        # perform the transformation
+        cs_t = transform(cs, ndim, a, b, c, d, e, f, g, h, i, xoff, yoff, zoff)
+        
+        # create a new geometry from the coordinate sequence
+        if geom.type == 'Point':
+            the_geom_t = GEOSGeom_createPoint_r(handle, cs_t)
+        elif geom.type == 'LineString':
+            the_geom_t = GEOSGeom_createLineString_r(handle, cs_t)
+        elif geom.type == 'LinearRing':
+            the_geom_t = GEOSGeom_createLinearRing_r(handle, cs_t)
+        
+        # return the geometry as a Python object
+        return geom_factory(<uintptr_t>the_geom_t)
+    elif geom.type == 'Polygon':
+        ring = geom.exterior
+        shell = affine_transform(ring, matrix)
+        holes = list(geom.interiors)
+        for pos, ring in enumerate(holes):
+            holes[pos] = affine_transform(ring, matrix)
+        return type(geom)(shell, holes)
+    elif geom.type.startswith('Multi') or geom.type == 'GeometryCollection':
+        return type(geom)([affine_transform(part, matrix) for part in geom.geoms])
+    else:
+        raise ValueError('Type %r not recognized' % geom.type)
diff --git a/shapely/topology.py b/shapely/topology.py
index 04fc412..6a4e79e 100644
--- a/shapely/topology.py
+++ b/shapely/topology.py
@@ -10,18 +10,37 @@ These methods return ctypes objects that should be recast by the caller.
 from ctypes import byref, c_double
 from shapely.geos import TopologicalError, lgeos
 
+
 class Validating(object):
+
     def _validate(self, ob, stop_prepared=False):
         if ob is None or ob._geom is None:
             raise ValueError("Null geometry supports no operations")
         if stop_prepared and not hasattr(ob, 'type'):
             raise ValueError("Prepared geometries cannot be operated on")
 
+
 class Delegating(Validating):
+
     def __init__(self, name):
         self.fn = lgeos.methods[name]
 
+    def _check_topology(self, err, *geoms):
+        """Raise TopologicalError if geoms are invalid.
+
+        Else, raise original error.
+        """
+        for geom in geoms:
+            if not geom.is_valid:
+                raise TopologicalError(
+                    "The operation '%s' could not be performed. "
+                    "Likely cause is invalidity of the geometry %s" % (
+                        self.fn.__name__, repr(geom)))
+        raise err
+
+
 class BinaryRealProperty(Delegating):
+
     def __call__(self, this, other):
         self._validate(this)
         self._validate(other, stop_prepared=True)
@@ -29,32 +48,31 @@ class BinaryRealProperty(Delegating):
         retval = self.fn(this._geom, other._geom, byref(d))
         return d.value
 
+
 class UnaryRealProperty(Delegating):
+
     def __call__(self, this):
         self._validate(this)
         d = c_double()
         retval = self.fn(this._geom, byref(d))
         return d.value
 
+
 class BinaryTopologicalOp(Delegating):
+
     def __call__(self, this, other, *args):
         self._validate(this)
         self._validate(other, stop_prepared=True)
         product = self.fn(this._geom, other._geom, *args)
         if product is None:
-            if not this.is_valid:
-                raise TopologicalError(
-                    "The operation '%s' produced a null geometry. Likely cause is invalidity of the geometry %s" % (self.fn.__name__, repr(this)))
-            elif not other.is_valid:
-                raise TopologicalError(
-                    "The operation '%s' produced a null geometry. Likely cause is invalidity of the 'other' geometry %s" % (self.fn.__name__, repr(other)))
-            else:
-                raise TopologicalError(
-                    "This operation produced a null geometry. Reason: unknown")
+            err = TopologicalError(
+                    "This operation could not be performed. Reason: unknown")
+            self._check_topology(err, this, other)
         return product
 
+
 class UnaryTopologicalOp(Delegating):
+
     def __call__(self, this, *args):
         self._validate(this)
         return self.fn(this._geom, *args)
-
diff --git a/tests/test_affinity.py b/tests/test_affinity.py
index f997e0a..94538c5 100755
--- a/tests/test_affinity.py
+++ b/tests/test_affinity.py
@@ -66,11 +66,9 @@ class AffineTestCase(unittest.TestCase):
         test_geom(load_wkt(
             'MULTIPOLYGON(((900 4300, -1100 -400, 900 -800, 900 4300)), '
             '((1200 4300, 2300 4400, 1900 1000, 1200 4300)))'))
-        # GeometryCollection fails, since it does not have a good constructor
-        gc = load_wkt('GEOMETRYCOLLECTION(POINT(20 70),'
+        test_geom(load_wkt('GEOMETRYCOLLECTION(POINT(20 70),'
                       ' POLYGON((60 70, 13 35, 60 -30, 60 70)),'
-                      ' LINESTRING(60 70, 50 100, 80 100))')
-        self.assertRaises(TypeError, test_geom, gc)  # TODO: fix this
+                      ' LINESTRING(60 70, 50 100, 80 100))'))
 
     def test_affine_2d(self):
         g = load_wkt('LINESTRING(2.4 4.1, 2.4 3, 3 3)')
diff --git a/tests/test_coords.py b/tests/test_coords.py
new file mode 100644
index 0000000..d4c15a8
--- /dev/null
+++ b/tests/test_coords.py
@@ -0,0 +1,40 @@
+from . import unittest, numpy
+from shapely import geometry
+
+
+class CoordsTestCase(unittest.TestCase):
+    """
+    Shapely assumes contiguous C-order float64 data for internal ops.
+    Data should be converted to contiguous float64 if numpy exists.
+    c9a0707 broke this a little bit.
+    """
+
+    @unittest.skipIf(not numpy, 'Numpy required')
+    def test_data_promotion(self):
+        coords = numpy.array([[ 12, 34 ], [ 56, 78 ]], dtype=numpy.float32)
+        processed_coords = numpy.array(
+            geometry.LineString(coords).coords
+        )
+
+        self.assertEqual(
+            coords.tolist(),
+            processed_coords.tolist()
+        )
+
+    @unittest.skipIf(not numpy, 'Numpy required')
+    def test_data_destriding(self):
+        coords = numpy.array([[ 12, 34 ], [ 56, 78 ]], dtype=numpy.float32)
+
+        # Easy way to introduce striding: reverse list order
+        processed_coords = numpy.array(
+            geometry.LineString(coords[::-1]).coords
+        )
+
+        self.assertEqual(
+            coords[::-1].tolist(),
+            processed_coords.tolist()
+        )
+
+
+def test_suite():
+    return unittest.TestLoader().loadTestsFromTestCase(CoordsTestCase)
diff --git a/tests/test_geos_err_handler.py b/tests/test_geos_err_handler.py
new file mode 100644
index 0000000..1ef79ae
--- /dev/null
+++ b/tests/test_geos_err_handler.py
@@ -0,0 +1,33 @@
+import logging
+
+import pytest
+
+from shapely.geometry import LineString
+from shapely.geos import ReadingError
+from shapely.wkt import loads
+
+
+def test_error_handler(tmpdir):
+    logger = logging.getLogger('shapely.geos')
+    logger.setLevel(logging.DEBUG)
+
+    logfile = str(tmpdir.join('test_error.log'))
+    fh = logging.FileHandler(logfile)
+    logger.addHandler(fh)
+
+    # This operation calls error_handler with a format string that
+    # has *no* conversion specifiers.
+    LineString([(0, 0), (2, 2)]).project(LineString([(1, 1), (1.5, 1.5)]))
+    
+    # This calls error_handler with a format string of "%s" and one
+    # value.
+    with pytest.raises(ReadingError):
+        loads('POINT (LOLWUT)')
+
+    g = loads('MULTIPOLYGON (((10 20, 10 120, 60 70, 30 70, 30 40, 60 40, 60 70, 90 20, 10 20)))')
+    assert g.is_valid == False
+
+    log = open(logfile).read()
+    assert "third argument of GEOSProject_r must be Point*" in log
+    assert "Expected number but encountered word: 'LOLWUT'" in log
+    assert "Ring Self-intersection at or near point 60 70" in log
diff --git a/tests/test_hash.py b/tests/test_hash.py
new file mode 100644
index 0000000..97f5919
--- /dev/null
+++ b/tests/test_hash.py
@@ -0,0 +1,21 @@
+from shapely.geometry import Point, MultiPoint, Polygon, GeometryCollection
+
+
+def test_point():
+    g = Point(0, 0)
+    assert hash(g)
+
+
+def test_multipoint():
+    g = MultiPoint([(0, 0)])
+    assert hash(g)
+
+
+def test_polygon():
+    g = Point(0, 0).buffer(1.0)
+    assert hash(g)
+
+
+def test_collection():
+    g = GeometryCollection([Point(0, 0)])
+    assert hash(g)
diff --git a/tests/test_iterops.py b/tests/test_iterops.py
index fd73ccd..887e119 100644
--- a/tests/test_iterops.py
+++ b/tests/test_iterops.py
@@ -3,6 +3,7 @@
 from . import unittest
 from shapely import iterops
 from shapely.geometry import Point, Polygon
+from shapely.geos import TopologicalError
 
 
 class IterOpsTestCase(unittest.TestCase):
@@ -43,5 +44,30 @@ class IterOpsTestCase(unittest.TestCase):
             [[(0.5, 0.5)]])
 
 
+    def test_err(self):
+        # bowtie polygon.
+        coords = ((0.0, 0.0), (0.0, 1.0), (1.0, 0.0), (1.0, 1.0), (0.0, 0.0))
+        polygon = Polygon(coords)
+        self.assertFalse(polygon.is_valid)
+        points = [Point(0.5, 0.5).buffer(2.0), Point(2.0, 2.0).buffer(3.0)]
+        # List of the points contained by the polygon
+        self.assertTrue(
+            all([isinstance(x, Polygon)
+                 for x in iterops.intersects(polygon, points, True)]))
+
+    def test_topological_error(self):
+        p1 = [(339, 346), (459, 346), (399, 311), (340, 277), (399, 173),
+              (280, 242), (339, 415), (280, 381), (460, 207), (339, 346)]
+        polygon1 = Polygon(p1)
+
+        p2 = [(339, 207), (280, 311), (460, 138), (399, 242), (459, 277),
+              (459, 415), (399, 381), (519, 311), (520, 242), (519, 173),
+              (399, 450), (339, 207)]
+        polygon2 = Polygon(p2)
+
+        with self.assertRaises(TopologicalError):
+            list(iterops.within(polygon1, [polygon2]))
+
+
 def test_suite():
     return unittest.TestLoader().loadTestsFromTestCase(IterOpsTestCase)
diff --git a/tests/test_operators.py b/tests/test_operators.py
index 407a7c0..7aa8262 100644
--- a/tests/test_operators.py
+++ b/tests/test_operators.py
@@ -1,5 +1,5 @@
 from . import unittest
-from shapely.geometry import Point
+from shapely.geometry import Point, MultiPoint, Polygon, LineString
 
 
 class OperatorsTestCase(unittest.TestCase):
@@ -12,6 +12,53 @@ class OperatorsTestCase(unittest.TestCase):
         self.assertTrue(point.equals(point - point2))
         self.assertTrue(
             point.symmetric_difference(point2).equals(point ^ point2))
+        self.assertNotEqual(point, point2)
+        point_dupe = Point(0, 0)
+        self.assertEqual(point, point_dupe)
+
+    def test_multipoint(self):
+        mp1 = MultiPoint(((0, 0), (1, 1)))
+        mp1_dup = MultiPoint(((0, 0), (1, 1)))
+        mp1_rev = MultiPoint(((1, 1), (0, 0)))
+        mp2 = MultiPoint(((0, 0), (1, 1), (2, 2)))
+        mp3 = MultiPoint(((0, 0), (1, 1), (2, 3)))
+
+        self.assertEqual(mp1, mp1_dup)
+        self.assertNotEqual(mp1, mp1_rev)  # is this correct?
+        self.assertNotEqual(mp1, mp2)
+        self.assertNotEqual(mp2, mp3)
+
+        p = Point(0, 0)
+        mp = MultiPoint([(0, 0)])
+        self.assertNotEqual(p, mp)
+        self.assertNotEqual(mp, p)
+
+    def test_polygon(self):
+        shell = ((0, 0), (3, 0), (3, 3), (0, 3))
+        hole = ((1, 1), (2, 1), (2, 2), (1, 2))
+        p_solid = Polygon(shell)
+        p2_solid = Polygon(shell)
+        p_hole = Polygon(shell, holes=[hole])
+        p2_hole = Polygon(shell, holes=[hole])
+
+        self.assertEqual(p_solid, p2_solid)
+        self.assertEqual(p_hole, p2_hole)
+        self.assertNotEqual(p_solid, p_hole)
+
+        shell2 = ((-5, 2), (10.5, 3), (7, 3))
+        p3_hole = Polygon(shell2, holes=[hole])
+        self.assertNotEqual(p_hole, p3_hole)
+
+    def test_linestring(self):
+        line1 = LineString([(0,0), (1,1), (2,2)])
+        line2 = LineString([(0,0), (2,2)])
+        line2_dup = LineString([(0,0), (2,2)])
+        # .equals() indicates these are the same
+        self.assertTrue(line1.equals(line2))
+        # but == indicates these are different
+        self.assertNotEqual(line1, line2)
+        # but dupes are the same with ==
+        self.assertEqual(line2, line2_dup)
 
 
 def test_suite():
diff --git a/tests/test_predicates.py b/tests/test_predicates.py
index e90a941..3062eac 100644
--- a/tests/test_predicates.py
+++ b/tests/test_predicates.py
@@ -1,7 +1,8 @@
 """Test GEOS predicates
 """
 from . import unittest
-from shapely.geometry import Point
+from shapely.geometry import Point, Polygon
+from shapely.geos import TopologicalError
 
 
 class PredicatesTestCase(unittest.TestCase):
@@ -18,6 +19,8 @@ class PredicatesTestCase(unittest.TestCase):
         self.assertFalse(point.equals(Point(-1.0, -1.0)))
         self.assertFalse(point.touches(Point(-1.0, -1.0)))
         self.assertTrue(point.equals(Point(0.0, 0.0)))
+        self.assertTrue(point.covers(Point(0.0, 0.0)))
+        self.assertFalse(point.covers(Point(-1.0, -1.0)))
 
     def test_unary_predicates(self):
 
@@ -29,6 +32,15 @@ class PredicatesTestCase(unittest.TestCase):
         self.assertFalse(point.is_ring)
         self.assertFalse(point.has_z)
 
+    def test_binary_predicate_exceptions(self):
+
+        p1 = [(339, 346), (459,346), (399,311), (340, 277), (399, 173),
+              (280, 242), (339, 415), (280, 381), (460, 207), (339, 346)]
+        p2 = [(339, 207), (280, 311), (460, 138), (399, 242), (459, 277),
+              (459, 415), (399, 381), (519, 311), (520, 242), (519, 173),
+              (399, 450), (339, 207)]
+        self.assertRaises(TopologicalError, Polygon(p1).within, Polygon(p2))
+
 
 def test_suite():
     return unittest.TestLoader().loadTestsFromTestCase(PredicatesTestCase)
diff --git a/tests/test_prepared.py b/tests/test_prepared.py
index fb3d4ed..4f43dc1 100644
--- a/tests/test_prepared.py
+++ b/tests/test_prepared.py
@@ -25,6 +25,26 @@ class PreparedGeometryTestCase(unittest.TestCase):
         p = prepared.PreparedGeometry(geometry.Point(0.0, 0.0).buffer(1.0))
         self.assertRaises(ValueError, geometry.Point(0.0, 0.0).contains, p)
 
+    @unittest.skipIf(geos_version < (3, 1, 0), 'GEOS 3.1.0 required')
+    def test_prepared_predicates(self):
+        # check prepared predicates give the same result as regular predicates
+        polygon1 = geometry.Polygon([
+            (0, 0), (0, 1), (1, 1), (1, 0), (0, 0)
+        ])
+        polygon2 = geometry.Polygon([
+            (0.5, 0.5), (1.5, 0.5), (1.0, 1.0), (0.5, 0.5)
+        ])
+        point2 = geometry.Point(0.5, 0.5)
+        polygon_empty = geometry.Polygon()
+        prepared_polygon1 = prepared.PreparedGeometry(polygon1)
+        for geom2 in (polygon2, point2, polygon_empty):
+            self.assertTrue(polygon1.disjoint(geom2) == prepared_polygon1.disjoint(geom2))
+            self.assertTrue(polygon1.touches(geom2) == prepared_polygon1.touches(geom2))
+            self.assertTrue(polygon1.intersects(geom2) == prepared_polygon1.intersects(geom2))
+            self.assertTrue(polygon1.crosses(geom2) == prepared_polygon1.crosses(geom2))
+            self.assertTrue(polygon1.within(geom2) == prepared_polygon1.within(geom2))
+            self.assertTrue(polygon1.contains(geom2) == prepared_polygon1.contains(geom2))
+            self.assertTrue(polygon1.overlaps(geom2) == prepared_polygon1.overlaps(geom2))
 
 def test_suite():
     loader = unittest.TestLoader()
diff --git a/tests/test_snap.py b/tests/test_snap.py
new file mode 100644
index 0000000..7f0a170
--- /dev/null
+++ b/tests/test_snap.py
@@ -0,0 +1,32 @@
+from . import unittest
+
+from shapely.geometry import LineString, Polygon
+from shapely.geos import geos_version
+from shapely.ops import snap
+
+ at unittest.skipIf(geos_version < (3, 3, 0), 'GEOS 3.3.0 required')
+class Snap(unittest.TestCase):
+    def test_snap(self):
+        
+        # input geometries
+        square = Polygon([(1,1), (2, 1), (2, 2), (1, 2), (1, 1)])
+        line = LineString([(0,0), (0.8, 0.8), (1.8, 0.95), (2.6, 0.5)])
+                
+        square_coords = square.exterior.coords[:]
+        line_coords = line.coords[:]
+
+        result = snap(line, square, 0.5)
+
+        # test result is correct
+        self.assertTrue(isinstance(result, LineString))
+        self.assertEqual(result.coords[:], [(0.0, 0.0), (1.0, 1.0), (2.0, 1.0), (2.6, 0.5)])
+        
+        # test inputs have not been modified
+        self.assertEqual(square.exterior.coords[:], square_coords)
+        self.assertEqual(line.coords[:], line_coords)
+
+def test_suite():
+    return unittest.TestLoader().loadTestsFromTestCase(Snap)
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/tests/test_svg.py b/tests/test_svg.py
new file mode 100644
index 0000000..5c8feba
--- /dev/null
+++ b/tests/test_svg.py
@@ -0,0 +1,192 @@
+# Tests SVG output and validity
+import os
+from xml.dom.minidom import parseString as parse_xml_string
+
+from . import unittest
+from shapely.geometry import Point, MultiPoint, LineString, MultiLineString,\
+    Polygon, MultiPolygon
+from shapely.geometry.collection import GeometryCollection
+
+
+class SvgTestCase(unittest.TestCase):
+
+    def assertSVG(self, geom, expected, **kwrds):
+        """Helper function to check XML and debug SVG"""
+        svg_elem = geom.svg(**kwrds)
+        try:
+            parse_xml_string(svg_elem)
+        except:
+            raise AssertionError(
+                'XML is not valid for SVG element: ' + str(svg_elem))
+        svg_doc = geom._repr_svg_()
+        try:
+            doc = parse_xml_string(svg_doc)
+        except:
+            raise AssertionError(
+                'XML is not valid for SVG doucment: ' + str(svg_doc))
+        svg_output_dir = None
+        # svg_output_dir = '.'  # useful for debugging SVG files
+        if svg_output_dir:
+            fname = geom.type
+            if geom.is_empty:
+                fname += '_empty'
+            if not geom.is_valid:
+                fname += '_invalid'
+            if kwrds:
+                fname += '_' + \
+                    ','.join(str(k) + '=' + str(kwrds[k]) for k in kwrds)
+            svg_path = os.path.join(svg_output_dir, fname + '.svg')
+            with open(svg_path, 'w') as fp:
+                fp.write(doc.toprettyxml())
+        self.assertEqual(svg_elem, expected)
+
+    def test_point(self):
+        # Empty
+        self.assertSVG(Point(), '<g />')
+        # Valid
+        g = Point(6, 7)
+        self.assertSVG(
+            g,
+            '<circle cx="6.0" cy="7.0" r="3.0" stroke="#555555" '
+            'stroke-width="1.0" fill="#66cc99" opacity="0.6" />')
+        self.assertSVG(
+            g,
+            '<circle cx="6.0" cy="7.0" r="15.0" stroke="#555555" '
+            'stroke-width="5.0" fill="#66cc99" opacity="0.6" />',
+            scale_factor=5)
+
+    def test_multipoint(self):
+        # Empty
+        self.assertSVG(MultiPoint(), '<g />')
+        # Valid
+        g = MultiPoint([(6, 7), (3, 4)])
+        self.assertSVG(
+            g,
+            '<g><circle cx="6.0" cy="7.0" r="3.0" stroke="#555555" '
+            'stroke-width="1.0" fill="#66cc99" opacity="0.6" />'
+            '<circle cx="3.0" cy="4.0" r="3.0" stroke="#555555" '
+            'stroke-width="1.0" fill="#66cc99" opacity="0.6" /></g>')
+        self.assertSVG(
+            g,
+            '<g><circle cx="6.0" cy="7.0" r="15.0" stroke="#555555" '
+            'stroke-width="5.0" fill="#66cc99" opacity="0.6" />'
+            '<circle cx="3.0" cy="4.0" r="15.0" stroke="#555555" '
+            'stroke-width="5.0" fill="#66cc99" opacity="0.6" /></g>',
+            scale_factor=5)
+
+    def test_linestring(self):
+        # Empty
+        self.assertSVG(LineString(), '<g />')
+        # Valid
+        g = LineString([(5, 8), (496, -6), (530, 20)])
+        self.assertSVG(
+            g,
+            '<polyline fill="none" stroke="#66cc99" stroke-width="2.0" '
+            'points="5.0,8.0 496.0,-6.0 530.0,20.0" opacity="0.8" />')
+        self.assertSVG(
+            g,
+            '<polyline fill="none" stroke="#66cc99" stroke-width="10.0" '
+            'points="5.0,8.0 496.0,-6.0 530.0,20.0" opacity="0.8" />',
+            scale_factor=5)
+        # Invalid
+        self.assertSVG(
+            LineString([(0, 0), (0, 0)]),
+            '<polyline fill="none" stroke="#ff3333" stroke-width="2.0" '
+            'points="0.0,0.0 0.0,0.0" opacity="0.8" />')
+
+    def test_multilinestring(self):
+        # Empty
+        self.assertSVG(MultiLineString(), '<g />')
+        # Valid
+        self.assertSVG(
+            MultiLineString([[(6, 7), (3, 4)], [(2, 8), (9, 1)]]),
+            '<g><polyline fill="none" stroke="#66cc99" stroke-width="2.0" '
+            'points="6.0,7.0 3.0,4.0" opacity="0.8" />'
+            '<polyline fill="none" stroke="#66cc99" stroke-width="2.0" '
+            'points="2.0,8.0 9.0,1.0" opacity="0.8" /></g>')
+        # Invalid
+        self.assertSVG(
+            MultiLineString([[(2, 3), (2, 3)], [(2, 8), (9, 1)]]),
+            '<g><polyline fill="none" stroke="#ff3333" stroke-width="2.0" '
+            'points="2.0,3.0 2.0,3.0" opacity="0.8" />'
+            '<polyline fill="none" stroke="#ff3333" stroke-width="2.0" '
+            'points="2.0,8.0 9.0,1.0" opacity="0.8" /></g>')
+
+    def test_polygon(self):
+        # Empty
+        self.assertSVG(Polygon(), '<g />')
+        # Valid
+        g = Polygon([(35, 10), (45, 45), (15, 40), (10, 20), (35, 10)],
+                    [[(20, 30), (35, 35), (30, 20), (20, 30)]])
+        self.assertSVG(
+            g,
+            '<path fill-rule="evenodd" fill="#66cc99" stroke="#555555" '
+            'stroke-width="2.0" opacity="0.6" d="M 35.0,10.0 L 45.0,45.0 L '
+            '15.0,40.0 L 10.0,20.0 L 35.0,10.0 z M 20.0,30.0 L 35.0,35.0 L '
+            '30.0,20.0 L 20.0,30.0 z" />')
+        self.assertSVG(
+            g,
+            '<path fill-rule="evenodd" fill="#66cc99" stroke="#555555" '
+            'stroke-width="10.0" opacity="0.6" d="M 35.0,10.0 L 45.0,45.0 L '
+            '15.0,40.0 L 10.0,20.0 L 35.0,10.0 z M 20.0,30.0 L 35.0,35.0 L '
+            '30.0,20.0 L 20.0,30.0 z" />',
+            scale_factor=5)
+        # Invalid
+        self.assertSVG(
+            Polygon([(0, 40), (0, 0), (40, 40), (40, 0), (0, 40)]),
+            '<path fill-rule="evenodd" fill="#ff3333" stroke="#555555" '
+            'stroke-width="2.0" opacity="0.6" d="M 0.0,40.0 L 0.0,0.0 L '
+            '40.0,40.0 L 40.0,0.0 L 0.0,40.0 z" />')
+
+    def test_multipolygon(self):
+        # Empty
+        self.assertSVG(MultiPolygon(), '<g />')
+        # Valid
+        self.assertSVG(
+            MultiPolygon([
+                Polygon([(40, 40), (20, 45), (45, 30), (40, 40)]),
+                Polygon([(20, 35), (10, 30), (10, 10), (30, 5), (45, 20),
+                         (20, 35)],
+                        [[(30, 20), (20, 15), (20, 25), (30, 20)]])
+                ]),
+            '<g><path fill-rule="evenodd" fill="#66cc99" stroke="#555555" '
+            'stroke-width="2.0" opacity="0.6" d="M 40.0,40.0 L 20.0,45.0 L '
+            '45.0,30.0 L 40.0,40.0 z" />'
+            '<path fill-rule="evenodd" fill="#66cc99" stroke="#555555" '
+            'stroke-width="2.0" opacity="0.6" d="M 20.0,35.0 L 10.0,30.0 L '
+            '10.0,10.0 L 30.0,5.0 L 45.0,20.0 L 20.0,35.0 z M 30.0,20.0 L '
+            '20.0,15.0 L 20.0,25.0 L 30.0,20.0 z" /></g>')
+        # Invalid
+        self.assertSVG(
+            MultiPolygon([
+                Polygon([(140, 140), (120, 145), (145, 130), (140, 140)]),
+                Polygon([(0, 40), (0, 0), (40, 40), (40, 0), (0, 40)])
+            ]),
+            '<g><path fill-rule="evenodd" fill="#ff3333" stroke="#555555" '
+            'stroke-width="2.0" opacity="0.6" d="M 140.0,140.0 L '
+            '120.0,145.0 L 145.0,130.0 L 140.0,140.0 z" />'
+            '<path fill-rule="evenodd" fill="#ff3333" stroke="#555555" '
+            'stroke-width="2.0" opacity="0.6" d="M 0.0,40.0 L 0.0,0.0 L '
+            '40.0,40.0 L 40.0,0.0 L 0.0,40.0 z" /></g>')
+
+    def test_collection(self):
+        # Empty
+        self.assertSVG(GeometryCollection(), '<g />')
+        # Valid
+        self.assertSVG(
+            Point(7, 3).union(LineString([(4, 2), (8, 4)])),
+            '<g><circle cx="7.0" cy="3.0" r="3.0" stroke="#555555" '
+            'stroke-width="1.0" fill="#66cc99" opacity="0.6" />'
+            '<polyline fill="none" stroke="#66cc99" stroke-width="2.0" '
+            'points="4.0,2.0 8.0,4.0" opacity="0.8" /></g>')
+        # Invalid
+        self.assertSVG(
+            Point(7, 3).union(LineString([(4, 2), (4, 2)])),
+            '<g><circle cx="7.0" cy="3.0" r="3.0" stroke="#555555" '
+            'stroke-width="1.0" fill="#ff3333" opacity="0.6" />'
+            '<polyline fill="none" stroke="#ff3333" stroke-width="2.0" '
+            'points="4.0,2.0 4.0,2.0" opacity="0.8" /></g>')
+
+
+def test_suite():
+    return unittest.TestLoader().loadTestsFromTestCase(SvgTestCase)

-- 
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/pkg-grass/python-shapely.git



More information about the Pkg-grass-devel mailing list