diff --git a/django/contrib/gis/geos/collections.py b/django/contrib/gis/geos/collections.py index becc6b0ce3..01277b1b65 100644 --- a/django/contrib/gis/geos/collections.py +++ b/django/contrib/gis/geos/collections.py @@ -7,10 +7,9 @@ import warnings from ctypes import byref, c_int, c_uint from django.contrib.gis.geos import prototypes as capi -from django.contrib.gis.geos.geometry import ( - GEOSGeometry, ProjectInterpolateMixin, -) -from django.contrib.gis.geos.libgeos import get_pointer_arr +from django.contrib.gis.geos.error import GEOSException +from django.contrib.gis.geos.geometry import GEOSGeometry, LinearGeometryMixin +from django.contrib.gis.geos.libgeos import geos_version_info, get_pointer_arr from django.contrib.gis.geos.linestring import LinearRing, LineString from django.contrib.gis.geos.point import Point from django.contrib.gis.geos.polygon import Polygon @@ -114,17 +113,15 @@ class MultiPoint(GeometryCollection): _typeid = 4 -class MultiLineString(ProjectInterpolateMixin, GeometryCollection): +class MultiLineString(LinearGeometryMixin, GeometryCollection): _allowed = (LineString, LinearRing) _typeid = 5 @property - def merged(self): - """ - Returns a LineString representing the line merge of this - MultiLineString. - """ - return self._topology(capi.geos_linemerge(self.ptr)) + def closed(self): + if geos_version_info()['version'] < '3.5': + raise GEOSException("MultiLineString.closed requires GEOS >= 3.5.0.") + return super(MultiLineString, self).closed class MultiPolygon(GeometryCollection): diff --git a/django/contrib/gis/geos/geometry.py b/django/contrib/gis/geos/geometry.py index 1aea19821d..f66effd756 100644 --- a/django/contrib/gis/geos/geometry.py +++ b/django/contrib/gis/geos/geometry.py @@ -685,7 +685,7 @@ class GEOSGeometry(GEOSBase, ListMixin): return GEOSGeometry(capi.geom_clone(self.ptr), srid=self.srid) -class ProjectInterpolateMixin(object): +class LinearGeometryMixin(object): """ Used for LineString and MultiLineString. """ @@ -706,3 +706,17 @@ class ProjectInterpolateMixin(object): if not isinstance(point, Point): raise TypeError('locate_point argument must be a Point') return capi.geos_project_normalized(self.ptr, point.ptr) + + @property + def merged(self): + """ + Return the line merge of this Geometry. + """ + return self._topology(capi.geos_linemerge(self.ptr)) + + @property + def closed(self): + """ + Return whether or not this Geometry is closed. + """ + return capi.geos_isclosed(self.ptr) diff --git a/django/contrib/gis/geos/linestring.py b/django/contrib/gis/geos/linestring.py index cf74aad7fa..7bfc004370 100644 --- a/django/contrib/gis/geos/linestring.py +++ b/django/contrib/gis/geos/linestring.py @@ -1,15 +1,13 @@ from django.contrib.gis.geos import prototypes as capi from django.contrib.gis.geos.coordseq import GEOSCoordSeq from django.contrib.gis.geos.error import GEOSException -from django.contrib.gis.geos.geometry import ( - GEOSGeometry, ProjectInterpolateMixin, -) +from django.contrib.gis.geos.geometry import GEOSGeometry, LinearGeometryMixin from django.contrib.gis.geos.point import Point from django.contrib.gis.shortcuts import numpy from django.utils.six.moves import range -class LineString(ProjectInterpolateMixin, GEOSGeometry): +class LineString(LinearGeometryMixin, GEOSGeometry): _init_func = capi.create_linestring _minlength = 2 has_cs = True @@ -154,11 +152,6 @@ class LineString(ProjectInterpolateMixin, GEOSGeometry): "Returns a numpy array for the LineString." return self._listarr(self._cs.__getitem__) - @property - def merged(self): - "Returns the line merge of this LineString." - return self._topology(capi.geos_linemerge(self.ptr)) - @property def x(self): "Returns a list or numpy array of the X variable." diff --git a/django/contrib/gis/geos/prototypes/__init__.py b/django/contrib/gis/geos/prototypes/__init__.py index 32073749d8..0357b8110e 100644 --- a/django/contrib/gis/geos/prototypes/__init__.py +++ b/django/contrib/gis/geos/prototypes/__init__.py @@ -19,8 +19,8 @@ from django.contrib.gis.geos.prototypes.geom import ( # NOQA from django.contrib.gis.geos.prototypes.misc import * # NOQA from django.contrib.gis.geos.prototypes.predicates import ( # NOQA geos_contains, geos_covers, geos_crosses, geos_disjoint, geos_equals, - geos_equalsexact, geos_hasz, geos_intersects, geos_isempty, geos_isring, - geos_issimple, geos_isvalid, geos_overlaps, geos_relatepattern, - geos_touches, geos_within, + geos_equalsexact, geos_hasz, geos_intersects, geos_isclosed, geos_isempty, + geos_isring, geos_issimple, geos_isvalid, geos_overlaps, + geos_relatepattern, geos_touches, geos_within, ) from django.contrib.gis.geos.prototypes.topology import * # NOQA diff --git a/django/contrib/gis/geos/prototypes/predicates.py b/django/contrib/gis/geos/prototypes/predicates.py index 8d335fa187..9021fc71f3 100644 --- a/django/contrib/gis/geos/prototypes/predicates.py +++ b/django/contrib/gis/geos/prototypes/predicates.py @@ -23,6 +23,7 @@ class BinaryPredicate(UnaryPredicate): # ## Unary Predicates ## geos_hasz = UnaryPredicate('GEOSHasZ') +geos_isclosed = UnaryPredicate('GEOSisClosed') geos_isempty = UnaryPredicate('GEOSisEmpty') geos_isring = UnaryPredicate('GEOSisRing') geos_issimple = UnaryPredicate('GEOSisSimple') diff --git a/docs/ref/contrib/gis/geos.txt b/docs/ref/contrib/gis/geos.txt index ddf4f0706f..259221ef23 100644 --- a/docs/ref/contrib/gis/geos.txt +++ b/docs/ref/contrib/gis/geos.txt @@ -694,6 +694,12 @@ is returned instead. In previous versions, an empty ``LineString`` couldn't be instantiated. + .. attribute:: closed + + .. versionadded:: 1.10 + + Returns whether or not this ``LineString`` is closed. + ``LinearRing`` -------------- @@ -790,6 +796,11 @@ Geometry Collections Returns a :class:`LineString` representing the line merge of all the components in this ``MultiLineString``. + .. attribute:: closed + + .. versionadded:: 1.10 + + Returns ``True`` if and only if all elements are closed. Requires GEOS 3.5. ``MultiPolygon`` ---------------- diff --git a/docs/releases/1.10.txt b/docs/releases/1.10.txt index 6276f2e102..25998ddc96 100644 --- a/docs/releases/1.10.txt +++ b/docs/releases/1.10.txt @@ -104,6 +104,11 @@ Minor features of :class:`~django.contrib.gis.geos.WKTWriter` allow controlling output of the fractional part of the coordinates in WKT. +* Added the :attr:`LineString.closed + ` and + :attr:`MultiLineString.closed + ` properties. + :mod:`django.contrib.messages` ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/tests/gis_tests/geos_tests/test_geos.py b/tests/gis_tests/geos_tests/test_geos.py index fd08e3037a..116e8d35f8 100644 --- a/tests/gis_tests/geos_tests/test_geos.py +++ b/tests/gis_tests/geos_tests/test_geos.py @@ -15,6 +15,7 @@ from django.contrib.gis.geos import ( fromfile, fromstr, ) from django.contrib.gis.geos.base import GEOSBase +from django.contrib.gis.geos.libgeos import geos_version_info from django.contrib.gis.shortcuts import numpy from django.template import Context from django.template.engine import Engine @@ -660,6 +661,20 @@ class GEOSTest(SimpleTestCase, TestDataMixin): self.assertTrue(poly.covers(Point(5, 5))) self.assertFalse(poly.covers(Point(100, 100))) + def test_closed(self): + ls_closed = LineString((0, 0), (1, 1), (0, 0)) + ls_not_closed = LineString((0, 0), (1, 1)) + self.assertFalse(ls_not_closed.closed) + self.assertTrue(ls_closed.closed) + + if geos_version_info()['version'] >= '3.5': + self.assertFalse(MultiLineString(ls_closed, ls_not_closed).closed) + self.assertTrue(MultiLineString(ls_closed, ls_closed).closed) + + with mock.patch('django.contrib.gis.geos.collections.geos_version_info', lambda: {'version': '3.4.9'}): + with self.assertRaisesMessage(GEOSException, "MultiLineString.closed requires GEOS >= 3.5.0."): + MultiLineString().closed + def test_srid(self): "Testing the SRID property and keyword." # Testing SRID keyword on Point