This is an automated email from the git hooks/post-receive script. sebastic pushed a commit to branch master in repository python-shapely.
commit 7369eeff99cc91f59fa34c04d4159298906a2fab Author: Bas Couwenberg <sebas...@xs4all.nl> Date: Fri Aug 28 14:51:41 2015 +0200 Imported Upstream version 1.5.10 --- .gitignore | 1 + CHANGES.txt | 5 + README.rst | 7 +- docs/manual.rst | 42 +++++++- requirements-dev.txt | 1 + setup.py | 190 +++++++++++++++++++++++---------- shapely/__init__.py | 2 +- shapely/ctypes_declarations.py | 22 +++- shapely/geometry/base.py | 29 +++-- shapely/geometry/linestring.py | 15 +-- shapely/geometry/multipoint.py | 2 +- shapely/geometry/polygon.py | 4 +- shapely/geos.py | 150 ++++++-------------------- shapely/impl.py | 31 +++++- shapely/libgeos.py | 236 +++++++++++++++++++++++++++++++++++++++++ shapely/linref.py | 4 +- shapely/speedups/__init__.py | 2 +- tests/__init__.py | 9 +- tests/test_default_impl.py | 24 +++++ tests/test_dlls.py | 2 +- tests/test_multi.py | 8 ++ tests/test_multilinestring.py | 10 +- tests/test_multipoint.py | 9 +- tests/test_multipolygon.py | 10 +- tests/test_operations.py | 11 +- tests/test_parallel_offset.py | 33 ++++++ tests/test_predicates.py | 23 +++- 27 files changed, 663 insertions(+), 219 deletions(-) diff --git a/.gitignore b/.gitignore index 7662bd5..76a880b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ *.pyc +*.pyo *.c *.so diff --git a/CHANGES.txt b/CHANGES.txt index 93f3772..b66b6ac 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,6 +1,11 @@ Changes ======= +1.5.10 (2015-08-22) +------------------- +- Monkey patch affinity module by absolute reference (#299). +- Raise TopologicalError in relate() instead of crashing (#294, #295, #303). + 1.5.9 (2015-05-27) ------------------ - Fix for 64 bit speedups compatibility (#274). diff --git a/README.rst b/README.rst index ea892c0..f80bba3 100644 --- a/README.rst +++ b/README.rst @@ -44,12 +44,13 @@ system library path, and install Shapely from the Python package index. $ 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. +If you've installed GEOS to a non-standard location, the geos-config program +will be used to get compiler and linker options. If it is not on the PATH, +it can be specified with a GEOS_CONFIG environment variable, e.g.: .. code-block:: console - $ CFLAGS=`geos-config --cflags` LDFLAGS=`geos-config --clibs` pip install shapely + $ GEOS_CONFIG=/path/to/geos-config 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. diff --git a/docs/manual.rst b/docs/manual.rst index 90b0a3d..07971b8 100644 --- a/docs/manual.rst +++ b/docs/manual.rst @@ -1054,8 +1054,9 @@ differently. .. method:: object.contains(other) - Returns ``True`` if the object's `interior` contains the `boundary` and - `interior` of the other object and their boundaries do not touch at all. + Returns ``True`` if no points of `other` lie in the exterior of the `object` + and at least one point of the interior of `other` lies in the interior of + `object`. This predicate applies to all types, and is inverse to :meth:`within`. The expression ``a.contains(b) == b.within(a)`` always evaluates to ``True``. @@ -1124,8 +1125,8 @@ This predicate applies to all types and is the inverse of :meth:`intersects`. Returns ``True`` if the `boundary` and `interior` of the object intersect in any way with those of the other. -This predicate is equivalent to the OR-ing of :meth:`contains`, :meth:`crosses`, -:meth:`equals`, :meth:`touches`, and :meth:`within`. +In other words, geometric objects intersect if they have any boundary or +interior point in common. .. method:: object.touches(other) @@ -1234,6 +1235,37 @@ elements. >>> Point(0, 0).relate(LineString([(0, 0), (1, 1)])) 'F0FFFF102' +.. method:: object.relate_pattern(other, pattern) + + Returns True if the DE-9IM string code for the relationship between the + geometries satisfies the pattern, otherwise False. + +The :meth:`relate_pattern` compares the DE-9IM code string for two geometries +against a specified pattern. If the string matches the pattern then ``True`` is +returned, otherwise ``False``. The pattern specified can be an exact match +(``0``, ``1`` or ``2``), a boolean match (``T`` or ``F``), or a wildcard +(``*``). For example, the pattern for the `within` predicate is ``T*****FF*``. + +.. code-block:: pycon + + >> point = Point(0.5, 0.5) + >> square = Polygon([(0, 0), (0, 1), (1, 1), (1, 0)]) + >> square.relate_pattern(point, 'T*****FF*') + True + >> point.within(square) + True + +Note that the order or the geometries is significant, as demonstrated below. +In this example the square contains the point, but the point does not contain +the square. + +.. code-block:: pycon + + >>> point.relate(square) + '0FFFFF212' + >>> square.relate(point) + '0F2FF1FF2' + Further discussion of the DE-9IM matrix is beyond the scope of this manual. See [4]_ and http://pypi.python.org/pypi/de9im. @@ -2037,7 +2069,7 @@ one geometry to the vertices in a second geometry with a given tolerance. The `tolerance` argument specifies the minimum distance between vertices for them to be snapped. - `New in version 1.4.5` + `New in version 1.5.0` .. code-block:: pycon diff --git a/requirements-dev.txt b/requirements-dev.txt index b16d71b..04f77c6 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -2,4 +2,5 @@ setuptools Numpy>=1.8.0 Cython>=0.19 descartes==1.0.1 +packaging pytest diff --git a/setup.py b/setup.py index 9748470..b2b7bd5 100755 --- a/setup.py +++ b/setup.py @@ -1,7 +1,24 @@ #!/usr/bin/env python -from __future__ import print_function +# Two environment variables influence this script. +# +# GEOS_LIBRARY_PATH: a path to a GEOS C shared library. +# +# GEOS_CONFIG: the path to a geos-config program that points to GEOS version, +# headers, and libraries. +# +# NB: within this setup scripts, software versions are evaluated according +# to https://www.python.org/dev/peps/pep-0440/. +import errno +import glob +import logging +import os +import platform +import re +import shutil +import subprocess +import sys try: # If possible, use setuptools from setuptools import setup @@ -11,36 +28,54 @@ except ImportError: from distutils.core import setup from distutils.extension import Extension from distutils.command.build_ext import build_ext as distutils_build_ext -from distutils.cmd import Command from distutils.errors import CCompilerError, DistutilsExecError, \ DistutilsPlatformError -from distutils.sysconfig import get_config_var -import errno -import glob -import os -import platform -import shutil -import subprocess -import sys +from packaging.version import Version + +# Get geos_version from GEOS dynamic library, which depends on +# GEOS_LIBRARY_PATH and/or GEOS_CONFIG environment variables +from shapely.libgeos import geos_version_string, geos_version, \ + geos_config, get_geos_config + +logging.basicConfig() +log = logging.getLogger(__file__) + +# python -W all setup.py ... +if 'all' in sys.warnoptions: + log.level = logging.DEBUG # Get the version from the shapely module -version = None +shapely_version = None with open('shapely/__init__.py', 'r') as fp: for line in fp: - if "__version__" in line: - exec(line.replace('_', '')) + if line.startswith("__version__"): + shapely_version = Version( + line.split("=")[1].strip().strip("\"'")) break -if version is None: + +if not shapely_version: raise ValueError("Could not determine Shapely's version") +# Fail installation if the GEOS shared library does not meet the minimum +# version. We ship it with Shapely for Windows, so no need to check on +# that platform. +log.debug('GEOS shared library: %s %s', geos_version_string, geos_version) +if (set(sys.argv).intersection(['install', 'build', 'build_ext']) and + shapely_version >= Version('1.3') and + geos_version < (3, 3)): + log.critical( + "Shapely >= 1.3 requires GEOS >= 3.3. " + "Install GEOS 3.3+ and reinstall Shapely.") + sys.exit(1) + # Handle UTF-8 encoding of certain text files. open_kwds = {} if sys.version_info >= (3,): open_kwds['encoding'] = 'utf-8' with open('VERSION.txt', 'w', **open_kwds) as fp: - fp.write(version) + fp.write(str(shapely_version)) with open('README.rst', 'r', **open_kwds) as fp: readme = fp.read() @@ -55,8 +90,8 @@ long_description = readme + '\n\n' + credits + '\n\n' + changes setup_args = dict( name = 'Shapely', - version = version, - requires = ['Python (>=2.6)', 'libgeos_c (>=3.1)'], + version = str(shapely_version), + requires = ['Python (>=2.6)', 'libgeos_c (>=3.3)'], description = 'Geometric objects, predicates, and operations', license = 'BSD', keywords = 'geometry topology gis', @@ -74,7 +109,6 @@ setup_args = dict( 'shapely.speedups', 'shapely.vectorized', ], - cmdclass = {}, classifiers = [ 'Development Status :: 5 - Production/Stable', 'Intended Audience :: Developers', @@ -86,7 +120,8 @@ setup_args = dict( 'Programming Language :: Python :: 3', 'Topic :: Scientific/Engineering :: GIS', ], - data_files = [('shapely', ['shapely/_geos.pxi'])] + data_files = [('shapely', ['shapely/_geos.pxi'])], + cmdclass = {}, ) # Add DLLs for Windows @@ -111,6 +146,47 @@ if sys.platform == 'win32': ) +# Prepare build opts and args for the speedups extension module. +include_dirs = [] +library_dirs = [] +libraries = [] +extra_link_args = [] + +try: + # Get the version from geos-config. Show error if this version tuple is + # different to the GEOS version loaded from the dynamic library. + geos_config_version_string = get_geos_config('--version') + res = re.findall(r'(\d+)\.(\d+)\.(\d+)', geos_config_version_string) + geos_config_version = tuple(int(x) for x in res[0]) + + if geos_config_version != geos_version: + log.error("The GEOS dynamic library version is %s %s,", + geos_version_string, geos_version) + log.error("but the version reported by %s is %s %s.", geos_config, + geos_config_version_string, geos_config_version) + sys.exit(1) +except OSError as ex: + log.error(ex) + log.error('Cannot find geos-config to get headers and check version.') + log.error('If available, specify a path to geos-config with a ' + 'GEOS_CONFIG environment variable') + geos_config = None + +if geos_config: + # Collect other options from GEOS + for item in get_geos_config('--cflags').split(): + if item.startswith("-I"): + include_dirs.extend(item[2:].split(":")) + for item in get_geos_config('--clibs').split(): + if item.startswith("-L"): + library_dirs.extend(item[2:].split(":")) + elif item.startswith("-l"): + libraries.append(item[2:]) + else: + # e.g. -framework GEOS + extra_link_args.append(item) + + # Optional compilation of speedups # setuptools stuff from Bob Ippolito's simplejson project if sys.platform == 'win32' and sys.version_info > (2, 6): @@ -149,10 +225,6 @@ if (hasattr(platform, 'python_implementation') # python_implementation is only available since 2.6 ext_modules = [] libraries = [] -elif sys.platform == 'win32': - libraries = ['geos'] -else: - libraries = ['geos_c'] if os.path.exists("MANIFEST.in"): @@ -166,67 +238,77 @@ if os.path.exists("MANIFEST.in"): try: if (force_cython or not os.path.exists(c_file) or os.path.getmtime(pyx_file) > os.path.getmtime(c_file)): - print("Updating C extension with Cython.", file=sys.stderr) + log.info("Updating C extension with Cython.") subprocess.check_call(["cython", "shapely/speedups/_speedups.pyx"]) except (subprocess.CalledProcessError, OSError): - print("Warning: Could not (re)create C extension with Cython.", - file=sys.stderr) + log.warn("Could not (re)create C extension with Cython.") if force_cython: raise - if not os.path.exists("shapely/speedups/_speedups.c"): - print("Warning: speedup extension not found", file=sys.stderr) + if not os.path.exists(c_file): + log.warn("speedup extension not found") ext_modules = [ Extension( "shapely.speedups._speedups", ["shapely/speedups/_speedups.c"], + include_dirs=include_dirs, + library_dirs=library_dirs, libraries=libraries, - include_dirs=[get_config_var('INCLUDEDIR')],), + extra_link_args=extra_link_args, + ), ] cmd_classes = setup_args.setdefault('cmdclass', {}) try: - import numpy as np + import numpy from Cython.Distutils import build_ext as cython_build_ext from distutils.extension import Extension as DistutilsExtension - if 'build_ext' in cmd_classes: + if 'build_ext' in setup_args['cmdclass']: raise ValueError('We need to put the Cython build_ext in ' 'cmd_classes, but it is already defined.') - cmd_classes['build_ext'] = cython_build_ext - - ext_modules.append(DistutilsExtension("shapely.vectorized._vectorized", - sources=["shapely/vectorized/_vectorized.pyx"], - libraries=libraries + [np.get_include()], - include_dirs=[get_config_var('INCLUDEDIR'), - np.get_include()], - )) + setup_args['cmdclass']['build_ext'] = cython_build_ext + + include_dirs.append(numpy.get_include()) + libraries.append(numpy.get_include()) + + ext_modules.append(DistutilsExtension( + "shapely.vectorized._vectorized", + sources=["shapely/vectorized/_vectorized.pyx"], + include_dirs=include_dirs, + library_dirs=library_dirs, + libraries=libraries, + extra_link_args=extra_link_args, + )) except ImportError: - print("Numpy or Cython not available, shapely.vectorized submodule not " - "being built.") + log.info("Numpy or Cython not available, shapely.vectorized submodule " + "not being built.") try: # try building with speedups - existing_build_ext = setup_args['cmdclass'].get('build_ext', distutils_build_ext) - setup_args['cmdclass']['build_ext'] = construct_build_ext(existing_build_ext) - setup( - ext_modules=ext_modules, - **setup_args - ) + existing_build_ext = setup_args['cmdclass'].\ + get('build_ext', distutils_build_ext) + setup_args['cmdclass']['build_ext'] = \ + construct_build_ext(existing_build_ext) + setup(ext_modules=ext_modules, **setup_args) except BuildFailed as ex: - BUILD_EXT_WARNING = "Warning: The C extension could not be compiled, " \ + BUILD_EXT_WARNING = "The C extension could not be compiled, " \ "speedups are not enabled." - print(ex) - print(BUILD_EXT_WARNING) - print("Failure information, if any, is above.") - print("I'm retrying the build without the C extension now.") + log.warn(ex) + log.warn(BUILD_EXT_WARNING) + log.warn("Failure information, if any, is above.") + log.warn("I'm retrying the build without the C extension now.") + + # Remove any previously defined build_ext command class. + if 'build_ext' in setup_args['cmdclass']: + del setup_args['cmdclass']['build_ext'] if 'build_ext' in cmd_classes: del cmd_classes['build_ext'] setup(**setup_args) - print(BUILD_EXT_WARNING) - print("Plain-Python installation succeeded.") + log.warn(BUILD_EXT_WARNING) + log.info("Plain-Python installation succeeded.") diff --git a/shapely/__init__.py b/shapely/__init__.py index 424ebc4..63d18fb 100644 --- a/shapely/__init__.py +++ b/shapely/__init__.py @@ -1 +1 @@ -__version__ = "1.5.9" +__version__ = "1.5.10" diff --git a/shapely/ctypes_declarations.py b/shapely/ctypes_declarations.py index 3ee5141..ac38fc5 100644 --- a/shapely/ctypes_declarations.py +++ b/shapely/ctypes_declarations.py @@ -129,8 +129,16 @@ def prototype(lgeos, geos_version): lgeos.GEOSBufferWithStyle.restype = c_void_p lgeos.GEOSBufferWithStyle.argtypes = [c_void_p, c_double, c_int, c_int, c_int, c_double] - lgeos.GEOSSingleSidedBuffer.restype = c_void_p - lgeos.GEOSSingleSidedBuffer.argtypes = [c_void_p, c_double, c_int, c_int, c_double, c_int] + if geos_version >= (3, 3, 0): + + lgeos.GEOSOffsetCurve.restype = c_void_p + lgeos.GEOSOffsetCurve.argtypes = [c_void_p, c_double, c_int, c_int, c_double] + + else: + + # deprecated in GEOS 3.3.0 in favour of GEOSOffsetCurve + lgeos.GEOSSingleSidedBuffer.restype = c_void_p + lgeos.GEOSSingleSidedBuffer.argtypes = [c_void_p, c_double, c_int, c_int, c_double, c_int] ''' Geometry constructors @@ -290,12 +298,16 @@ def prototype(lgeos, geos_version): Dimensionally Extended 9 Intersection Model related ''' - lgeos.GEOSRelatePattern.restype = c_char - lgeos.GEOSRelatePattern.argtypes = [c_void_p, c_void_p, c_char_p] - lgeos.GEOSRelate.restype = allocated_c_char_p lgeos.GEOSRelate.argtypes = [c_void_p, c_void_p] + lgeos.GEOSRelatePattern.restype = c_byte + lgeos.GEOSRelatePattern.argtypes = [c_void_p, c_void_p, c_char_p] + + if geos_version >= (3, 3, 0): + lgeos.GEOSRelatePatternMatch.restype = c_byte + lgeos.GEOSRelatePatternMatch.argtypes = [c_char_p, c_char_p] + ''' Prepared Geometry Binary predicates Return 2 on exception, 1 on true, 0 on false diff --git a/shapely/geometry/base.py b/shapely/geometry/base.py index 69d8405..6498e06 100644 --- a/shapely/geometry/base.py +++ b/shapely/geometry/base.py @@ -14,6 +14,16 @@ from shapely.impl import DefaultImplementation, delegated if sys.version_info[0] < 3: range = xrange + integer_types = (int, long) +else: + integer_types = (int,) + +try: + import numpy as np + integer_types = integer_types + (np.integer,) +except ImportError: + pass + GEOMETRY_TYPES = [ 'Point', @@ -63,7 +73,7 @@ def geom_factory(g, parent=None): [geom_type], ) ob.__class__ = getattr(mod, geom_type) - ob.__geom__ = g + ob._geom = g ob.__p__ = parent if lgeos.methods['has_z'](g): ob._ndim = 3 @@ -188,14 +198,11 @@ class BaseGeometry(object): _ndim = None _crs = None _other_owned = False + _is_empty = True # Backend config impl = DefaultImplementation - @property - def _is_empty(self): - return self.__geom__ in [EMPTY, None] - # a reference to the so/dll proxy to preserve access during clean up _lgeos = lgeos @@ -207,6 +214,7 @@ class BaseGeometry(object): self._lgeos.GEOSGeom_destroy(self.__geom__) except AttributeError: pass # _lgeos might be empty on shutdown + self._is_empty = True self.__geom__ = val def __del__(self): @@ -235,6 +243,7 @@ class BaseGeometry(object): @_geom.setter def _geom(self, val): self.empty() + self._is_empty = val in [EMPTY, None] self.__geom__ = val # Operators @@ -528,7 +537,7 @@ class BaseGeometry(object): @delegated def simplify(self, tolerance, preserve_topology=True): - """Returns a simplified geometry produced by the Douglas-Puecker + """Returns a simplified geometry produced by the Douglas-Peucker algorithm Coordinates of the simplified geometry will be no more than the @@ -663,6 +672,12 @@ class BaseGeometry(object): specified decimal place""" return self.equals_exact(other, 0.5 * 10**(-decimal)) + def relate_pattern(self, other, pattern): + """Returns True if the DE-9IM string code for the relationship between + the geometries satisfies the pattern, else False""" + pattern = c_char_p(pattern.encode('ascii')) + return bool(self.impl['relate_pattern'](self, other, pattern)) + # Linear referencing # ------------------ @@ -828,7 +843,7 @@ class GeometrySequence(object): def __getitem__(self, key): self._update() m = self.__len__() - if isinstance(key, int): + if isinstance(key, integer_types): if key + m < 0 or key >= m: raise IndexError("index out of range") if key < 0: diff --git a/shapely/geometry/linestring.py b/shapely/geometry/linestring.py index 7cb5cf1..7a9fb6b 100644 --- a/shapely/geometry/linestring.py +++ b/shapely/geometry/linestring.py @@ -110,16 +110,18 @@ class LineString(BaseGeometry): return self.coords.xy def parallel_offset( - self, distance, side, + self, distance, side='right', resolution=16, join_style=JOIN_STYLE.round, mitre_limit=5.0): """Returns a LineString or MultiLineString geometry at a distance from the object on its right or its left side. - Distance must be a positive float value. The side parameter may be - 'left' or 'right'. The resolution of the buffer around each vertex of - the object increases by increasing the resolution keyword parameter or - third positional parameter. + The side parameter may be 'left' or 'right' (default is 'right'). The + resolution of the buffer around each vertex of the object increases by + increasing the resolution keyword parameter or third positional + parameter. If the distance parameter is negative the side is inverted, + e.g. distance=5.0, side='left' is the same as distance=-5.0, + side='right'. The join style is for outside corners between line segments. Accepted values are JOIN_STYLE.round (1), JOIN_STYLE.mitre (2), and @@ -136,8 +138,7 @@ class LineString(BaseGeometry): 'Cannot compute offset from zero-length line segment') try: return geom_factory(self.impl['parallel_offset']( - self, distance, resolution, join_style, mitre_limit, - bool(side == 'left'))) + self, distance, resolution, join_style, mitre_limit, side)) except OSError: raise TopologicalError() diff --git a/shapely/geometry/multipoint.py b/shapely/geometry/multipoint.py index 9ea8a6e..d9f8908 100644 --- a/shapely/geometry/multipoint.py +++ b/shapely/geometry/multipoint.py @@ -53,7 +53,7 @@ class MultiPoint(BaseMultipartGeometry): """ super(MultiPoint, self).__init__() - if points is None: + if points is None or len(points) == 0: # allow creation of empty multipoints, to support unpickling pass else: diff --git a/shapely/geometry/polygon.py b/shapely/geometry/polygon.py index 167f4d9..7c7c0cb 100644 --- a/shapely/geometry/polygon.py +++ b/shapely/geometry/polygon.py @@ -176,7 +176,7 @@ class InteriorRingSequence(object): if i not in self.__rings__: g = lgeos.GEOSGetInteriorRingN(self._geom, i) ring = LinearRing() - ring.__geom__ = g + ring._geom = g ring.__p__ = self ring._other_owned = True ring._ndim = self._ndim @@ -235,7 +235,7 @@ class Polygon(BaseGeometry): elif self._exterior is None or self._exterior() is None: g = lgeos.GEOSGetExteriorRing(self._geom) ring = LinearRing() - ring.__geom__ = g + ring._geom = g ring.__p__ = self ring._other_owned = True ring._ndim = self._ndim diff --git a/shapely/geos.py b/shapely/geos.py index a00aa26..c767d72 100644 --- a/shapely/geos.py +++ b/shapely/geos.py @@ -1,8 +1,7 @@ """ -Proxies for the libgeos_c shared lib, GEOS-specific exceptions, and utilities +Proxies for libgeos, GEOS-specific exceptions, and utilities """ -import os import re import sys import atexit @@ -12,8 +11,9 @@ 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 from .ctypes_declarations import prototype, EXCEPTION_HANDLER_FUNCTYPE +from .libgeos import lgeos as _lgeos, geos_version +from . import ftools # Add message handler to this module's logger @@ -31,111 +31,6 @@ else: LOG.addHandler(NullHandler()) -# Find and load the GEOS and C libraries -# If this ever gets any longer, we'll break it into separate modules - -def load_dll(libname, fallbacks=None): - lib = find_library(libname) - if lib is not None: - try: - return CDLL(lib) - except OSError: - pass - if fallbacks is not None: - for name in fallbacks: - try: - return CDLL(name) - except OSError: - # move on to the next fallback - pass - # No shared library was loaded. Raise OSError. - raise OSError( - "Could not find library %s or load any of its variants %s" % ( - libname, fallbacks or [])) - - -if sys.platform.startswith('linux'): - _lgeos = load_dll('geos_c', fallbacks=['libgeos_c.so.1', 'libgeos_c.so']) - free = load_dll('c').free - free.argtypes = [c_void_p] - free.restype = None - -elif sys.platform == 'darwin': - # 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: - 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 - -elif sys.platform == 'win32': - try: - egg_dlls = os.path.abspath(os.path.join(os.path.dirname(__file__), - "DLLs")) - wininst_dlls = os.path.abspath(os.__file__ + "../../../DLLs") - original_path = os.environ['PATH'] - os.environ['PATH'] = "%s;%s;%s" % \ - (egg_dlls, wininst_dlls, original_path) - _lgeos = CDLL("geos.dll") - except (ImportError, WindowsError, OSError): - raise - - def free(m): - try: - cdll.msvcrt.free(m) - except WindowsError: - # XXX: See http://trac.gispython.org/projects/PCL/ticket/149 - pass - -elif sys.platform == 'sunos5': - _lgeos = load_dll('geos_c', fallbacks=['libgeos_c.so.1', 'libgeos_c.so']) - free = CDLL('libc.so.1').free - free.argtypes = [c_void_p] - free.restype = None -else: # other *nix systems - _lgeos = load_dll('geos_c', fallbacks=['libgeos_c.so.1', 'libgeos_c.so']) - free = load_dll('c', fallbacks=['libc.so.6']).free - free.argtypes = [c_void_p] - free.restype = None - - -def _geos_version(): - # extern const char GEOS_DLL *GEOSversion(); - GEOSversion = _lgeos.GEOSversion - GEOSversion.restype = c_char_p - GEOSversion.argtypes = [] - #define GEOS_CAPI_VERSION "@VERSION@-CAPI-@CAPI_VERSION@" - geos_version_string = GEOSversion() - if sys.version_info[0] >= 3: - geos_version_string = geos_version_string.decode('ascii') - res = re.findall(r'(\d+)\.(\d+)\.(\d+)', geos_version_string) - assert len(res) == 2, res - geos_version = tuple(int(x) for x in res[0]) - capi_version = tuple(int(x) for x in res[1]) - return geos_version_string, geos_version, capi_version - -geos_version_string, geos_version, geos_capi_version = _geos_version() - # If we have the new interface, then record a baseline so that we know what # additional functions are declared in ctypes_declarations. if geos_version >= (3, 1, 0): @@ -430,7 +325,7 @@ class WKBWriter(object): def big_endian(self): """Byte order is big endian, True (default) or False""" return (self._lgeos.GEOSWKBWriter_getByteOrder(self._writer) == - self._ENDIAN_BIG) + self._ENDIAN_BIG) @big_endian.setter def big_endian(self, value): @@ -500,7 +395,7 @@ class WKBWriter(object): # Errcheck functions for ctypes def errcheck_wkb(result, func, argtuple): - '''Returns bytes from a C pointer''' + """Returns bytes from a C pointer""" if not result: return None size_ref = argtuple[-1] @@ -511,7 +406,7 @@ def errcheck_wkb(result, func, argtuple): def errcheck_just_free(result, func, argtuple): - '''Returns string from a C pointer''' + """Returns string from a C pointer""" retval = string_at(result) lgeos.GEOSFree(result) if sys.version_info[0] >= 3: @@ -519,9 +414,15 @@ def errcheck_just_free(result, func, argtuple): else: return retval +def errcheck_null_exception(result, func, argtuple): + """Wraps errcheck_just_free, raising a TopologicalError if result is NULL""" + if not result: + raise TopologicalError("The operation '{0}' could not be performed." + "Likely cause is invalidity of the geometry.".format(func.__name__)) + return errcheck_just_free(result, func, argtuple) def errcheck_predicate(result, func, argtuple): - '''Result is 2 on exception, 1 on True, 0 on False''' + """Result is 2 on exception, 1 on True, 0 on False""" if result == 2: raise PredicateError("Failed to evaluate %s" % repr(func)) return result @@ -562,7 +463,7 @@ class LGEOS300(LGEOSBase): # Deprecated self.GEOSGeomToWKB_buf.errcheck = errcheck_wkb self.GEOSGeomToWKT.errcheck = errcheck_just_free - self.GEOSRelate.errcheck = errcheck_just_free + self.GEOSRelate.errcheck = errcheck_null_exception for pred in ( self.GEOSDisjoint, self.GEOSTouches, @@ -573,6 +474,7 @@ class LGEOS300(LGEOSBase): self.GEOSOverlaps, self.GEOSEquals, self.GEOSEqualsExact, + self.GEOSRelatePattern, self.GEOSisEmpty, self.GEOSisValid, self.GEOSisSimple, @@ -608,6 +510,7 @@ class LGEOS300(LGEOSBase): self.methods['symmetric_difference'] = self.GEOSSymDifference self.methods['union'] = self.GEOSUnion self.methods['intersection'] = self.GEOSIntersection + self.methods['relate_pattern'] = self.GEOSRelatePattern self.methods['simplify'] = self.GEOSSimplify self.methods['topology_preserve_simplify'] = \ self.GEOSTopologyPreserveSimplify @@ -637,7 +540,7 @@ class LGEOS310(LGEOSBase): # Deprecated self.GEOSGeomToWKB_buf.func.errcheck = errcheck_wkb self.GEOSGeomToWKT.func.errcheck = errcheck_just_free - self.GEOSRelate.func.errcheck = errcheck_just_free + self.GEOSRelate.func.errcheck = errcheck_null_exception for pred in ( self.GEOSDisjoint, self.GEOSTouches, @@ -658,6 +561,7 @@ class LGEOS310(LGEOSBase): self.GEOSPreparedContainsProperly, self.GEOSPreparedCovers, self.GEOSPreparedIntersects, + self.GEOSRelatePattern, self.GEOSisEmpty, self.GEOSisValid, self.GEOSisSimple, @@ -706,6 +610,7 @@ class LGEOS310(LGEOSBase): self.GEOSPreparedContainsProperly self.methods['prepared_overlaps'] = self.GEOSPreparedOverlaps self.methods['prepared_covers'] = self.GEOSPreparedCovers + self.methods['relate_pattern'] = self.GEOSRelatePattern self.methods['simplify'] = self.GEOSSimplify self.methods['topology_preserve_simplify'] = \ self.GEOSTopologyPreserveSimplify @@ -731,7 +636,15 @@ class LGEOS320(LGEOS311): def __init__(self, dll): super(LGEOS320, self).__init__(dll) - self.methods['parallel_offset'] = self.GEOSSingleSidedBuffer + if geos_version >= (3, 2, 0): + def parallel_offset(geom, distance, resolution=16, join_style=1, mitre_limit=5.0, side='right'): + side = side == 'left' + if distance < 0: + distance = abs(distance) + side = not side + return self.GEOSSingleSidedBuffer(geom, distance, resolution, join_style, mitre_limit, side) + self.methods['parallel_offset'] = parallel_offset + self.methods['project'] = self.GEOSProject self.methods['project_normalized'] = self.GEOSProjectNormalized self.methods['interpolate'] = self.GEOSInterpolate @@ -760,6 +673,12 @@ class LGEOS330(LGEOS320): for pred in (self.GEOSisClosed,): pred.func.errcheck = errcheck_predicate + def parallel_offset(geom, distance, resolution=16, join_style=1, mitre_limit=5.0, side='right'): + if side == 'right': + distance *= -1 + return self.GEOSOffsetCurve(geom, distance, resolution, join_style, mitre_limit) + self.methods['parallel_offset'] = parallel_offset + self.methods['unary_union'] = self.GEOSUnaryUnion self.methods['is_closed'] = self.GEOSisClosed self.methods['cascaded_union'] = self.methods['unary_union'] @@ -793,6 +712,7 @@ else: lgeos = L(_lgeos) + def cleanup(proxy): del proxy diff --git a/shapely/impl.py b/shapely/impl.py index fda6281..828e0ae 100644 --- a/shapely/impl.py +++ b/shapely/impl.py @@ -21,6 +21,13 @@ from shapely.predicates import BinaryPredicate, UnaryPredicate from shapely.topology import BinaryRealProperty, BinaryTopologicalOp from shapely.topology import UnaryRealProperty, UnaryTopologicalOp + +class ImplementationError( + AttributeError, KeyError, NotImplementedError): + """To be raised when the registered implementation does not + support the requested method.""" + + def delegated(func): """A delegated method raises AttributeError in the absence of backend support.""" @@ -29,27 +36,43 @@ def delegated(func): try: return func(*args, **kwargs) except KeyError: - raise AttributeError("Method %r is not supported by %r" % - (func.__name__, args[0].impl)) + raise ImplementationError( + "Method '%s' not provided by registered " + "implementation '%s'" % (func.__name__, args[0].impl)) return wrapper # Map geometry methods to their GEOS delegates + class BaseImpl(object): + """Base class for registrable implementations.""" + def __init__(self, values): self.map = dict(values) + def update(self, values): self.map.update(values) + def __getitem__(self, key): - return self.map[key] + try: + return self.map[key] + except KeyError: + raise ImplementationError( + "Method '%s' not provided by registered " + "implementation '%s'" % (key, self.map)) + def __contains__(self, key): return key in self.map + class GEOSImpl(BaseImpl): + """GEOS implementation""" + def __repr__(self): return '<GEOSImpl object: GEOS C API version %s>' % ( lgeos.geos_capi_version,) + IMPL300 = { 'area': (UnaryRealProperty, 'area'), 'distance': (BinaryRealProperty, 'distance'), @@ -85,6 +108,7 @@ IMPL300 = { 'within': (BinaryPredicate, 'within'), 'covers': (BinaryPredicate, 'covers'), 'equals_exact': (BinaryPredicate, 'equals_exact'), + 'relate_pattern': (BinaryPredicate, 'relate_pattern'), # First pure Python implementation 'is_ccw': (cga.is_ccw_impl, 'is_ccw'), @@ -121,6 +145,7 @@ IMPL320 = { IMPL330 = { 'is_closed': (UnaryPredicate, 'is_closed')} + def impl_items(defs): return [(k, v[0](v[1])) for k, v in list(defs.items())] diff --git a/shapely/libgeos.py b/shapely/libgeos.py new file mode 100644 index 0000000..8bae437 --- /dev/null +++ b/shapely/libgeos.py @@ -0,0 +1,236 @@ +""" +Minimal proxy to a GEOS C dynamic library, which is system dependant + +Two environment variables influence this module: GEOS_LIBRARY_PATH and/or +GEOS_CONFIG. + +If GEOS_LIBRARY_PATH is set to a path to a GEOS C shared library, this is +used. Otherwise GEOS_CONFIG can be set to a path to `geos-config`. If +`geos-config` is already on the PATH environment variable, then it will +be used to help better guess the name for the GEOS C dynamic library. +""" + +import os +import logging +import re +import subprocess +import sys +from ctypes import CDLL, cdll, c_void_p, c_char_p +from ctypes.util import find_library + + +logging.basicConfig() +log = logging.getLogger(__name__) +if 'all' in sys.warnoptions: + log.level = logging.DEBUG + + +# The main point of this module is to load a dynamic library to this variable +lgeos = None + +# First try: use GEOS_LIBRARY_PATH environment variable +if 'GEOS_LIBRARY_PATH' in os.environ: + geos_library_path = os.environ['GEOS_LIBRARY_PATH'] + try: + lgeos = CDLL(geos_library_path) + except: + log.warn('cannot open shared object from GEOS_LIBRARY_PATH: %s', + geos_library_path) + if lgeos: + if hasattr(lgeos, 'GEOSversion'): + log.debug('found GEOS C library using GEOS_LIBRARY_PATH') + else: + raise OSError( + 'shared object GEOS_LIBRARY_PATH is not a GEOS C library: ' + + str(geos_library_path)) + +# Second try: use GEOS_CONFIG environment variable +if 'GEOS_CONFIG' in os.environ: + geos_config = os.environ['GEOS_CONFIG'] + log.debug('geos_config: %s', geos_config) +else: + geos_config = 'geos-config' + + +def get_geos_config(option): + '''Get configuration option from the `geos-config` development utility + + Path to utility is set with a module-level `geos_config` variable, which + can be changed or unset. + ''' + geos_config = globals().get('geos_config') + if not geos_config or not isinstance(geos_config, str): + raise OSError('Path to geos-config is not set') + try: + stdout, stderr = subprocess.Popen( + [geos_config, option], + stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate() + except OSError as ex: + # e.g., [Errno 2] No such file or directory + raise OSError( + 'Could not find geos-config %r: %s' % (geos_config, ex)) + if stderr and not stdout: + raise ValueError(stderr.strip()) + if sys.version_info[0] >= 3: + result = stdout.decode('ascii').strip() + else: + result = stdout.strip() + log.debug('%s %s: %r', geos_config, option, result) + return result + +# Now try and use the utility to load from `geos-config --clibs` with +# some magic smoke to guess the other parts of the library name +try: + clibs = get_geos_config('--clibs') +except OSError: + geos_config = None +if not lgeos and geos_config: + base = '' + name = 'geos_c' + for item in clibs.split(): + if item.startswith("-L"): + base = item[2:] + elif item.startswith("-l"): + name = item[2:] + # Now guess the actual library name using a list of possible formats + if sys.platform == 'win32': + # Unlikely, since geos-config is a shell script, but you never know... + fmts = ['{name}.dll'] + elif sys.platform == 'darwin': + fmts = ['lib{name}.dylib', '{name}.dylib', '{name}.framework/{name}'] + elif os.name == 'posix': + fmts = ['lib{name}.so', 'lib{name}.so.1'] + guesses = [] + for fmt in fmts: + lib_name = fmt.format(name=name) + geos_library_path = os.path.join(base, lib_name) + try: + lgeos = CDLL(geos_library_path) + break + except: + guesses.append(geos_library_path) + if lgeos: + if hasattr(lgeos, 'GEOSversion'): + log.debug('found GEOS C library using geos-config') + else: + raise OSError( + 'shared object found by geos-config is not a GEOS C library: ' + + str(geos_library_path)) + else: + log.warn("cannot open shared object from '%s --clibs': %r", + geos_config, clibs) + log.warn("there were %d guess(es) for this path:\n\t%s", + len(guesses), '\n\t'.join(guesses)) + + +# Platform-specific attempts, and build `free` object + +def load_dll(libname, fallbacks=[]): + '''Load GEOS dynamic library''' + lib = find_library(libname) + if lib is not None: + try: + return CDLL(lib) + except OSError: + pass + for name in fallbacks: + try: + return CDLL(name) + except OSError: + # move on to the next fallback + pass + raise OSError( + "Could not find library %s or load any of its variants %s" % ( + libname, fallbacks)) + +if sys.platform.startswith('linux'): + if not lgeos: + lgeos = load_dll('geos_c', + fallbacks=['libgeos_c.so.1', 'libgeos_c.so']) + free = load_dll('c').free + free.argtypes = [c_void_p] + free.restype = None + +elif sys.platform == 'darwin': + if not lgeos: + # 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: + if hasattr(sys, 'frozen'): + # .app file from py2app + alt_paths = [os.path.join(os.environ['RESOURCEPATH'], + '..', 'Frameworks', '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', fallbacks=['/usr/lib/libc.dylib']).free + free.argtypes = [c_void_p] + free.restype = None + +elif sys.platform == 'win32': + if not lgeos: + try: + egg_dlls = os.path.abspath( + os.path.join(os.path.dirname(__file__), "DLLs")) + wininst_dlls = os.path.abspath(os.__file__ + "../../../DLLs") + original_path = os.environ['PATH'] + os.environ['PATH'] = "%s;%s;%s" % \ + (egg_dlls, wininst_dlls, original_path) + lgeos = CDLL("geos.dll") + except (ImportError, WindowsError, OSError): + raise + + def free(m): + try: + cdll.msvcrt.free(m) + except WindowsError: + # TODO: http://web.archive.org/web/20070810024932/ + # + http://trac.gispython.org/projects/PCL/ticket/149 + pass + +elif sys.platform == 'sunos5': + if not lgeos: + lgeos = load_dll('geos_c', + fallbacks=['libgeos_c.so.1', 'libgeos_c.so']) + free = CDLL('libc.so.1').free + free.argtypes = [c_void_p] + free.restype = None + +else: # other *nix systems + if not lgeos: + lgeos = load_dll('geos_c', + fallbacks=['libgeos_c.so.1', 'libgeos_c.so']) + free = load_dll('c', fallbacks=['libc.so.6']).free + free.argtypes = [c_void_p] + free.restype = None + +# TODO: what to do with 'free'? It isn't used. + + +def _geos_version(): + # extern const char GEOS_DLL *GEOSversion(); + GEOSversion = lgeos.GEOSversion + GEOSversion.restype = c_char_p + GEOSversion.argtypes = [] + # #define GEOS_CAPI_VERSION "@VERSION@-CAPI-@CAPI_VERSION@" + geos_version_string = GEOSversion() + if sys.version_info[0] >= 3: + geos_version_string = geos_version_string.decode('ascii') + + res = re.findall(r'(\d+)\.(\d+)\.(\d+)', geos_version_string) + assert len(res) == 2, res + geos_version = tuple(int(x) for x in res[0]) + capi_version = tuple(int(x) for x in res[1]) + + return geos_version_string, geos_version, capi_version + +geos_version_string, geos_version, geos_capi_version = _geos_version() diff --git a/shapely/linref.py b/shapely/linref.py index fc3d0f4..8e727fc 100644 --- a/shapely/linref.py +++ b/shapely/linref.py @@ -7,9 +7,7 @@ from shapely.topology import Delegating class LinearRefBase(Delegating): def _validate_line(self, ob): super(LinearRefBase, self)._validate(ob) - try: - assert ob.geom_type in ['LineString', 'MultiLineString'] - except AssertionError: + if not ob.geom_type in ['LinearRing', 'LineString', 'MultiLineString']: raise TypeError("Only linear types support this operation") class ProjectOp(LinearRefBase): diff --git a/shapely/speedups/__init__.py b/shapely/speedups/__init__.py index 60d9d0b..2e84a1f 100644 --- a/shapely/speedups/__init__.py +++ b/shapely/speedups/__init__.py @@ -58,5 +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'] + shapely.affinity.affine_transform = _orig['affine_transform'] _orig.clear() diff --git a/tests/__init__.py b/tests/__init__.py index b4cf8ab..057380d 100755 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1,10 +1,17 @@ import sys -from shapely.geos import geos_version_string, lgeos, WKTWriter +from shapely.libgeos import geos_version_string +from shapely.geos import lgeos, WKTWriter from shapely import speedups +from packaging.version import Version + + +test_int_types = [int] + try: import numpy numpy_version = numpy.version.version + test_int_types.extend([int, numpy.int16, numpy.int32, numpy.int64]) except ImportError: numpy = False numpy_version = 'not available' diff --git a/tests/test_default_impl.py b/tests/test_default_impl.py new file mode 100644 index 0000000..7a33193 --- /dev/null +++ b/tests/test_default_impl.py @@ -0,0 +1,24 @@ +import pytest + +from shapely.geometry import Point +from shapely.impl import delegated, ImplementationError + + +def test_error(): + with pytest.raises(ImplementationError): + Point(0, 0).impl['bogus']() + with pytest.raises(NotImplementedError): + Point(0, 0).impl['bogus']() + with pytest.raises(KeyError): + Point(0, 0).impl['bogus']() + + +def test_delegated(): + class Poynt(Point): + @delegated + def bogus(self): + return self.impl['bogus']() + with pytest.raises(ImplementationError): + Poynt(0, 0).bogus() + with pytest.raises(AttributeError): + Poynt(0, 0).bogus() diff --git a/tests/test_dlls.py b/tests/test_dlls.py index 8c82bcb..a887411 100644 --- a/tests/test_dlls.py +++ b/tests/test_dlls.py @@ -1,6 +1,6 @@ from . import unittest -from shapely.geos import load_dll +from shapely.libgeos import load_dll class LoadingTestCase(unittest.TestCase): diff --git a/tests/test_multi.py b/tests/test_multi.py new file mode 100644 index 0000000..49fd4e3 --- /dev/null +++ b/tests/test_multi.py @@ -0,0 +1,8 @@ +from . import unittest, test_int_types + +class MultiGeometryTestCase(unittest.TestCase): + def subgeom_access_test(self, cls, geoms): + geom = cls(geoms) + for t in test_int_types: + for i, g in enumerate(geoms): + self.assertEqual(geom[t(i)], geoms[i]) diff --git a/tests/test_multilinestring.py b/tests/test_multilinestring.py index 61b6bc3..be3f4ab 100644 --- a/tests/test_multilinestring.py +++ b/tests/test_multilinestring.py @@ -1,10 +1,11 @@ -from . import unittest, numpy +from . import unittest, numpy, test_int_types +from .test_multi import MultiGeometryTestCase from shapely.geos import lgeos from shapely.geometry import LineString, MultiLineString, asMultiLineString from shapely.geometry.base import dump_coords -class MultiLineStringTestCase(unittest.TestCase): +class MultiLineStringTestCase(MultiGeometryTestCase): def test_multilinestring(self): @@ -73,6 +74,11 @@ class MultiLineStringTestCase(unittest.TestCase): # TODO: is there an inverse? + def test_subgeom_access(self): + line0 = LineString([(0.0, 1.0), (2.0, 3.0)]) + line1 = LineString([(4.0, 5.0), (6.0, 7.0)]) + self.subgeom_access_test(MultiLineString, [line0, line1]) + def test_suite(): return unittest.TestLoader().loadTestsFromTestCase(MultiLineStringTestCase) diff --git a/tests/test_multipoint.py b/tests/test_multipoint.py index 013ba0d..c84443c 100644 --- a/tests/test_multipoint.py +++ b/tests/test_multipoint.py @@ -1,9 +1,10 @@ -from . import unittest, numpy +from . import unittest, numpy, test_int_types +from .test_multi import MultiGeometryTestCase from shapely.geometry import Point, MultiPoint, asMultiPoint from shapely.geometry.base import dump_coords -class MultiPointTestCase(unittest.TestCase): +class MultiPointTestCase(MultiGeometryTestCase): def test_multipoint(self): @@ -68,6 +69,10 @@ class MultiPointTestCase(unittest.TestCase): pas = asarray(geoma) assert_array_equal(pas, array([[1., 2.], [3., 4.]])) + def test_subgeom_access(self): + p0 = Point(1.0, 2.0) + p1 = Point(3.0, 4.0) + self.subgeom_access_test(MultiPoint, [p0, p1]) def test_suite(): return unittest.TestLoader().loadTestsFromTestCase(MultiPointTestCase) diff --git a/tests/test_multipolygon.py b/tests/test_multipolygon.py index 9e7d257..188beb9 100644 --- a/tests/test_multipolygon.py +++ b/tests/test_multipolygon.py @@ -1,9 +1,10 @@ -from . import unittest +from . import unittest, numpy, test_int_types +from .test_multi import MultiGeometryTestCase from shapely.geometry import Polygon, MultiPolygon, asMultiPolygon from shapely.geometry.base import dump_coords -class MultiPolygonTestCase(unittest.TestCase): +class MultiPolygonTestCase(MultiGeometryTestCase): def test_multipolygon(self): @@ -67,6 +68,11 @@ class MultiPolygonTestCase(unittest.TestCase): self.assertEqual(len(mpa.geoms[0].interiors), 1) self.assertEqual(len(mpa.geoms[0].interiors[0].coords), 5) + def test_subgeom_access(self): + poly0 = Polygon([(0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (1.0, 0.0)]) + poly1 = Polygon([(0.25, 0.25), (0.25, 0.5), (0.5, 0.5), (0.5, 0.25)]) + self.subgeom_access_test(MultiPolygon, [poly0, poly1]) + def test_suite(): return unittest.TestLoader().loadTestsFromTestCase(MultiPolygonTestCase) diff --git a/tests/test_operations.py b/tests/test_operations.py index bdea03a..3377646 100644 --- a/tests/test_operations.py +++ b/tests/test_operations.py @@ -1,7 +1,8 @@ from . import unittest +import pytest from shapely.geometry import Point, Polygon, MultiPoint, GeometryCollection from shapely.wkt import loads - +from shapely.geos import TopologicalError class OperationsTestCase(unittest.TestCase): @@ -62,9 +63,15 @@ class OperationsTestCase(unittest.TestCase): self.assertIsInstance(point.centroid, Point) + def test_relate(self): # Relate - self.assertEqual(point.relate(Point(-1, -1)), 'FF0FFF0F2') + self.assertEqual(Point(0, 0).relate(Point(-1, -1)), 'FF0FFF0F2') + # issue #294: should raise TopologicalError on exception + invalid_polygon = loads('POLYGON ((40 100, 80 100, 80 60, 40 60, 40 100), (60 60, 80 60, 80 40, 60 40, 60 60))') + assert(not invalid_polygon.is_valid) + with pytest.raises(TopologicalError): + invalid_polygon.relate(invalid_polygon) def test_suite(): return unittest.TestLoader().loadTestsFromTestCase(OperationsTestCase) diff --git a/tests/test_parallel_offset.py b/tests/test_parallel_offset.py new file mode 100644 index 0000000..ae7a2f1 --- /dev/null +++ b/tests/test_parallel_offset.py @@ -0,0 +1,33 @@ +from . import unittest +from shapely.geos import geos_version +from shapely.geometry import LineString, LinearRing +from shapely.wkt import loads + +class OperationsTestCase(unittest.TestCase): + @unittest.skipIf(geos_version < (3, 2, 0), 'GEOS 3.2.0 required') + def test_parallel_offset_linestring(self): + line1 = LineString([(0, 0), (10, 0)]) + left = line1.parallel_offset(5, 'left') + self.assertEqual(left, LineString([(0, 5), (10, 5)])) + right = line1.parallel_offset(5, 'right') + self.assertEqual(right, LineString([(10, -5), (0, -5)])) + right = line1.parallel_offset(-5, 'left') + self.assertEqual(right, LineString([(10, -5), (0, -5)])) + left = line1.parallel_offset(-5, 'right') + self.assertEqual(left, LineString([(0, 5), (10, 5)])) + + # by default, parallel_offset is right-handed + self.assertEqual(line1.parallel_offset(5), right) + + line2 = LineString([(0, 0), (5, 0), (5, -5)]) + self.assertEqual(line2.parallel_offset(2, 'left', resolution=1), + LineString([(0, 2), (5, 2), (7, 0), (7, -5)])) + self.assertEqual(line2.parallel_offset(2, 'left', join_style=2, + resolution=1), + LineString([(0, 2), (7, 2), (7, -5)])) + + @unittest.skipIf(geos_version < (3, 2, 0), 'GEOS 3.2.0 required') + def test_parallel_offset_linear_ring(self): + lr1 = LinearRing([(0, 0), (5, 0), (5, 5), (0, 5), (0, 0)]) + self.assertEqual(lr1.parallel_offset(2, 'left', resolution=1), + LineString([(2, 2), (3, 2), (3, 3), (2, 3), (2, 2)])) diff --git a/tests/test_predicates.py b/tests/test_predicates.py index 3062eac..c3c1cb4 100644 --- a/tests/test_predicates.py +++ b/tests/test_predicates.py @@ -2,8 +2,8 @@ """ from . import unittest from shapely.geometry import Point, Polygon -from shapely.geos import TopologicalError - +from shapely.geos import TopologicalError, PredicateError +import pytest class PredicatesTestCase(unittest.TestCase): @@ -41,6 +41,25 @@ class PredicatesTestCase(unittest.TestCase): (399, 450), (339, 207)] self.assertRaises(TopologicalError, Polygon(p1).within, Polygon(p2)) + def test_relate_pattern(self): + + # a pair of partially overlapping polygons, and a nearby point + g1 = Polygon([(0, 0), (0, 1), (3, 1), (3, 0), (0, 0)]) + g2 = Polygon([(1, -1), (1, 2), (2, 2), (2, -1), (1, -1)]) + g3 = Point(5, 5) + + assert(g1.relate(g2) == '212101212') + assert(g1.relate_pattern(g2, '212101212')) + assert(g1.relate_pattern(g2, '*********')) + assert(g1.relate_pattern(g2, '2********')) + assert(g1.relate_pattern(g2, 'T********')) + assert(not g1.relate_pattern(g2, '112101212')) + assert(not g1.relate_pattern(g2, '1********')) + assert(g1.relate_pattern(g3, 'FF2FF10F2')) + + # an invalid pattern should raise an exception + with pytest.raises(PredicateError): + g1.relate_pattern(g2, 'fail') def test_suite(): return unittest.TestLoader().loadTestsFromTestCase(PredicatesTestCase) -- 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