From e9224451864d39c2ba2b2b82070bbafa61563fe1 Mon Sep 17 00:00:00 2001 From: Justin Bronn Date: Sat, 21 Jul 2007 17:39:09 +0000 Subject: [PATCH] gis: big changes within in GEOS internals: (1) All Geometry types and Collections now have their own constructor (e.g., Polygon(LinearRing(..)), MultiPoint(Point(..), Point(..))) (2) Memory management improved, laying the foundation for fully mutable geometries. (3) Added set-like operations (|, &, -, ^) (4) Added & improved tests. (5) docstring changes git-svn-id: http://code.djangoproject.com/svn/django/branches/gis@5742 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- django/contrib/gis/geos/__init__.py | 9 +- django/contrib/gis/geos/base.py | 154 ++++++++++++------ django/contrib/gis/geos/collections.py | 112 ++++++++++--- django/contrib/gis/geos/coordseq.py | 29 +++- django/contrib/gis/geos/geometries.py | 199 ++++++++++++++++------- django/contrib/gis/geos/libgeos.py | 96 +++++++++-- django/contrib/gis/tests/geometries.py | 9 + django/contrib/gis/tests/test_geos.py | 157 +++++++++++++++--- django/contrib/gis/utils/LayerMapping.py | 2 +- 9 files changed, 582 insertions(+), 185 deletions(-) diff --git a/django/contrib/gis/geos/__init__.py b/django/contrib/gis/geos/__init__.py index 12714ed94a..832b8906e6 100644 --- a/django/contrib/gis/geos/__init__.py +++ b/django/contrib/gis/geos/__init__.py @@ -30,8 +30,13 @@ """ from base import GEOSGeometry -from geometries import Point, LineString, LinearRing, HAS_NUMPY -from error import GEOSException +from geometries import Point, LineString, LinearRing, Polygon, HAS_NUMPY +from collections import GeometryCollection, MultiPoint, MultiLineString, MultiPolygon +from error import GEOSException, GEOSGeometryIndexError + +def fromstr(wkt_or_hex): + "Given a string value (wkt or hex), returns a GEOSGeometry object." + return GEOSGeometry(wkt_or_hex) def hex_to_wkt(hex): "Converts HEXEWKB into WKT." diff --git a/django/contrib/gis/geos/base.py b/django/contrib/gis/geos/base.py index ef4c81118e..10213a63c0 100644 --- a/django/contrib/gis/geos/base.py +++ b/django/contrib/gis/geos/base.py @@ -1,35 +1,38 @@ -# Trying not to pollute the namespace. +""" + This module contains the 'base' GEOSGeometry object -- all GEOS geometries + inherit from this object. +""" + +# ctypes and types dependencies. from ctypes import \ byref, string_at, create_string_buffer, pointer, \ c_char_p, c_double, c_int, c_size_t -from types import StringType, IntType, FloatType, TupleType, ListType +from types import StringType, IntType, FloatType -# Getting GEOS-related dependencies. +# Python and GEOS-related dependencies. import re from warnings import warn from django.contrib.gis.geos.libgeos import lgeos, GEOSPointer, HAS_NUMPY 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 -if HAS_NUMPY: - from numpy import ndarray, array - -# For recognizing HEXEWKB. -hex_regex = re.compile(r'^[0-9A-Fa-f]+') +# Regular expression for recognizing HEXEWKB. +hex_regex = re.compile(r'^[0-9A-Fa-f]+$') class GEOSGeometry(object): "A class that, generally, encapsulates a GEOS geometry." #### Python 'magic' routines #### - def __init__(self, geo_input, input_type=False, child=False): + def __init__(self, geo_input, input_type=False, parent=False): """The constructor for GEOS geometry objects. May take the following strings as inputs, WKT ("wkt"), HEXEWKB ("hex", PostGIS-specific canonical form). The `input_type` keyword has been deprecated -- geometry type is now auto-detected. - The `child` keyword is for internal use only, and indicates to the garbage collector - not to delete this geometry if it was spawned from a parent (e.g., the exterior - ring from a polygon). + The `parent` keyword is for internal use only, and indicates to the garbage collector + not to delete this geometry because it was spawned from a parent (e.g., the exterior + ring from a polygon). Its value is the GEOSPointer of the parent geometry. """ # Initially, setting the pointer to NULL @@ -55,84 +58,126 @@ class GEOSGeometry(object): raise TypeError, 'Improper geometry input type: %s' % str(type(geo_input)) if bool(g): - # If we have a GEOSPointer object, just set the '_ptr' attribute with g + # If we have a GEOSPointer object, just set the '_ptr' attribute with input if isinstance(g, GEOSPointer): self._ptr = g - else: self._ptr.set(g) # Otherwise, set the address + else: self._ptr.set(g) # Otherwise, set with the address else: raise GEOSException, 'Could not initialize GEOS Geometry with given input.' - # Setting the 'child' flag -- when the object is labeled with this flag - # it will not be destroyed by __del__(). This is used for child geometries from + # Setting the 'parent' flag -- when the object is labeled with this flag + # it will not be destroyed by __del__(). This is used for child geometries spawned from # parent geometries (e.g., LinearRings from a Polygon, Points from a MultiPoint, etc.). - self._child = child - + if isinstance(parent, GEOSPointer): + self._parent = parent + else: + self._parent = GEOSPointer(0) + # Setting the class type (e.g., 'Point', 'Polygon', etc.) self.__class__ = GEOS_CLASSES[self.geom_type] + # Getting the coordinate sequence for the geometry (will be None on geometries that + # do not have coordinate sequences) + self._get_cs() + # Extra setup needed for Geometries that may be parents. - if isinstance(self, GeometryCollection): self._geoms = {} - if isinstance(self, Polygon): self._rings = {} + if isinstance(self, (Polygon, GeometryCollection)): self._populate() def __del__(self): - "Destroys this geometry -- only if the pointer is valid and this is not a child geometry." - #print 'Deleting %s (child=%s, valid=%s)' % (self.geom_type, self._child, self._ptr.valid) - if self._ptr.valid and not self._child: lgeos.GEOSGeom_destroy(self._ptr()) + "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) + # Only calling destroy on valid pointers not spawned from a parent + if self._ptr.valid and not self._parent: lgeos.GEOSGeom_destroy(self._ptr()) def __str__(self): "WKT is used for the string representation." return self.wkt + def __repr__(self): + return '<%s object>' % self.geom_type + + # Comparison operators def __eq__(self, other): "Equivalence testing." return self.equals(other) + def __ne__(self, other): + "The not equals operator." + return not self.equals(other) + + ### Geometry set-like operations ### + # Thanks to Sean Gillies for inspiration: + # http://lists.gispython.org/pipermail/community/2007-July/001034.html + # g = g1 | g2 + def __or__(self, other): + "Returns 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) + + # g = g1 - g2 + def __sub__(self, other): + "Return the difference 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) + #### Coordinate Sequence Routines #### - def _cache_cs(self): - "Caches the coordinate sequence for this Geometry." - if not hasattr(self, '_cs'): - # Only these geometries are allowed to have coordinate sequences. - if self.geom_type in ('LineString', 'LinearRing', 'Point'): - self._cs = GEOSCoordSeq(GEOSPointer(lgeos.GEOSGeom_getCoordSeq(self._ptr())), self.hasz) - else: - self._cs = None + @property + def has_cs(self): + "Returns True if this Geometry has a coordinate sequence, False if not." + # Only these geometries are allowed to have coordinate sequences. + if isinstance(self, (Point, LineString, LinearRing)): + return True + else: + return False + + def _get_cs(self): + "Gets the coordinate sequence for this Geometry." + if self.has_cs: + self._ptr.set(lgeos.GEOSGeom_getCoordSeq(self._ptr()), coordseq=True) + self._cs = GEOSCoordSeq(self._ptr, self.hasz) + else: + self._cs = None @property def coord_seq(self): - "Returns the coordinate sequence for the geometry." - # Getting the coordinate sequence for the geometry - self._cache_cs() - - # Returning a GEOSCoordSeq wrapped around the pointer. + "Returns the coordinate sequence for this Geometry." return self._cs #### Geometry Info #### @property def geom_type(self): - "Returns a string representing the geometry type, e.g. 'Polygon'" + "Returns a string representing the Geometry type, e.g. 'Polygon'" return string_at(lgeos.GEOSGeomType(self._ptr())) @property def geom_typeid(self): - "Returns an integer representing the geometry type." + "Returns an integer representing the Geometry type." return lgeos.GEOSGeomTypeId(self._ptr()) @property def num_geom(self): - "Returns the number of geometries in the geometry." + "Returns the number of geometries in the Geometry." n = lgeos.GEOSGetNumGeometries(self._ptr()) if n == -1: raise GEOSException, 'Error getting number of geometries.' else: return n @property def num_coords(self): - "Returns the number of coordinates in the geometry." + "Returns the number of coordinates in the Geometry." n = lgeos.GEOSGetNumCoordinates(self._ptr()) if n == -1: raise GEOSException, 'Error getting number of coordinates.' else: return n @property def num_points(self): - "Returns the number points, or coordinates, in the geometry." + "Returns the number points, or coordinates, in the Geometry." return self.num_coords @property @@ -145,6 +190,7 @@ class GEOSGeometry(object): status = lgeos.GEOSNormalize(self._ptr()) if status == -1: raise GEOSException, 'failed to normalize geometry' + ## Internal for GEOS unary & binary predicate functions ## def _unary_predicate(self, func): "Returns the result, or raises an exception for the given unary predicate function." val = func(self._ptr()) @@ -164,12 +210,12 @@ class GEOSGeometry(object): #### Unary predicates #### @property def empty(self): - "Returns a boolean indicating whether the set of points in this geometry are empty." + "Returns a boolean indicating whether the set of points in this Geometry are empty." return self._unary_predicate(lgeos.GEOSisEmpty) @property def valid(self): - "This property tests the validity of this geometry." + "This property tests the validity of this Geometry." return self._unary_predicate(lgeos.GEOSisValid) @property @@ -190,17 +236,17 @@ class GEOSGeometry(object): #### Binary predicates. #### def relate_pattern(self, other, pattern): """Returns true if the elements in the DE-9IM intersection matrix for - the two Geometrys match the elements in pattern.""" + the two Geometries match the elements in pattern.""" if len(pattern) > 9: raise GEOSException, 'invalid intersection matrix pattern' return self._binary_predicate(lgeos.GEOSRelatePattern, other, c_char_p(pattern)) def disjoint(self, other): - "Returns true if the DE-9IM intersection matrix for the two Geometrys is FF*FF****." + "Returns true if the DE-9IM intersection matrix for the two Geometries is FF*FF****." return self._binary_predicate(lgeos.GEOSDisjoint, other) def touches(self, other): - "Returns true if the DE-9IM intersection matrix for the two Geometrys is FT*******, F**T***** or F***T****." + "Returns true if the DE-9IM intersection matrix for the two Geometries is FT*******, F**T***** or F***T****." return self._binary_predicate(lgeos.GEOSTouches, other) def intersects(self, other): @@ -208,12 +254,12 @@ class GEOSGeometry(object): return self._binary_predicate(lgeos.GEOSIntersects, other) def crosses(self, other): - """Returns true if the DE-9IM intersection matrix for the two Geometrys is T*T****** (for a point and a curve, + """Returns true if the DE-9IM intersection matrix for the two Geometries is T*T****** (for a point and a curve, a point and an area or a line and an area) 0******** (for two curves).""" return self._binary_predicate(lgeos.GEOSCrosses, other) def within(self, other): - "Returns true if the DE-9IM intersection matrix for the two Geometrys is T*F**F***." + "Returns true if the DE-9IM intersection matrix for the two Geometries is T*F**F***." return self._binary_predicate(lgeos.GEOSWithin, other) def contains(self, other): @@ -221,16 +267,16 @@ class GEOSGeometry(object): return self._binary_predicate(lgeos.GEOSContains, other) def overlaps(self, other): - """Returns true if the DE-9IM intersection matrix for the two Geometrys is T*T***T** (for two points + """Returns true if the DE-9IM intersection matrix for the two Geometries is T*T***T** (for two points or two surfaces) 1*T***T** (for two curves).""" return self._binary_predicate(lgeos.GEOSOverlaps, other) def equals(self, other): - "Returns true if the DE-9IM intersection matrix for the two Geometrys is T*F**FFF*." + "Returns true if the DE-9IM intersection matrix for the two Geometries is T*F**FFF*." return self._binary_predicate(lgeos.GEOSEquals, other) def equals_exact(self, other, tolerance=0): - "Returns true if the two Geometrys are exactly equal, up to a specified tolerance." + "Returns true if the two Geometries are exactly equal, up to a specified tolerance." tol = c_double(tolerance) return self._binary_predicate(lgeos.GEOSEqualsExact, other, tol) @@ -290,7 +336,7 @@ class GEOSGeometry(object): @property def centroid(self): - """The centroid is equal to the centroid of the set of component Geometrys + """The centroid is equal to the centroid of the set of component Geometries of highest dimension (since the lower-dimension geometries contribute zero "weight" to the centroid).""" return self._unary_topology(lgeos.GEOSGetCentroid) @@ -344,7 +390,7 @@ class GEOSGeometry(object): def clone(self): "Clones this Geometry." return GEOSGeometry(lgeos.GEOSGeom_clone(self._ptr())) - + # Class mapping dictionary from django.contrib.gis.geos.geometries import Point, Polygon, LineString, LinearRing from django.contrib.gis.geos.collections import GeometryCollection, MultiPoint, MultiLineString, MultiPolygon @@ -352,8 +398,8 @@ GEOS_CLASSES = {'Point' : Point, 'Polygon' : Polygon, 'LineString' : LineString, 'LinearRing' : LinearRing, - 'GeometryCollection' : GeometryCollection, 'MultiPoint' : MultiPoint, 'MultiLineString' : MultiLineString, 'MultiPolygon' : MultiPolygon, + 'GeometryCollection' : GeometryCollection, } diff --git a/django/contrib/gis/geos/collections.py b/django/contrib/gis/geos/collections.py index 9d995fb13e..9105b34b2f 100644 --- a/django/contrib/gis/geos/collections.py +++ b/django/contrib/gis/geos/collections.py @@ -2,33 +2,94 @@ This module houses the Geometry Collection objects: GeometryCollection, MultiPoint, MultiLineString, and MultiPolygon """ -from ctypes import c_int -from django.contrib.gis.geos.libgeos import lgeos, GEOSPointer +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.base import GEOSGeometry -from django.contrib.gis.geos.error import GEOSException +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): + self._ptr = GEOSPointer(0) # Initially NULL + self._geoms = {} + self._parent = False + + if not args: + raise TypeError, 'Must provide at least one LinearRing to initialize Polygon.' + + if len(args) == 1: # If only one geometry provided or a list of geometries is provided + if isinstance(args[0], (TupleType, ListType)): + init_geoms = args[0] + else: + init_geoms = args + else: + init_geoms = args + + # Ensuring that only the permitted geometries are allowed in this collection + if False in [isinstance(geom, self._allowed) for geom in init_geoms]: + raise TypeError, 'Invalid Geometry type encountered in the arguments.' + + # Creating the geometry pointer array + ngeom = len(init_geoms) + geoms = get_pointer_arr(ngeom) + + # 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) + + # 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))) def __del__(self): - "Override the GEOSGeometry delete routine to safely take care of any spawned geometries." - # Nullifying the pointers to internal geometries, preventing any attempted future access - for k in self._geoms: self._geoms[k].nullify() - super(GeometryCollection, self).__del__() # Calling the parent __del__() method. - + "Overloaded deletion method for Geometry Collections." + #print '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 + # on entire geometry will result in an attempted deletion of NULL pointers for + # the missing components. + for k in self._geoms: + if self._geoms[k].valid: + lgeos.GEOSGeom_destroy(self._geoms[k].address) + self._geoms[k].nullify() + def __getitem__(self, index): "For indexing on the multiple geometries." + # Checking the index and returning the corresponding GEOS geometry. self._checkindex(index) - - # Setting an entry in the _geoms dictionary for the requested geometry. - if not index in self._geoms: - self._geoms[index] = GEOSPointer(lgeos.GEOSGetGeometryN(self._ptr(), c_int(index))) - - # Cloning the GEOS Geometry first, before returning it. - return GEOSGeometry(self._geoms[index], child=True) + return GEOSGeometry(self._geoms[index], parent=self._ptr) def __iter__(self): "For iteration on the multiple geometries." - for i in xrange(self.__len__()): + for i in xrange(len(self)): yield self.__getitem__(i) def __len__(self): @@ -40,7 +101,20 @@ class GeometryCollection(GEOSGeometry): if index < 0 or index >= self.num_geom: raise GEOSGeometryIndexError, 'invalid GEOS Geometry index: %s' % str(index) + 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): pass -class MultiLineString(GeometryCollection): pass -class MultiPolygon(GeometryCollection): pass +class MultiPoint(GeometryCollection): + _allowed = Point + _typeid = 4 +class MultiLineString(GeometryCollection): + _allowed = (LineString, LinearRing) + _typeid = 5 +class MultiPolygon(GeometryCollection): + _allowed = Polygon + _typeid = 6 diff --git a/django/contrib/gis/geos/coordseq.py b/django/contrib/gis/geos/coordseq.py index eba49f768b..c6f84ea4b2 100644 --- a/django/contrib/gis/geos/coordseq.py +++ b/django/contrib/gis/geos/coordseq.py @@ -1,6 +1,8 @@ -from django.contrib.gis.geos.libgeos import lgeos +from django.contrib.gis.geos.libgeos import lgeos, GEOSPointer, HAS_NUMPY from django.contrib.gis.geos.error import GEOSException, GEOSGeometryIndexError from ctypes import c_double, c_int, c_uint, byref +from types import ListType, TupleType +if HAS_NUMPY: from numpy import ndarray """ This module houses the GEOSCoordSeq object, and is used internally @@ -14,6 +16,8 @@ class GEOSCoordSeq(object): #### Python 'magic' routines #### def __init__(self, ptr, z=False): "Initializes from a GEOS pointer." + if not isinstance(ptr, GEOSPointer): + raise TypeError, 'Coordinate sequence should initialize with a GEOSPointer.' self._ptr = ptr self._z = z @@ -39,6 +43,14 @@ class GEOSCoordSeq(object): def __setitem__(self, index, value): "Can use the index [] operator to set coordinate sequence at an index." + # Checking the input value + if isinstance(value, (ListType, TupleType)): + pass + elif HAS_NUMPY and isinstance(value, ndarray): + pass + else: + raise TypeError, 'Must set coordinate with a sequence (list, tuple, or numpy array).' + # Checking the dims of the input if self.dims == 3 and self._z: n_args = 3 set_3d = True @@ -47,6 +59,7 @@ class GEOSCoordSeq(object): set_3d = False if len(value) != n_args: raise TypeError, 'Dimension of value does not match.' + # Setting the X, Y, Z self.setX(index, value[0]) self.setY(index, value[1]) if set_3d: self.setZ(index, value[2]) @@ -73,11 +86,11 @@ class GEOSCoordSeq(object): dim = c_uint(dimension) idx = c_uint(index) - # 'd' is the value of the point, passed in by reference + # 'd' is the value of the ordinate, passed in by reference d = c_double() - status = lgeos.GEOSCoordSeq_getOrdinate(self._ptr(), idx, dim, byref(d)) + status = lgeos.GEOSCoordSeq_getOrdinate(self._ptr.coordseq(), idx, dim, byref(d)) if status == 0: - raise GEOSException, 'could not retrieve %th ordinate at index: %s' % (str(dimension), str(index)) + raise GEOSException, 'could not retrieve %sth ordinate at index: %s' % (dimension, index) return d.value def setOrdinate(self, dimension, index, value): @@ -90,7 +103,7 @@ class GEOSCoordSeq(object): idx = c_uint(index) # Setting the ordinate - status = lgeos.GEOSCoordSeq_setOrdinate(self._ptr(), idx, dim, c_double(value)) + status = lgeos.GEOSCoordSeq_setOrdinate(self._ptr.coordseq(), idx, dim, c_double(value)) if status == 0: raise GEOSException, 'Could not set the ordinate for (dim, index): (%d, %d)' % (dimension, index) @@ -123,7 +136,7 @@ class GEOSCoordSeq(object): def size(self): "Returns the size of this coordinate sequence." n = c_uint(0) - status = lgeos.GEOSCoordSeq_getSize(self._ptr(), byref(n)) + status = lgeos.GEOSCoordSeq_getSize(self._ptr.coordseq(), byref(n)) if status == 0: raise GEOSException, 'Could not get CoordSeq size.' return n.value @@ -132,7 +145,7 @@ class GEOSCoordSeq(object): def dims(self): "Returns the dimensions of this coordinate sequence." n = c_uint(0) - status = lgeos.GEOSCoordSeq_getDimensions(self._ptr(), byref(n)) + status = lgeos.GEOSCoordSeq_getDimensions(self._ptr.coordseq(), byref(n)) if status == 0: raise GEOSException, 'Could not get CoordSeq dimensions.' return n.value @@ -146,7 +159,7 @@ class GEOSCoordSeq(object): @property def clone(self): "Clones this coordinate sequence." - pass + return GEOSCoordSeq(GEOSPointer(0, lgeos.GEOSCoordSeq_clone(self._ptr.coordseq())), self.hasz) @property def tuple(self): diff --git a/django/contrib/gis/geos/geometries.py b/django/contrib/gis/geos/geometries.py index 449a79becb..cdd3d49922 100644 --- a/django/contrib/gis/geos/geometries.py +++ b/django/contrib/gis/geos/geometries.py @@ -1,12 +1,16 @@ -from ctypes import c_double, c_int, c_uint +""" + This module houses the Point, LineString, LinearRing, and Polygon OGC + geometry classes. All geometry classes in this module inherit from + GEOSGeometry. +""" + +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, HAS_NUMPY +from django.contrib.gis.geos.libgeos import lgeos, GEOSPointer, get_pointer_arr, init_from_geom, GEOM_PTR, HAS_NUMPY from django.contrib.gis.geos.base import GEOSGeometry -from django.contrib.gis.geos.error import GEOSException - -if HAS_NUMPY: - from numpy import ndarray, array +from django.contrib.gis.geos.error import GEOSException, GEOSGeometryIndexError +if HAS_NUMPY: from numpy import ndarray, array class Point(GEOSGeometry): @@ -17,6 +21,8 @@ class Point(GEOSGeometry): >>> p = Point(5, 23, 8) # 3D point, passed in with individual parameters """ + self._ptr = GEOSPointer(0) # Initially NULL + if isinstance(x, (TupleType, ListType)): # Here a tuple or list was passed in under the ``x`` parameter. ndim = len(x) @@ -52,14 +58,17 @@ class Point(GEOSGeometry): # Initializing from the geometry, and getting a Python object super(Point, self).__init__(lgeos.GEOSGeom_createPoint(cs)) + def __len__(self): + "Returns the number of dimensions for this Point (either 2 or 3)." + if self.hasz: return 3 + else: return 2 + def _getOrdinate(self, dim, idx): "The coordinate sequence getOrdinate() wrapper." - self._cache_cs() return self._cs.getOrdinate(dim, idx) def _setOrdinate(self, dim, idx, value): "The coordinate sequence setOrdinate() wrapper." - self._cache_cs() self._cs.setOrdinate(dim, idx, value) def get_x(self): @@ -100,38 +109,50 @@ class Point(GEOSGeometry): ### Tuple setting and retrieval routines. ### def get_tuple(self): "Returns a tuple of the point." - self._cache_cs() return self._cs.tuple - def set_tuple(self): - "Sets the tuple for this point object." - pass - + def set_tuple(self, tup): + "Sets the coordinates of the point with the given tuple." + self._cs[0] = tup + # The tuple property tuple = property(get_tuple, set_tuple) class LineString(GEOSGeometry): #### Python 'magic' routines #### - def __init__(self, coords, ring=False): - """Initializes on the given sequence, may take lists, tuples, or NumPy arrays - of X,Y pairs.""" + def __init__(self, *args, **kwargs): + """Initializes on the given sequence -- may take lists, tuples, NumPy arrays + of X,Y pairs, or Point objects. If Point objects are used, ownership is + _not_ transferred to the LineString object. + + Examples: + ls = LineString((1, 1), (2, 2)) + ls = LineString([(1, 1), (2, 2)]) + ls = LineString(array([(1, 1), (2, 2)])) + ls = LineString(Point(1, 1), Point(2, 2)) + """ + self._ptr = GEOSPointer(0) # Initially NULL + + # If only one argument was provided, then set the coords array appropriately + if len(args) == 1: coords = args[0] + else: coords = args if isinstance(coords, (TupleType, ListType)): + # Getting the number of coords and the number of dimensions -- which + # must stay the same, e.g., no LineString((1, 2), (1, 2, 3)). ncoords = len(coords) - first = True - for coord in coords: - if not isinstance(coord, (TupleType, ListType)): + if coords: ndim = len(coords[0]) + else: raise TypeError, 'Cannot initialize on empty sequence.' + self._checkdim(ndim) + # Incrementing through each of the coordinates and verifying + for i in xrange(1, ncoords): + if not isinstance(coords[i], (TupleType, ListType, Point)): raise TypeError, 'each coordinate should be a sequence (list or tuple)' - if first: - ndim = len(coord) - self._checkdim(ndim) - first = False - else: - if len(coord) != ndim: raise TypeError, 'Dimension mismatch.' + if len(coords[i]) != ndim: raise TypeError, 'Dimension mismatch.' numpy_coords = False elif HAS_NUMPY and isinstance(coords, ndarray): - shape = coords.shape + shape = coords.shape # Using numpy's shape. if len(shape) != 2: raise TypeError, 'Too many dimensions.' self._checkdim(shape[1]) ncoords = shape[0] @@ -141,40 +162,38 @@ class LineString(GEOSGeometry): raise TypeError, 'Invalid initialization input for LineStrings.' # Creating the coordinate sequence - cs = GEOSCoordSeq(GEOSPointer(create_cs(c_uint(ncoords), c_uint(ndim)))) + cs = GEOSCoordSeq(GEOSPointer(0, create_cs(c_uint(ncoords), c_uint(ndim)))) # Setting each point in the coordinate sequence for i in xrange(ncoords): if numpy_coords: cs[i] = coords[i,:] + elif isinstance(coords[i], Point): cs[i] = coords[i].tuple else: cs[i] = coords[i] # Getting the initialization function - if ring: + if kwargs.get('ring', False): func = lgeos.GEOSGeom_createLinearRing else: func = lgeos.GEOSGeom_createLineString # Calling the base geometry initialization with the returned pointer from the function. - super(LineString, self).__init__(func(cs._ptr())) + super(LineString, self).__init__(func(cs._ptr.coordseq())) def __getitem__(self, index): "Gets the point at the specified index." - self._cache_cs() return self._cs[index] def __setitem__(self, index, value): "Sets the point at the specified index, e.g., line_str[0] = (1, 2)." - self._cache_cs() self._cs[index] = value def __iter__(self): "Allows iteration over this LineString." for i in xrange(self.__len__()): - yield self.__getitem__(index) + yield self.__getitem__(i) def __len__(self): "Returns the number of points in this LineString." - self._cache_cs() return len(self._cs) def _checkdim(self, dim): @@ -184,55 +203,103 @@ class LineString(GEOSGeometry): @property def tuple(self): "Returns a tuple version of the geometry from the coordinate sequence." - self._cache_cs() return self._cs.tuple def _listarr(self, func): """Internal routine that returns a sequence (list) corresponding with the given function. Will return a numpy array if possible.""" - lst = [func(i) for i in xrange(self.__len__())] # constructing the list, using the function + lst = [func(i) for i in xrange(len(self))] # constructing the list, using the function if HAS_NUMPY: return array(lst) # ARRRR! else: return lst @property def array(self): "Returns a numpy array for the LineString." - self._cache_cs() return self._listarr(self._cs.__getitem__) @property def x(self): "Returns a list or numpy array of the X variable." - self._cache_cs() return self._listarr(self._cs.getX) @property def y(self): "Returns a list or numpy array of the Y variable." - self._cache_cs() return self._listarr(self._cs.getY) @property def z(self): "Returns a list or numpy array of the Z variable." - self._cache_cs() if not self.hasz: return None else: return self._listarr(self._cs.getZ) # LinearRings are LineStrings used within Polygons. class LinearRing(LineString): - def __init__(self, coords): + def __init__(self, *args): "Overriding the initialization function to set the ring keyword." - super(LinearRing, self).__init__(coords, ring=True) + kwargs = {'ring' : True} + super(LinearRing, self).__init__(*args, **kwargs) class Polygon(GEOSGeometry): + def __init__(self, *args): + """Initializes on an exterior ring and a sequence of holes (both instances of LinearRings. + All LinearRing instances used for creation will become owned by this Polygon. + + Examples, where shell, hole1, and hole2 are valid LinearRing geometries: + poly = Polygon(shell, hole1, hole2) + poly = Polygon(shell, (hole1, hole2)) + """ + self._ptr = GEOSPointer(0) # Initially NULL + self._rings = {} + if not args: + raise TypeError, 'Must provide at list one LinearRing instance to initialize Polygon.' + + # Getting the ext_ring and init_holes parameters from the argument list + ext_ring = args[0] + init_holes = args[1:] + if len(init_holes) == 1 and isinstance(init_holes[0], (TupleType, ListType)): + init_holes = init_holes[0] + + # Ensuring the exterior ring parameter is a LinearRing object + if not isinstance(ext_ring, LinearRing): + raise TypeError, 'First argument for Polygon initialization must be a LinearRing.' + + # Making sure all of the holes are LinearRing objects + if False in [isinstance(hole, LinearRing) for hole in init_holes]: + raise TypeError, 'Holes parameter must be a sequence of LinearRings.' + + # Getting the holes + nholes = len(init_holes) + 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) + + # Getting the shell pointer address, + shell = init_from_geom(ext_ring) + + # Calling with the GEOS createPolygon factory. + super(Polygon, self).__init__(lgeos.GEOSGeom_createPolygon(shell, byref(holes), c_uint(nholes))) + def __del__(self): - "Override the GEOSGeometry delete routine to safely take care of any spawned rings." - # Nullifying the pointers to internal rings, preventing any attempted future access - for k in self._rings: self._rings[k].nullify() - super(Polygon, self).__del__() # Calling the parent __del__() method. - + "Overloaded deletion method for Polygons." + #print '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: + # 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. + for k in self._rings: + if self._rings[k].valid: + lgeos.GEOSGeom_destroy(self._rings[k].address) + self._rings[k].nullify() + 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.""" @@ -247,13 +314,22 @@ class Polygon(GEOSGeometry): def __iter__(self): "Iterates over each ring in the polygon." - for i in xrange(self.__len__()): + for i in xrange(len(self)): yield self.__getitem__(i) def __len__(self): "Returns the number of rings in this Polygon." return self.num_interior_rings + 1 + def _populate(self): + "Populates the internal rings dictionary." + # Getting the exterior ring first for the 0th index. + self._rings = {0 : GEOSPointer(lgeos.GEOSGetExteriorRing(self._ptr()))} + + # Getting the interior rings. + for i in xrange(self.num_interior_rings): + self._rings[i+1] = GEOSPointer(lgeos.GEOSGetInteriorRingN(self._ptr(), c_int(i))) + 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.""" @@ -262,13 +338,9 @@ class Polygon(GEOSGeometry): if ring_i < 0 or ring_i >= self.num_interior_rings: raise IndexError, 'ring index out of range' - # Placing the ring in internal rings dictionary. - idx = ring_i+1 # the index for the polygon is +1 because of the exterior ring - if not idx in self._rings: - self._rings[idx] = GEOSPointer(lgeos.GEOSGetInteriorRingN(self._ptr(), c_int(ring_i))) - - # Returning the ring at the given index. - return GEOSGeometry(self._rings[idx], child=True) + # Returning the ring from the internal ring dictionary (have to + # add one to the index) + return GEOSGeometry(self._rings[ring_i+1], parent=self._ptr) #### Polygon Properties #### @property @@ -282,17 +354,18 @@ class Polygon(GEOSGeometry): if n == -1: raise GEOSException, 'Error getting the number of interior rings.' else: return n - @property - def exterior_ring(self): + def get_ext_ring(self): "Gets the exterior ring of the Polygon." - # Returns exterior ring - self._rings[0] = GEOSPointer(lgeos.GEOSGetExteriorRing((self._ptr()))) - return GEOSGeometry(self._rings[0], child=True) + return GEOSGeometry(self._rings[0], parent=self._ptr) - @property - def shell(self): - "Gets the shell (exterior ring) of the Polygon." - return self.exterior_ring + def set_ext_ring(self): + "Sets the exterior ring of the Polygon." + # Sets the exterior ring + raise NotImplementedError + + # properties for the exterior ring/shell + exterior_ring = property(get_ext_ring) + shell = property(get_ext_ring) @property def tuple(self): diff --git a/django/contrib/gis/geos/libgeos.py b/django/contrib/gis/geos/libgeos.py index f0d8642e59..141d3fa55c 100644 --- a/django/contrib/gis/geos/libgeos.py +++ b/django/contrib/gis/geos/libgeos.py @@ -5,9 +5,7 @@ """ from django.contrib.gis.geos.error import GEOSException -from ctypes import \ - c_char_p, c_int, c_size_t, c_ubyte, pointer, addressof, \ - CDLL, CFUNCTYPE, POINTER, Structure +from ctypes import c_char_p, c_int, pointer, CDLL, CFUNCTYPE, POINTER, Structure import os, sys # NumPy supported? @@ -64,29 +62,55 @@ 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. #### + +# 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 to accessing the values returned by - GEOS geometry creation routines.""" + """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, + which allows parent geometries to be 'nullified' if their children's memory is used + in construction of another geometry. Related coordinate sequence pointers are kept + in this object for the same reason.""" ### Python 'magic' routines ### - def __init__(self, ptr): - "Given a ctypes pointer(c_int)" - if isinstance(ptr, int): - self._ptr = pointer(c_int(ptr)) + def __init__(self, address, coordseq=0): + "Initializes on an address (an integer)." + if isinstance(address, int): + self._geom = pointer(c_int(address)) + self._coordseq = pointer(c_int(coordseq)) else: raise TypeError, 'GEOSPointer object must initialize with an integer.' def __call__(self): """If the pointer is NULL, then an exception will be raised, otherwise the - address value (a GEOSGeom_ptr) will be returned.""" + address value (an integer) will be returned.""" if self.valid: return self.address - else: raise GEOSException, 'GEOS pointer no longer valid (was the parent geometry deleted?)' + else: raise GEOSException, 'GEOS pointer no longer valid (was this geometry or the parent geometry deleted or modified?)' + + def __bool__(self): + "Returns True when the GEOSPointer is valid." + return self.valid + + def __str__(self): + return str(self.address) ### GEOSPointer Properties ### @property def address(self): "Returns the address of the GEOSPointer (represented as an integer)." - return self._ptr.contents.value + return self._geom.contents.value @property def valid(self): @@ -94,13 +118,55 @@ class GEOSPointer(object): if bool(self.address): return True else: return False + ### Coordinate Sequence properties ### + def coordseq(self): + "If the coordinate sequence pointer is NULL (0), an exception will be raised." + if self.coordseq_valid: return self.coordseq_address + else: raise GEOSException, 'GEOS coordinate sequence pointer invalid (was this geometry or the parent geometry deleted or modified?)' + + @property + def coordseq_address(self): + "Returns the address of the related coordinate sequence." + return self._coordseq.contents.value + + @property + def coordseq_valid(self): + "Returns True if the coordinate sequence address is valid, False otherwise." + if bool(self.coordseq_address): return True + else: return False + ### GEOSPointer Methods ### - def set(self, address): + def set(self, address, coordseq=False): "Sets this pointer with the new address (represented as an integer)" if not isinstance(address, int): raise TypeError, 'GEOSPointer must be set with an address (an integer).' - self._ptr.contents = c_int(address) + if coordseq: + self._coordseq.contents = c_int(address) + else: + self._geom.contents = c_int(address) def nullify(self): - "Nullify this geometry pointer (set the address to 0)." + """Nullify this geometry pointer (set the address to 0). This does not delete + any memory, rather, it sets the GEOS pointer to a NULL address, to prevent + access to addressses of deleted objects.""" + # 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/geometries.py b/django/contrib/gis/tests/geometries.py index 40b73e4737..7b5a2cce8f 100644 --- a/django/contrib/gis/tests/geometries.py +++ b/django/contrib/gis/tests/geometries.py @@ -109,17 +109,26 @@ multilinestrings = (TestGeom('MULTILINESTRING ((0 0, 0 100), (100 0, 100 100))', topology_geoms = ( (TestGeom('POLYGON ((-5.0 0.0, -5.0 10.0, 5.0 10.0, 5.0 0.0, -5.0 0.0))'), TestGeom('POLYGON ((0.0 -5.0, 0.0 5.0, 10.0 5.0, 10.0 -5.0, 0.0 -5.0))') ), + (TestGeom('POLYGON ((2 0, 18 0, 18 15, 2 15, 2 0))'), + 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))'), + ), ) 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))') ) 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))'), ) 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))'), ) +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))'), + ) relate_geoms = ( (TestGeom('MULTIPOINT(80 70, 20 20, 200 170, 140 120)'), TestGeom('MULTIPOINT(80 170, 140 120, 200 80, 80 70)'), diff --git a/django/contrib/gis/tests/test_geos.py b/django/contrib/gis/tests/test_geos.py index 103c789f55..5999b61409 100644 --- a/django/contrib/gis/tests/test_geos.py +++ b/django/contrib/gis/tests/test_geos.py @@ -1,9 +1,12 @@ import unittest -from django.contrib.gis.geos import GEOSGeometry, GEOSException, Point, LineString, LinearRing, HAS_NUMPY +from django.contrib.gis.geos import \ + GEOSException, GEOSGeometryIndexError, \ + GEOSGeometry, Point, LineString, LinearRing, Polygon, \ + MultiPoint, MultiLineString, MultiPolygon, GeometryCollection, \ + fromstr, HAS_NUMPY from geometries import * -if HAS_NUMPY: - from numpy import array +if HAS_NUMPY: from numpy import array class GEOSTest(unittest.TestCase): @@ -52,10 +55,12 @@ class GEOSTest(unittest.TestCase): self.assertEqual(p.z, pnt.z) self.assertEqual(p.z, pnt.tuple[2], 9) tup_args = (p.x, p.y, p.z) + set_tup = (2.71, 3.14, 5.23) else: self.assertEqual(False, pnt.hasz) self.assertEqual(None, pnt.z) tup_args = (p.x, p.y) + set_tup = (2.71, 3.14) # Centroid operation on point should be point itself self.assertEqual(p.centroid, pnt.centroid.tuple) @@ -71,6 +76,11 @@ class GEOSTest(unittest.TestCase): pnt.x = 2.71 self.assertEqual(3.14, pnt.y) self.assertEqual(2.71, pnt.x) + + # Setting via the tuple property + pnt.tuple = set_tup + self.assertEqual(set_tup, pnt.tuple) + prev = pnt # setting the previous geometry def test02b_multipoints(self): @@ -83,6 +93,7 @@ class GEOSTest(unittest.TestCase): self.assertAlmostEqual(mp.centroid[0], mpnt.centroid.tuple[0], 9) self.assertAlmostEqual(mp.centroid[1], mpnt.centroid.tuple[1], 9) + self.assertRaises(GEOSGeometryIndexError, mpnt.__getitem__, len(mpnt)) self.assertEqual(mp.centroid, mpnt.centroid.tuple) self.assertEqual(mp.points, tuple(m.tuple for m in mpnt)) for p in mpnt: @@ -107,17 +118,15 @@ class GEOSTest(unittest.TestCase): self.assertEqual(True, ls == GEOSGeometry(l.wkt)) self.assertEqual(False, ls == prev) - + self.assertRaises(GEOSGeometryIndexError, ls.__getitem__, len(ls)) prev = ls # Creating a LineString from a tuple, list, and numpy array - ls2 = LineString(ls.tuple) - self.assertEqual(ls, ls2) - ls3 = LineString([list(tup) for tup in ls.tuple]) - self.assertEqual(ls, ls3) - if HAS_NUMPY: - ls4 = LineString(array(ls.tuple)) - self.assertEqual(ls, ls4) + self.assertEqual(ls, LineString(ls.tuple)) # tuple + self.assertEqual(ls, LineString(*ls.tuple)) # as individual arguments + self.assertEqual(ls, LineString([list(tup) for tup in ls.tuple])) # as list + self.assertEqual(ls.wkt, LineString(*tuple(Point(tup) for tup in ls.tuple)).wkt) # Point individual arguments + if HAS_NUMPY: self.assertEqual(ls, LineString(array(ls.tuple))) # as numpy array def test03b_multilinestring(self): "Testing MultiLineString objects." @@ -139,7 +148,11 @@ class GEOSTest(unittest.TestCase): self.assertEqual(ls.geom_typeid, 1) self.assertEqual(ls.empty, False) - def test04a_linearring(self): + self.assertRaises(GEOSGeometryIndexError, ml.__getitem__, len(ml)) + self.assertEqual(ml.wkt, MultiLineString(*tuple(s.clone() for s in ml)).wkt) + self.assertEqual(ml, MultiLineString(*tuple(LineString(s.tuple) for s in ml))) + + def test04_linearring(self): "Testing LinearRing objects." for rr in linearrings: lr = GEOSGeometry(rr.wkt) @@ -150,13 +163,10 @@ class GEOSTest(unittest.TestCase): self.assertEqual(False, lr.empty) # Creating a LinearRing from a tuple, list, and numpy array - lr2 = LinearRing(lr.tuple) - self.assertEqual(lr, lr2) - lr3 = LinearRing([list(tup) for tup in lr.tuple]) - self.assertEqual(lr, lr3) - if HAS_NUMPY: - lr4 = LineString(array(lr.tuple)) - self.assertEqual(lr, lr4) + 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))) def test05a_polygons(self): "Testing Polygon objects." @@ -180,6 +190,7 @@ class GEOSTest(unittest.TestCase): # Testing the geometry equivalence self.assertEqual(True, poly == GEOSGeometry(p.wkt)) self.assertEqual(False, poly == prev) # Should not be equal to previous geometry + self.assertEqual(True, poly != prev) # Testing the exterior ring ring = poly.exterior_ring @@ -194,6 +205,15 @@ class GEOSTest(unittest.TestCase): self.assertEqual(ring.geom_type, 'LinearRing') self.assertEqual(ring.geom_typeid, 2) + # Testing polygon construction. + self.assertRaises(TypeError, Polygon, 0, [1, 2, 3]) + self.assertRaises(TypeError, Polygon, 'foo') + + rings = tuple(r.clone() for r in poly) + self.assertEqual(poly, Polygon(rings[0], rings[1:])) + self.assertEqual(poly.wkt, Polygon(*tuple(r.clone() for r in poly)).wkt) + self.assertEqual(poly.wkt, Polygon(*tuple(LinearRing(r.tuple) for r in poly)).wkt) + # Setting the second point of the first ring (which should set the # first point of the polygon). prev = poly.clone() # Using clone() to get a copy of the current polygon @@ -221,10 +241,13 @@ class GEOSTest(unittest.TestCase): self.assertEqual(mp.num_geom, mpoly.num_geom) self.assertEqual(mp.n_p, mpoly.num_coords) self.assertEqual(mp.num_geom, len(mpoly)) + self.assertRaises(GEOSGeometryIndexError, mpoly.__getitem__, len(mpoly)) for p in mpoly: self.assertEqual(p.geom_type, 'Polygon') self.assertEqual(p.geom_typeid, 3) self.assertEqual(p.valid, True) + self.assertEqual(mpoly.wkt, MultiPolygon(*tuple(poly.clone() for poly in mpoly)).wkt) + print "\nEND - expecting GEOS_NOTICE; safe to ignore.\n" def test06_memory_hijinks(self): @@ -254,7 +277,7 @@ class GEOSTest(unittest.TestCase): self.assertRaises(GEOSException, str, ring2) #### Memory issues with geometries from Geometry Collections - mp = GEOSGeometry('MULTIPOINT(85 715, 235 1400, 4620 1711)') + mp = fromstr('MULTIPOINT(85 715, 235 1400, 4620 1711)') # Getting the points pts = [p for p in mp] @@ -278,7 +301,82 @@ class GEOSTest(unittest.TestCase): # after it has been deleted. del mp for p in pts: - self.assertRaises(GEOSException, str, p) + self.assertRaises(GEOSException, str, p) # tests p's geometry pointer + self.assertRaises(GEOSException, p.get_tuple) # tests p's coordseq pointer + + # Now doing this with a GeometryCollection + polywkt = polygons[3].wkt # a 'real life' polygon. + lrwkt = linearrings[0].wkt # a 'real life' linear ring + poly = fromstr(polywkt) + linring = fromstr(lrwkt) + + # Pulling out the shell and cloning our initial geometries for later comparison. + shell = poly.shell + polyc = poly.clone() + linringc = linring.clone() + + gc = GeometryCollection(poly, linring, Point(5, 23)) + + # Should no longer be able to access these variables + self.assertRaises(GEOSException, str, poly) + self.assertRaises(GEOSException, str, shell) + self.assertRaises(GEOSException, str, linring) + + r1 = gc[1] # pulling out the ring + pnt = gc[2] # pulling the point from the geometry collection + + # Now lets create a MultiPolygon from the geometry collection components + mpoly = MultiPolygon(gc[0], Polygon(gc[1])) + self.assertEqual(polyc.wkt, mpoly[0].wkt) + + # Should no longer be able to access the geometry collection directly + self.assertRaises(GEOSException, len, gc) + + # BUT, should still be able to access the Point we obtained earlier, but + # not the linear ring (since it is now part of the MultiPolygon. + self.assertEqual(5, pnt.x) + self.assertEqual(23, pnt.y) + + # __len__ is called on the coordinate sequence pointer -- make sure its nullified as well + self.assertRaises(GEOSException, len, r1) + self.assertRaises(GEOSException, str, r1) + + # Can't access point after deletion of parent geometry. + del gc + self.assertRaises(GEOSException, str, pnt) + + # Cleaning up. + del polyc + del mpoly + + #### Memory issues with creating geometries from coordinate sequences within other geometries + + # Creating the initial polygon from the following tuples, and then pulling out + # the individual rings. + ext_tup = ((0, 0), (0, 7), (7, 7), (7, 0), (0, 0)) + itup1 = ((1, 1), (1, 2), (2, 2), (2, 1), (1, 1)) + itup2 = ((4, 4), (4, 5), (5, 5), (5, 4), (4, 4)) + poly1 = Polygon(LinearRing(ext_tup), LinearRing(itup1), LinearRing(itup2)) + shell = poly1.shell + hole1 = poly1[1] + hole2 = poly1[2] + + # Creating a Polygon from the shell and one of the holes + poly2 = Polygon(shell, hole1) + + # We should no longer be able to access the original Polygon, its + # shell or its first internal ring. + self.assertRaises(GEOSException, str, poly1) + self.assertRaises(GEOSException, str, shell) + self.assertRaises(GEOSException, str, hole1) + + # BUT, the second hole is still accessible. + self.assertEqual(itup2, hole2.tuple) + + # Deleting the first polygon, and ensuring that + # the second hole is now gone for good. + del poly1 + self.assertRaises(GEOSException, str, hole2) def test08_coord_seq(self): "Testing Coordinate Sequence objects." @@ -309,7 +407,6 @@ class GEOSTest(unittest.TestCase): def test09_relate_pattern(self): "Testing relate() and relate_pattern()." - g = GEOSGeometry('POINT (0 0)') self.assertRaises(GEOSException, g.relate_pattern, 0, 'invalid pattern, yo') @@ -333,6 +430,7 @@ class GEOSTest(unittest.TestCase): self.assertEqual(True, a.intersects(b)) i2 = a.intersection(b) self.assertEqual(i1, i2) + self.assertEqual(i1, a & b) def test11_union(self): "Testing union()." @@ -343,6 +441,7 @@ 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 def test12_difference(self): "Testing difference()." @@ -353,8 +452,20 @@ 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 - def test13_buffer(self): + def test13_symdifference(self): + "Testing sym_difference()." + for i in xrange(len(topology_geoms)): + g_tup = topology_geoms[i] + a = GEOSGeometry(g_tup[0].wkt) + b = GEOSGeometry(g_tup[1].wkt) + d1 = GEOSGeometry(sdiff_geoms[i].wkt) + d2 = a.sym_difference(b) + self.assertEqual(d1, d2) + self.assertEqual(d1, a ^ b) # Symmetric difference ('^') operator + + def test14_buffer(self): "Testing buffer()." for i in xrange(len(buffer_geoms)): g_tup = buffer_geoms[i] diff --git a/django/contrib/gis/utils/LayerMapping.py b/django/contrib/gis/utils/LayerMapping.py index 8c93590ae2..f5d3758860 100644 --- a/django/contrib/gis/utils/LayerMapping.py +++ b/django/contrib/gis/utils/LayerMapping.py @@ -228,7 +228,7 @@ class LayerMapping: "A class that maps OGR Layers to Django Models." def __init__(self, model, data, mapping, layer=0, source_srs=None): - "Takes the Django model, the mapping (dictionary), and the SHP file." + "Takes the Django model, the data source, and the mapping (dictionary)" # Getting the field names and types from the model fields = dict((f.name, map_foreign_key(f)) for f in model._meta.fields)