From 056f87ab28b327b82508792bbac1e70270885812 Mon Sep 17 00:00:00 2001 From: Justin Bronn Date: Thu, 2 Aug 2007 05:31:10 +0000 Subject: [PATCH] gis: geos: fully-mutable geometries have landed; fixed GEOSPointer boolean value; equivalence now uses equals_exact (vertex-by-vertex matching); added in-place set operations; improved tests; getquoted() will return ST_* for PostGIS 1.2.2+. git-svn-id: http://code.djangoproject.com/svn/django/branches/gis@5786 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- django/contrib/gis/geos/base.py | 74 +++++++++++++--- django/contrib/gis/geos/collections.py | 66 +++++++------- django/contrib/gis/geos/geometries.py | 78 +++++++++++------ django/contrib/gis/geos/libgeos.py | 34 +++----- django/contrib/gis/tests/__init__.py | 5 ++ django/contrib/gis/tests/geometries.py | 8 +- django/contrib/gis/tests/test_geos.py | 115 ++++++++++++++++++++++--- 7 files changed, 275 insertions(+), 105 deletions(-) diff --git a/django/contrib/gis/geos/base.py b/django/contrib/gis/geos/base.py index fdc52e1b2b..d0af4388a8 100644 --- a/django/contrib/gis/geos/base.py +++ b/django/contrib/gis/geos/base.py @@ -12,7 +12,7 @@ from types import StringType, IntType, FloatType # Python and GEOS-related dependencies. import re from warnings import warn -from django.contrib.gis.geos.libgeos import lgeos, GEOSPointer, HAS_NUMPY, ISQLQuote +from django.contrib.gis.geos.libgeos import lgeos, GEOSPointer, HAS_NUMPY, ISQLQuote, GEOM_FUNC_PREFIX from django.contrib.gis.geos.error import GEOSException, GEOSGeometryIndexError from django.contrib.gis.geos.coordseq import GEOSCoordSeq, create_cs if HAS_NUMPY: from numpy import ndarray, array @@ -87,7 +87,7 @@ class GEOSGeometry(object): def __del__(self): "Destroys this geometry -- only if the pointer is valid and whether or not it belongs to a parent." - #print 'Deleting %s (parent=%s, valid=%s)' % (self.__class__.__name__, self._parent, self._ptr.valid) + #print 'base: Deleting %s (parent=%s, valid=%s)' % (self.__class__.__name__, self._parent, self._ptr.valid) # Only calling destroy on valid pointers not spawned from a parent if self._ptr.valid and not self._parent: lgeos.GEOSGeom_destroy(self._ptr()) @@ -101,11 +101,11 @@ class GEOSGeometry(object): # Comparison operators def __eq__(self, other): "Equivalence testing." - return self.equals(other) + return self.equals_exact(other) def __ne__(self, other): "The not equals operator." - return not self.equals(other) + return not self.equals_exact(other) ### Geometry set-like operations ### # Thanks to Sean Gillies for inspiration: @@ -115,21 +115,76 @@ class GEOSGeometry(object): "Returns the union of this Geometry and the other." return self.union(other) + # g1 |= g2 + def __ior__(self, other): + "Reassigns this Geometry to the union of this Geometry and the other." + return self.union(other) + # g = g1 & g2 def __and__(self, other): "Returns the intersection of this Geometry and the other." return self.intersection(other) + # g1 &= g2 + def __iand__(self, other): + "Reassigns this Geometry to the intersection of this Geometry and the other." + return self.intersection(other) + # g = g1 - g2 def __sub__(self, other): "Return the difference this Geometry and the other." return self.difference(other) + # g1 -= g2 + def __isub__(self, other): + "Reassigns this Geometry to the difference of this Geometry and the other." + return self.difference(other) + # g = g1 ^ g2 def __xor__(self, other): "Return the symmetric difference of this Geometry and the other." return self.sym_difference(other) + # g1 ^= g2 + def __ixor__(self, other): + "Reassigns this Geometry to the symmetric difference of this Geometry and the other." + return self.sym_difference(other) + + def _nullify(self): + """During initialization of geometries from other geometries, this routine is + used to nullify any parent geometries (since they will now be missing memory + components) and to nullify the geometry itself to prevent future access. + Only the address (an integer) of the current geometry is returned for use in + initializing the new geometry.""" + # First getting the memory address of the geometry. + address = self._ptr() + + # If the geometry is a child geometry, then the parent geometry pointer is + # nullified. + if self._parent: self._parent.nullify() + + # Nullifying the geometry pointer + self._ptr.nullify() + + return address + + def _reassign(self, new_geom): + "Internal routine for reassigning internal pointer to a new geometry." + # Only can re-assign when given a pointer or a geometry. + if not isinstance(new_geom, (GEOSPointer, GEOSGeometry)): + raise TypeError, 'cannot reassign geometry on given type: %s' % type(new_geom) + gtype = new_geom.geom_type + + # Re-assigning the internal GEOSPointer to the new geometry, nullifying + # the new Geometry in the process. + if isinstance(new_geom, GEOSGeometry): self._ptr.set(new_geom._nullify()) + else: self._ptr = new_geom + + # The new geometry class may be different from the original, so setting + # the __class__ and populating the internal geometry or ring dictionary. + self.__class__ = GEOS_CLASSES[gtype] + if isinstance(self, (Polygon, GeometryCollection)): self._populate() + #### Psycopg2 database adaptor routines #### def __conform__(self, proto): # Does the given protocol conform to what Psycopg2 expects? @@ -140,7 +195,8 @@ class GEOSGeometry(object): def getquoted(self): "Returns a properly quoted string for use in PostgresSQL/PostGIS." - return "GeometryFromText('%s', %s)" % (self.wkt, self.srid or -1) + # GeometryFromText() is ST_GeometryFromText() in PostGIS >= 1.2.2 + return "%sGeometryFromText('%s', %s)" % (GEOM_FUNC_PREFIX, self.wkt, self.srid or -1) #### Coordinate Sequence Routines #### @property @@ -299,10 +355,8 @@ class GEOSGeometry(object): def get_srid(self): "Gets the SRID for the geometry, returns None if no SRID is set." s = lgeos.GEOSGetSRID(self._ptr()) - if s == 0: - return None - else: - return s + if s == 0: return None + else: return s def set_srid(self, srid): "Sets the SRID for the geometry." @@ -404,7 +458,7 @@ class GEOSGeometry(object): def clone(self): "Clones this Geometry." - return GEOSGeometry(lgeos.GEOSGeom_clone(self._ptr())) + return GEOSGeometry(lgeos.GEOSGeom_clone(self._ptr()), srid=self.srid) # Class mapping dictionary from django.contrib.gis.geos.geometries import Point, Polygon, LineString, LinearRing diff --git a/django/contrib/gis/geos/collections.py b/django/contrib/gis/geos/collections.py index 070147837d..6711d41cf3 100644 --- a/django/contrib/gis/geos/collections.py +++ b/django/contrib/gis/geos/collections.py @@ -4,37 +4,25 @@ """ from ctypes import c_int, c_uint, byref, cast from types import TupleType, ListType -from django.contrib.gis.geos.libgeos import lgeos, GEOSPointer, init_from_geom, get_pointer_arr, GEOM_PTR +from django.contrib.gis.geos.libgeos import lgeos, GEOSPointer, get_pointer_arr, GEOM_PTR from django.contrib.gis.geos.base import GEOSGeometry from django.contrib.gis.geos.error import GEOSException, GEOSGeometryIndexError from django.contrib.gis.geos.geometries import Point, LineString, LinearRing, Polygon -def init_from_poly(poly): - "Internal routine used for initializing Geometry Collections from Polygons." - # Constructing a new Polygon to take control of the rings. - p = Polygon(*tuple(ring for ring in poly)) - - # If this Polygon came from a GeometryCollection, it is a child - # and the parent geometry pointer is nullified. - if poly._parent: poly._parent.nullify() - - # Nullifying the polygon pointer - poly._ptr.nullify() - - # Returning the address of the new Polygon. - return p._ptr() - class GeometryCollection(GEOSGeometry): _allowed = (Point, LineString, LinearRing, Polygon) _typeid = 7 def __init__(self, *args, **kwargs): + "Initializes a Geometry Collection from a sequence of Geometry objects." + # Setting up the collection for creation self._ptr = GEOSPointer(0) # Initially NULL self._geoms = {} - self._parent = False + self._parent = None + # Checking the arguments if not args: - raise TypeError, 'Must provide at least one LinearRing to initialize Polygon.' + raise TypeError, 'Must provide at least one Geometry to initialize %s.' % self.__class__.__name__ if len(args) == 1: # If only one geometry provided or a list of geometries is provided if isinstance(args[0], (TupleType, ListType)): @@ -54,23 +42,18 @@ class GeometryCollection(GEOSGeometry): # Incrementing through each input geometry. for i in xrange(ngeom): - if isinstance(init_geoms[i], Polygon): - # Special care is taken when importing from Polygons - geoms[i] = cast(init_from_poly(init_geoms[i]), GEOM_PTR) - else: - geoms[i] = cast(init_from_geom(init_geoms[i]), GEOM_PTR) - + geoms[i] = cast(init_geoms[i]._nullify(), GEOM_PTR) + # Calling the parent class, using the pointer returned from GEOS createCollection() super(GeometryCollection, self).__init__(lgeos.GEOSGeom_createCollection(c_int(self._typeid), byref(geoms), c_uint(ngeom)), **kwargs) def __del__(self): "Overloaded deletion method for Geometry Collections." - #print 'Deleting %s (parent=%s, valid=%s)' % (self.__class__.__name__, self._parent, self._ptr.valid) + #print 'collection: Deleting %s (parent=%s, valid=%s)' % (self.__class__.__name__, self._parent, self._ptr.valid) # If this geometry is still valid, it hasn't been modified by others. if self._ptr.valid: # Nullifying pointers to internal geometries, preventing any attempted future access. for k in self._geoms: self._geoms[k].nullify() - super(GeometryCollection, self).__del__() else: # Internal memory has become part of other Geometry objects, must delete the # internal objects which are still valid individually, since calling destructor @@ -80,20 +63,38 @@ class GeometryCollection(GEOSGeometry): if self._geoms[k].valid: lgeos.GEOSGeom_destroy(self._geoms[k].address) self._geoms[k].nullify() + super(GeometryCollection, self).__del__() def __getitem__(self, index): - "For indexing on the multiple geometries." + "Returns the Geometry from this Collection at the given index (0-based)." # Checking the index and returning the corresponding GEOS geometry. self._checkindex(index) return GEOSGeometry(self._geoms[index], parent=self._ptr, srid=self.srid) + def __setitem__(self, index, geom): + "Sets the Geometry at the specified index." + self._checkindex(index) + if not isinstance(geom, self._allowed): + raise TypeError, 'Incompatible Geometry for collection.' + + # Constructing the list of geometries that will go in the collection. + new_geoms = [] + for i in xrange(len(self)): + if i == index: new_geoms.append(geom) + else: new_geoms.append(self[i]) + + # Creating a new geometry collection from the list, and + # re-assigning the pointers. + new_collection = self.__class__(*new_geoms) + self._reassign(new_collection) + def __iter__(self): - "For iteration on the multiple geometries." + "Iterates over each Geometry in the Collection." for i in xrange(len(self)): yield self.__getitem__(i) def __len__(self): - "Returns the number of geometries in this collection." + "Returns the number of geometries in this Collection." return self.num_geom def _checkindex(self, index): @@ -101,13 +102,18 @@ class GeometryCollection(GEOSGeometry): if index < 0 or index >= self.num_geom: raise GEOSGeometryIndexError, 'invalid GEOS Geometry index: %s' % str(index) + def _nullify(self): + "Overloaded from base method to nullify geometry references in this Collection." + # Nullifying the references to the internal Geometry objects from this Collection. + for k in self._geoms: self._geoms[k].nullify() + return super(GeometryCollection, self)._nullify() + def _populate(self): "Populates the internal child geometry dictionary." self._geoms = {} for i in xrange(self.num_geom): self._geoms[i] = GEOSPointer(lgeos.GEOSGetGeometryN(self._ptr(), c_int(i))) - # MultiPoint, MultiLineString, and MultiPolygon class definitions. class MultiPoint(GeometryCollection): _allowed = Point diff --git a/django/contrib/gis/geos/geometries.py b/django/contrib/gis/geos/geometries.py index 635297e58b..50650e61b7 100644 --- a/django/contrib/gis/geos/geometries.py +++ b/django/contrib/gis/geos/geometries.py @@ -7,7 +7,7 @@ from ctypes import c_double, c_int, c_uint, byref, cast from types import FloatType, IntType, ListType, TupleType from django.contrib.gis.geos.coordseq import GEOSCoordSeq, create_cs -from django.contrib.gis.geos.libgeos import lgeos, GEOSPointer, get_pointer_arr, init_from_geom, GEOM_PTR, HAS_NUMPY +from django.contrib.gis.geos.libgeos import lgeos, GEOSPointer, get_pointer_arr, GEOM_PTR, HAS_NUMPY from django.contrib.gis.geos.base import GEOSGeometry from django.contrib.gis.geos.error import GEOSException, GEOSGeometryIndexError if HAS_NUMPY: from numpy import ndarray, array @@ -21,7 +21,9 @@ class Point(GEOSGeometry): >>> p = Point(5, 23, 8) # 3D point, passed in with individual parameters """ + # Setting-up for Point Creation self._ptr = GEOSPointer(0) # Initially NULL + self._parent = None if isinstance(x, (TupleType, ListType)): # Here a tuple or list was passed in under the ``x`` parameter. @@ -132,7 +134,9 @@ class LineString(GEOSGeometry): ls = LineString(array([(1, 1), (2, 2)])) ls = LineString(Point(1, 1), Point(2, 2)) """ + # Setting up for LineString creation self._ptr = GEOSPointer(0) # Initially NULL + self._parent = None # If only one argument was provided, then set the coords array appropriately if len(args) == 1: coords = args[0] @@ -254,6 +258,7 @@ class Polygon(GEOSGeometry): poly = Polygon(shell, (hole1, hole2)) """ self._ptr = GEOSPointer(0) # Initially NULL + self._parent = None self._rings = {} if not args: raise TypeError, 'Must provide at list one LinearRing instance to initialize Polygon.' @@ -277,43 +282,58 @@ class Polygon(GEOSGeometry): holes = get_pointer_arr(nholes) for i in xrange(nholes): # Casting to the Geometry Pointer type - holes[i] = cast(init_from_geom(init_holes[i]), GEOM_PTR) + holes[i] = cast(init_holes[i]._nullify(), GEOM_PTR) # Getting the shell pointer address, - shell = init_from_geom(ext_ring) + shell = ext_ring._nullify() # Calling with the GEOS createPolygon factory. super(Polygon, self).__init__(lgeos.GEOSGeom_createPolygon(shell, byref(holes), c_uint(nholes)), **kwargs) def __del__(self): "Overloaded deletion method for Polygons." - #print 'Deleting %s (parent=%s, valid=%s)' % (self.__class__.__name__, self._parent, self._ptr.valid) + #print 'polygon: Deleting %s (parent=%s, valid=%s)' % (self.__class__.__name__, self._parent, self._ptr.valid) # If this geometry is still valid, it hasn't been modified by others. if self._ptr.valid: # Nulling the pointers to internal rings, preventing any attempted future access for k in self._rings: self._rings[k].nullify() - super(Polygon, self).__del__() - else: + elif not self._parent: # Internal memory has become part of other objects; must delete the # internal objects which are still valid individually, since calling # destructor on entire geometry will result in an attempted deletion - # of NULL pointers for the missing components. + # of NULL pointers for the missing components. Not performed on + # children Polygons from MultiPolygon or GeometryCollection objects. for k in self._rings: if self._rings[k].valid: lgeos.GEOSGeom_destroy(self._rings[k].address) self._rings[k].nullify() + super(Polygon, self).__del__() def __getitem__(self, index): """Returns the ring at the specified index. The first index, 0, will always return the exterior ring. Indices > 0 will return the interior ring.""" - if index < 0 or index > self.num_interior_rings: - raise GEOSGeometryIndexError, 'invalid GEOS Geometry index: %s' % str(index) + if index == 0: + return self.exterior_ring else: - if index == 0: - return self.exterior_ring - else: - # Getting the interior ring, have to subtract 1 from the index. - return self.get_interior_ring(index-1) + # Getting the interior ring, have to subtract 1 from the index. + return self.get_interior_ring(index-1) + + def __setitem__(self, index, ring): + "Sets the ring at the specified index with the given ring." + # Checking the index and ring parameters. + self._checkindex(index) + if not isinstance(ring, LinearRing): + raise TypeError, 'must set Polygon index with a LinearRing object' + + # Constructing the ring parameters + new_rings = [] + for i in xrange(len(self)): + if index == i: new_rings.append(ring) + else: new_rings.append(self[i]) + + # Constructing the new Polygon with the ring parameters, and reassigning the internals. + new_poly = Polygon(*new_rings) + self._reassign(new_poly) def __iter__(self): "Iterates over each ring in the polygon." @@ -324,6 +344,17 @@ class Polygon(GEOSGeometry): "Returns the number of rings in this Polygon." return self.num_interior_rings + 1 + def _checkindex(self, index): + "Internal routine for checking the given ring index." + if index < 0 or index >= len(self): + raise GEOSGeometryIndexError, 'invalid Polygon ring index: %s' % index + + def _nullify(self): + "Overloaded from base method to nullify ring references as well." + # Nullifying the references to the internal rings of this Polygon. + for k in self._rings: self._rings[k].nullify() + return super(Polygon, self)._nullify() + def _populate(self): "Populates the internal rings dictionary." # Getting the exterior ring first for the 0th index. @@ -336,13 +367,9 @@ class Polygon(GEOSGeometry): def get_interior_ring(self, ring_i): """Gets the interior ring at the specified index, 0 is for the first interior ring, not the exterior ring.""" - - # Making sure the ring index is within range - if ring_i < 0 or ring_i >= self.num_interior_rings: - raise IndexError, 'ring index out of range' - - # Returning the ring from the internal ring dictionary (have to - # add one to the index) + # Returning the ring from the internal ring dictionary (have to add one + # to index since all internal rings come after the exterior ring) + self._checkindex(ring_i+1) return GEOSGeometry(self._rings[ring_i+1], parent=self._ptr, srid=self.srid) #### Polygon Properties #### @@ -361,14 +388,13 @@ class Polygon(GEOSGeometry): "Gets the exterior ring of the Polygon." return GEOSGeometry(self._rings[0], parent=self._ptr, srid=self.srid) - def set_ext_ring(self): + def set_ext_ring(self, ring): "Sets the exterior ring of the Polygon." - # Sets the exterior ring - raise NotImplementedError + self[0] = ring # properties for the exterior ring/shell - exterior_ring = property(get_ext_ring) - shell = property(get_ext_ring) + exterior_ring = property(get_ext_ring, set_ext_ring) + shell = property(get_ext_ring, set_ext_ring) @property def tuple(self): diff --git a/django/contrib/gis/geos/libgeos.py b/django/contrib/gis/geos/libgeos.py index 6728da3d29..f505522095 100644 --- a/django/contrib/gis/geos/libgeos.py +++ b/django/contrib/gis/geos/libgeos.py @@ -18,11 +18,13 @@ try: except ImportError: HAS_NUMPY = False -# Psycopg2 supported? +# Are psycopg2 and GeoDjango models being used? try: from psycopg2.extensions import ISQLQuote -except ImportError: + from django.contrib.gis.db.backend.postgis import GEOM_FUNC_PREFIX +except (ImportError, EnvironmentError): ISQLQuote = None + GEOM_FUNC_PREFIX = None # Setting the appropriate name for the GEOS-C library, depending on which # OS and POSIX platform we're running. @@ -71,21 +73,22 @@ error_h = ERRORFUNC(error_h) # "extern void GEOS_DLL initGEOS(GEOSMessageHandler notice_function, GEOSMessageHandler error_function);" lgeos.initGEOS(notice_h, error_h) -#### GEOS Geometry Pointer utilities. #### +#### GEOS Geometry Pointer object, related C data structures, and functions. #### # Opaque GEOS geometry structure class GEOSGeom_t(Structure): "Opaque structure used when arrays of geometries are needed as parameters." pass + # Pointer to opaque geometry structure GEOM_PTR = POINTER(GEOSGeom_t) + # Used specifically by the GEOSGeom_createPolygon and GEOSGeom_createCollection GEOS routines def get_pointer_arr(n): "Gets a ctypes pointer array (of length `n`) for GEOSGeom_t opaque pointer." GeomArr = GEOM_PTR * n return GeomArr() -#### GEOS Pointer object and routines #### class GEOSPointer(object): """The GEOSPointer provides a layer of abstraction in accessing the values returned by GEOS geometry creation routines. Memory addresses (integers) are kept in a C pointer, @@ -108,13 +111,16 @@ class GEOSPointer(object): if self.valid: return self.address else: raise GEOSException, 'GEOS pointer no longer valid (was this geometry or the parent geometry deleted or modified?)' - def __bool__(self): + def __nonzero__(self): "Returns True when the GEOSPointer is valid." return self.valid def __str__(self): return str(self.address) + def __repr__(self): + return 'GEOSPointer(%s)' % self.address + ### GEOSPointer Properties ### @property def address(self): @@ -161,21 +167,3 @@ class GEOSPointer(object): # Nullifying both the geometry and coordinate sequence pointer. self.set(0) self.set(0, coordseq=True) - -def init_from_geom(geom): - """During initialization of geometries from other geometries, this routine is - used to nullify any parent geometries (since they will now be missing memory - components) and to nullify the geometry itself to prevent future access. - Only the address (an integer) of the current geometry is returned for use in - initializing the new geometry.""" - # First getting the memory address of the geometry. - address = geom._ptr() - - # If the geometry is a child geometry, then the parent geometry pointer is - # nullified. - if geom._parent: geom._parent.nullify() - - # Nullifying the geometry pointer - geom._ptr.nullify() - - return address diff --git a/django/contrib/gis/tests/__init__.py b/django/contrib/gis/tests/__init__.py index fc9bd6cb03..317008923d 100644 --- a/django/contrib/gis/tests/__init__.py +++ b/django/contrib/gis/tests/__init__.py @@ -56,9 +56,13 @@ def run_tests(module_list, verbosity=1): from django.conf import settings # Getting initial values. + old_debug = settings.DEBUG old_name = copy(settings.DATABASE_NAME) old_installed = copy(settings.INSTALLED_APPS) + # Want DEBUG to be set to False. + settings.DEBUG = False + # Creating the test suite, adding the test models to INSTALLED_APPS, and # adding the model test suites to our suite package. test_suite = suite() @@ -82,6 +86,7 @@ def run_tests(module_list, verbosity=1): # Cleaning up, destroying the test spatial database and resetting the INSTALLED_APPS. destroy_test_db(old_name, verbosity) + settings.DEBUG = old_debug settings.INSTALLED_APPS = old_installed # Returning the total failures and errors diff --git a/django/contrib/gis/tests/geometries.py b/django/contrib/gis/tests/geometries.py index 7b5a2cce8f..ac052359f5 100644 --- a/django/contrib/gis/tests/geometries.py +++ b/django/contrib/gis/tests/geometries.py @@ -115,19 +115,19 @@ topology_geoms = ( (TestGeom('POLYGON ((-5.0 0.0, -5.0 10.0, 5.0 10.0, 5.0 0.0, ) intersect_geoms = ( TestGeom('POLYGON ((5 5,5 0,0 0,0 5,5 5))'), - TestGeom('POLYGON ((10 1, 11 3, 13 4, 15 6, 16 8, 16 10, 15 12, 13 13, 11 12, 10 10, 9 12, 7 13, 5 12, 4 10, 4 8, 5 6, 7 4, 9 3, 10 1))') + TestGeom('POLYGON ((10 1, 9 3, 7 4, 5 6, 4 8, 4 10, 5 12, 7 13, 9 12, 10 10, 11 12, 13 13, 15 12, 16 10, 16 8, 15 6, 13 4, 11 3, 10 1))'), ) union_geoms = ( TestGeom('POLYGON ((-5 0,-5 10,5 10,5 5,10 5,10 -5,0 -5,0 0,-5 0))'), - TestGeom('POLYGON ((2 0, 18 0, 18 15, 2 15, 2 0))'), + TestGeom('POLYGON ((2 0, 2 15, 18 15, 18 0, 2 0))'), ) diff_geoms = ( TestGeom('POLYGON ((-5 0,-5 10,5 10,5 5,0 5,0 0,-5 0))'), - TestGeom('POLYGON ((2 0, 18 0, 18 15, 2 15, 2 0), (10 1, 11 3, 13 4, 15 6, 16 8, 16 10, 15 12, 13 13, 11 12, 10 10, 9 12, 7 13, 5 12, 4 10, 4 8, 5 6, 7 4, 9 3, 10 1))'), + TestGeom('POLYGON ((2 0, 2 15, 18 15, 18 0, 2 0), (10 1, 11 3, 13 4, 15 6, 16 8, 16 10, 15 12, 13 13, 11 12, 10 10, 9 12, 7 13, 5 12, 4 10, 4 8, 5 6, 7 4, 9 3, 10 1))'), ) sdiff_geoms = ( TestGeom('MULTIPOLYGON (((-5 0,-5 10,5 10,5 5,0 5,0 0,-5 0)),((0 0,5 0,5 5,10 5,10 -5,0 -5,0 0)))'), - TestGeom('POLYGON ((2 0, 18 0, 18 15, 2 15, 2 0), (10 1, 11 3, 13 4, 15 6, 16 8, 16 10, 15 12, 13 13, 11 12, 10 10, 9 12, 7 13, 5 12, 4 10, 4 8, 5 6, 7 4, 9 3, 10 1))'), + TestGeom('POLYGON ((2 0, 2 15, 18 15, 18 0, 2 0), (10 1, 11 3, 13 4, 15 6, 16 8, 16 10, 15 12, 13 13, 11 12, 10 10, 9 12, 7 13, 5 12, 4 10, 4 8, 5 6, 7 4, 9 3, 10 1))'), ) relate_geoms = ( (TestGeom('MULTIPOINT(80 70, 20 20, 200 170, 140 120)'), diff --git a/django/contrib/gis/tests/test_geos.py b/django/contrib/gis/tests/test_geos.py index 5999b61409..ded55680e8 100644 --- a/django/contrib/gis/tests/test_geos.py +++ b/django/contrib/gis/tests/test_geos.py @@ -1,4 +1,4 @@ -import unittest +import random, unittest from django.contrib.gis.geos import \ GEOSException, GEOSGeometryIndexError, \ GEOSGeometry, Point, LineString, LinearRing, Polygon, \ @@ -166,7 +166,7 @@ class GEOSTest(unittest.TestCase): self.assertEqual(lr, LinearRing(lr.tuple)) self.assertEqual(lr, LinearRing(*lr.tuple)) self.assertEqual(lr, LinearRing([list(tup) for tup in lr.tuple])) - if HAS_NUMPY: self.assertEqual(lr, LineString(array(lr.tuple))) + if HAS_NUMPY: self.assertEqual(lr, LinearRing(array(lr.tuple))) def test05a_polygons(self): "Testing Polygon objects." @@ -200,10 +200,15 @@ class GEOSTest(unittest.TestCase): self.assertEqual(p.ext_ring_cs, ring.tuple) self.assertEqual(p.ext_ring_cs, poly[0].tuple) # Testing __getitem__ - # Testing __iter__ + # Testing __getitem__ and __setitem__ on invalid indices + self.assertRaises(GEOSGeometryIndexError, poly.__getitem__, len(poly)) + self.assertRaises(GEOSGeometryIndexError, poly.__setitem__, len(poly), False) + self.assertRaises(GEOSGeometryIndexError, poly.__getitem__, -1) + + # Testing __iter__ for r in poly: - self.assertEqual(ring.geom_type, 'LinearRing') - self.assertEqual(ring.geom_typeid, 2) + self.assertEqual(r.geom_type, 'LinearRing') + self.assertEqual(r.geom_typeid, 2) # Testing polygon construction. self.assertRaises(TypeError, Polygon, 0, [1, 2, 3]) @@ -224,8 +229,8 @@ class GEOSTest(unittest.TestCase): except TypeError: pass poly[0][1] = newval # setting the second point in the polygon with the newvalue (based on the old) - self.assertEqual(newval, poly[0][1]) # The point in the polygon should be the - self.assertEqual(False, poly == prev) # Even different from the clone we just made + self.assertEqual(newval, poly[0][1]) # The point in the polygon should be the new value + self.assertEqual(False, poly == prev) # Should be different from the clone we just made def test05b_multipolygons(self): "Testing MultiPolygon objects." @@ -426,11 +431,12 @@ class GEOSTest(unittest.TestCase): a = GEOSGeometry(g_tup[0].wkt) b = GEOSGeometry(g_tup[1].wkt) i1 = GEOSGeometry(intersect_geoms[i].wkt) - self.assertEqual(True, a.intersects(b)) i2 = a.intersection(b) self.assertEqual(i1, i2) - self.assertEqual(i1, a & b) + self.assertEqual(i1, a & b) # __and__ is intersection operator + a &= b # testing __iand__ + self.assertEqual(i1, a) def test11_union(self): "Testing union()." @@ -441,7 +447,9 @@ class GEOSTest(unittest.TestCase): u1 = GEOSGeometry(union_geoms[i].wkt) u2 = a.union(b) self.assertEqual(u1, u2) - self.assertEqual(u1, a | b) # Union ('|') operator + self.assertEqual(u1, a | b) # __or__ is union operator + a |= b # testing __ior__ + self.assertEqual(u1, a) def test12_difference(self): "Testing difference()." @@ -452,7 +460,9 @@ class GEOSTest(unittest.TestCase): d1 = GEOSGeometry(diff_geoms[i].wkt) d2 = a.difference(b) self.assertEqual(d1, d2) - self.assertEqual(d1, a - b) # Difference ('-') operator + self.assertEqual(d1, a - b) # __sub__ is difference operator + a -= b # testing __isub__ + self.assertEqual(d1, a) def test13_symdifference(self): "Testing sym_difference()." @@ -463,7 +473,9 @@ class GEOSTest(unittest.TestCase): d1 = GEOSGeometry(sdiff_geoms[i].wkt) d2 = a.sym_difference(b) self.assertEqual(d1, d2) - self.assertEqual(d1, a ^ b) # Symmetric difference ('^') operator + self.assertEqual(d1, a ^ b) # __xor__ is symmetric difference operator + a ^= b # testing __ixor__ + self.assertEqual(d1, a) def test14_buffer(self): "Testing buffer()." @@ -492,6 +504,85 @@ class GEOSTest(unittest.TestCase): self.assertAlmostEqual(exp_ring[k][0], buf_ring[k][0], 9) self.assertAlmostEqual(exp_ring[k][1], buf_ring[k][1], 9) + def test15_srid(self): + "Testing the SRID property and keyword." + # Testing SRID keyword on Point + pnt = Point(5, 23, srid=4326) + self.assertEqual(4326, pnt.srid) + pnt.srid = 3084 + self.assertEqual(3084, pnt.srid) + self.assertRaises(TypeError, pnt.set_srid, '4326') + + # Testing SRID keyword on fromstr(), and on Polygon rings. + poly = fromstr(polygons[1].wkt, srid=4269) + self.assertEqual(4269, poly.srid) + for ring in poly: self.assertEqual(4269, ring.srid) + poly.srid = 4326 + self.assertEqual(4326, poly.shell.srid) + + # Testing SRID keyword on GeometryCollection + gc = GeometryCollection(Point(5, 23), LineString((0, 0), (1.5, 1.5), (3, 3)), srid=32021) + self.assertEqual(32021, gc.srid) + for i in range(len(gc)): self.assertEqual(32021, gc[i].srid) + + def test16_mutable_geometries(self): + "Testing the mutability of Polygons and Geometry Collections." + ### Testing the mutability of Polygons ### + for p in polygons: + poly = fromstr(p.wkt) + + # Should only be able to use __setitem__ with LinearRing geometries. + self.assertRaises(TypeError, poly.__setitem__, 0, LineString((1, 1), (2, 2))) + + # Constructing the new shell by adding 500 to every point in the old shell. + shell_tup = poly.shell.tuple + new_coords = [] + for point in shell_tup: new_coords.append((point[0] + 500., point[1] + 500.)) + shell1 = LinearRing(*tuple(new_coords)) + shell2 = shell1.clone() + + # Assigning polygon's exterior ring w/the new shell + poly.exterior_ring = shell1 + self.assertRaises(GEOSException, str, shell1) # shell1 should no longer be accessible + self.assertEqual(poly.exterior_ring, shell2) + self.assertEqual(poly[0], shell2) + del poly, shell1, shell_tup # cleaning up + + ### Testing the mutability of Geometry Collections + for tg in multipoints: + mp = fromstr(tg.wkt) + for i in range(len(mp)): + # Creating a random point. + pnt = mp[i].clone() + new = Point(random.randint(1, 100), random.randint(1, 100)) + tmp = new.clone() + # Testing the assignmen + mp[i] = tmp + self.assertRaises(GEOSException, len, tmp) + self.assertEqual(mp[i], new) + self.assertEqual(mp[i].wkt, new.wkt) + self.assertNotEqual(pnt, mp[i]) + del mp + + # Multipolygons involve much more memory management because each + # polygon w/in the collection has its own rings. + for tg in multipolygons: + mpoly = fromstr(tg.wkt) + for i in xrange(len(mpoly)): + poly = mpoly[i].clone() + # Offsetting the each ring in the polygon by 500. + tmp = poly.clone() + for r in tmp: + for j in xrange(len(r)): r[j] = (r[j][0] + 500., r[j][1] + 500.) + self.assertNotEqual(poly, tmp) + new = tmp.clone() # a 'reference' copy of the geometry used in assignment + # Testing the assignment + mpoly[i] = tmp + self.assertRaises(GEOSException, str, tmp) + self.assertEqual(mpoly[i], new) + self.assertNotEqual(poly, mpoly[i]) + del mpoly + def suite(): s = unittest.TestSuite() s.addTest(unittest.makeSuite(GEOSTest))