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.vandew...@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 + +@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 _______________________________________________ Pkg-grass-devel mailing list Pkg-grass-devel@lists.alioth.debian.org http://lists.alioth.debian.org/cgi-bin/mailman/listinfo/pkg-grass-devel