[python-shapely] 128/148: Imported Upstream version 1.5.9
Sebastiaan Couwenberg
sebastic at moszumanska.debian.org
Thu Aug 20 17:42:11 UTC 2015
This is an automated email from the git hooks/post-receive script.
sebastic pushed a commit to branch master
in repository python-shapely.
commit 12abd9e351dff2366962db8c9acc18dd4403e6ca
Author: Pietro Battiston <me at pietrobattiston.it>
Date: Mon Jul 27 12:21:09 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