diff --git a/django/contrib/gis/geos/__init__.py b/django/contrib/gis/geos/__init__.py index 67eda8de26..ca9691e243 100644 --- a/django/contrib/gis/geos/__init__.py +++ b/django/contrib/gis/geos/__init__.py @@ -33,6 +33,7 @@ from django.contrib.gis.geos.base import GEOSGeometry from django.contrib.gis.geos.geometries import Point, LineString, LinearRing, Polygon, HAS_NUMPY from django.contrib.gis.geos.collections import GeometryCollection, MultiPoint, MultiLineString, MultiPolygon from django.contrib.gis.geos.error import GEOSException, GEOSGeometryIndexError +from django.contrib.gis.geos.libgeos import geos_version def fromstr(wkt_or_hex, **kwargs): "Given a string value (wkt or hex), returns a GEOSGeometry object." diff --git a/django/contrib/gis/geos/base.py b/django/contrib/gis/geos/base.py index 4302fd0e45..e36db67ae0 100644 --- a/django/contrib/gis/geos/base.py +++ b/django/contrib/gis/geos/base.py @@ -1,8 +1,7 @@ """ This module contains the 'base' GEOSGeometry object -- all GEOS geometries - inherit from this object. + inherit from this object. """ - # ctypes and types dependencies. from ctypes import \ byref, string_at, create_string_buffer, pointer, \ @@ -11,11 +10,10 @@ from types import StringType, UnicodeType, 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.error import GEOSException, GEOSGeometryIndexError from django.contrib.gis.geos.coordseq import GEOSCoordSeq, create_cs -if HAS_NUMPY: from numpy import ndarray, array +from django.contrib.gis.geos.error import GEOSException, GEOSGeometryIndexError +from django.contrib.gis.geos.libgeos import lgeos, HAS_NUMPY, ISQLQuote +from django.contrib.gis.geos.pointer import GEOSPointer, NULL_GEOM # Regular expression for recognizing HEXEWKB and WKT. A prophylactic measure # to prevent potentially malicious input from reaching the underlying C @@ -26,28 +24,23 @@ wkt_regex = re.compile(r'^(POINT|LINESTRING|LINEARRING|POLYGON|MULTIPOINT|MULTIL class GEOSGeometry(object): "A class that, generally, encapsulates a GEOS geometry." + # Initially, all geometries use a NULL pointer. + _ptr = NULL_GEOM + #### Python 'magic' routines #### - def __init__(self, geo_input, input_type=False, parent=None, srid=None): - """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 `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. + def __init__(self, geo_input, srid=None): """ + The base constructor for GEOS geometry objects, and may take the following + string inputs: WKT and HEXEWKB (a PostGIS-specific canonical form). - # Initially, setting the pointer to NULL - self._ptr = GEOSPointer(0) + The `srid` keyword is used to specify the Source Reference Identifier + (SRID) number for this Geometry. If not set, the SRID will be None. + """ if isinstance(geo_input, UnicodeType): # Encoding to ASCII, WKT or HEXEWKB doesn't need any more. geo_input = geo_input.encode('ascii') - if isinstance(geo_input, StringType): - if input_type: warn('input_type keyword is deprecated') - if hex_regex.match(geo_input): # If the regex matches, the geometry is in HEX form. sz = c_size_t(len(geo_input)) @@ -58,47 +51,41 @@ class GEOSGeometry(object): g = lgeos.GEOSGeomFromWKT(c_char_p(geo_input)) else: raise GEOSException, 'given string input "%s" unrecognized as WKT or HEXEWKB.' % geo_input - elif isinstance(geo_input, (IntType, GEOSPointer)): - # When the input is either a raw pointer value (an integer), or a GEOSPointer object. + # When the input is either a memory address (an integer), or a + # GEOSPointer object. g = geo_input else: # Invalid geometry type. 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 input - if isinstance(g, GEOSPointer): self._ptr = g - else: self._ptr.set(g) # Otherwise, set with the address + # Setting the pointer object with a valid pointer. + self._ptr = GEOSPointer(g) else: raise GEOSException, 'Could not initialize GEOS Geometry with given input.' - # 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.). - if isinstance(parent, GEOSPointer): - self._parent = parent - else: - self._parent = GEOSPointer(0) - # Setting the SRID, if given. if srid and isinstance(srid, int): self.srid = srid # 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() + # Setting the coordinate sequence for the geometry (will be None on + # geometries that do not have coordinate sequences) + self._set_cs() - # Extra setup needed for Geometries that may be parents. + # _populate() needs to be called for parent Geometries. if isinstance(self, (Polygon, GeometryCollection)): self._populate() def __del__(self): - "Destroys this geometry -- only if the pointer is valid and whether or not it belongs to a parent." - #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()) + """ + Destroys this Geometry; in other words, frees the memory used by the + GEOS C++ object -- but only if the pointer is not a child Geometry + (e.g., don't delete the LinearRings spawned from a Polygon). + """ + #print 'base: Deleting %s (parent=%s, valid=%s)' % (self.__class__.__name__, self._ptr.parent, self._ptr.valid) + if not self._ptr.child: self._ptr.destroy() def __str__(self): "WKT is used for the string representation." @@ -156,29 +143,38 @@ class GEOSGeometry(object): # g1 ^= g2 def __ixor__(self, other): - "Reassigns this Geometry to the symmetric difference of this Geometry and the other." + """ + Reassigns this Geometry to the symmetric difference of this Geometry + and the other. + """ return self.sym_difference(other) + #### Internal GEOSPointer-related routines. #### 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.""" + """ + Returns the address of this Geometry, and nullifies any related pointers. + This function is called if this Geometry is used in the initialization + of another 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() - + if self._ptr.child: + p = self._ptr.parent + # If we have a grandchild (a LinearRing from a MultiPolygon or + # GeometryCollection), then nullify the collection as well. + if p.child: p.parent.nullify() + p.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." + "Reassigns the internal pointer to that of the 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) @@ -186,8 +182,8 @@ class GEOSGeometry(object): # 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 + if isinstance(new_geom, GEOSPointer): self._ptr = new_geom + else: self._ptr = GEOSPointer(new_geom._nullify()) # The new geometry class may be different from the original, so setting # the __class__ and populating the internal geometry or ring dictionary. @@ -200,10 +196,10 @@ class GEOSGeometry(object): if proto == ISQLQuote: return self else: - raise GEOSException, 'Error implementing psycopg2 protocol. Is psycopg2 installed?' + raise GEOSException, 'Error implementing psycopg2 protocol. Is psycopg2 installed?' def getquoted(self): - "Returns a properly quoted string for use in PostgresSQL/PostGIS." + "Returns a properly quoted string for use in PostgreSQL/PostGIS." # Using ST_GeomFromText(), corresponds to SQL/MM ISO standard. return "ST_GeomFromText('%s', %s)" % (self.wkt, self.srid or -1) @@ -217,10 +213,11 @@ class GEOSGeometry(object): else: return False - def _get_cs(self): - "Gets the coordinate sequence for this Geometry." + def _set_cs(self): + "Sets the coordinate sequence for this Geometry." if self.has_cs: - self._ptr.set(lgeos.GEOSGeom_getCoordSeq(self._ptr()), coordseq=True) + if not self._ptr.coordseq_valid: + self._ptr.set_coordseq(lgeos.GEOSGeom_getCoordSeq(self._ptr())) self._cs = GEOSCoordSeq(self._ptr, self.hasz) else: self._cs = None @@ -272,16 +269,20 @@ class GEOSGeometry(object): ## 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.""" + """ + Returns the result, or raises an exception for the given unary predicate + function. + """ val = func(self._ptr()) if val == 0: return False elif val == 1: return True else: raise GEOSException, '%s: exception occurred.' % func.__name__ def _binary_predicate(self, func, other, *args): - """Returns the result, or raises an exception for the given binary - predicate function.""" + """ + Returns the result, or raises an exception for the given binary + predicate function. + """ if not isinstance(other, GEOSGeometry): raise TypeError, 'Binary predicate operation ("%s") requires another GEOSGeometry instance.' % func.__name__ val = func(self._ptr(), other._ptr(), *args) @@ -292,7 +293,10 @@ 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 @@ -317,20 +321,26 @@ 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 Geometries match the elements in pattern.""" + """ + Returns true if the elements in the DE-9IM intersection matrix for 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 Geometries - 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 Geometries - 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): @@ -338,14 +348,18 @@ 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 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).""" + """ + 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 Geometries - 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): @@ -353,18 +367,24 @@ 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 Geometries - is T*T***T** (for two points or two surfaces) 1*T***T** (for two curves).""" + """ + 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 Geometries - 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 Geometries are exactly equal, up to a - specified tolerance.""" + """ + Returns true if the two Geometries are exactly equal, up to a + specified tolerance. + """ return self._binary_predicate(lgeos.GEOSEqualsExact, other, c_double(tolerance)) @@ -383,12 +403,12 @@ class GEOSGeometry(object): #### Output Routines #### @property def wkt(self): - "Returns the WKT of the Geometry." + "Returns the WKT (Well-Known Text) of the Geometry." return string_at(lgeos.GEOSGeomToWKT(self._ptr())) @property def hex(self): - "Returns the WKBHEX of the Geometry." + "Returns the HEXEWKB of the Geometry." sz = c_size_t() h = lgeos.GEOSGeomToHEX_buf(self._ptr(), byref(sz)) return string_at(h, sz.value) @@ -401,21 +421,26 @@ class GEOSGeometry(object): #### Topology Routines #### def _unary_topology(self, func, *args): - """Returns a GEOSGeometry for the given unary (takes only one Geomtery - as a paramter) topological operation.""" + """ + Returns a GEOSGeometry for the given unary (takes only one Geomtery + as a paramter) topological operation. + """ return GEOSGeometry(func(self._ptr(), *args), srid=self.srid) def _binary_topology(self, func, other, *args): - """Returns a GEOSGeometry for the given binary (takes two Geometries - as parameters) topological operation.""" + """ + Returns a GEOSGeometry for the given binary (takes two Geometries + as parameters) topological operation. + """ return GEOSGeometry(func(self._ptr(), other._ptr(), *args), srid=self.srid) def buffer(self, width, quadsegs=8): - """Returns a geometry that represents all points whose distance from this - Geometry is less than or equal to distance. Calculations are in the - Spatial Reference System of this Geometry. The optional third parameter sets - the number of segment used to approximate a quarter circle (defaults to 8). - (Text from PostGIS documentation at ch. 6.1.3) + """ + Returns a geometry that represents all points whose distance from this + Geometry is less than or equal to distance. Calculations are in the + Spatial Reference System of this Geometry. The optional third parameter sets + the number of segment used to approximate a quarter circle (defaults to 8). + (Text from PostGIS documentation at ch. 6.1.3) """ if not isinstance(width, (FloatType, IntType)): raise TypeError, 'width parameter must be a float' @@ -430,9 +455,11 @@ class GEOSGeometry(object): @property def centroid(self): - """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).""" + """ + 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) @property @@ -442,8 +469,10 @@ class GEOSGeometry(object): @property def convex_hull(self): - """Returns the smallest convex Polygon that contains all the points - in the Geometry.""" + """ + Returns the smallest convex Polygon that contains all the points + in the Geometry. + """ return self._unary_topology(lgeos.GEOSConvexHull) @property @@ -451,8 +480,16 @@ class GEOSGeometry(object): "Computes an interior point of this Geometry." return self._unary_topology(lgeos.GEOSPointOnSurface) + def simplify(self, tolerance=0.0): + """ + Returns the Geometry, simplified using the Douglas-Peucker algorithm + to the specified tolerance (higher tolerance => less points). If no + tolerance provided, defaults to 0. + """ + return self._unary_topology(lgeos.GEOSSimplify, c_double(tolerance)) + def relate(self, other): - "Returns the DE-9IM intersection matrix for this geometry and the other." + "Returns the DE-9IM intersection matrix for this Geometry and the other." return string_at(lgeos.GEOSRelate(self._ptr(), other._ptr())) def difference(self, other): @@ -461,8 +498,10 @@ class GEOSGeometry(object): return self._binary_topology(lgeos.GEOSDifference, other) def sym_difference(self, other): - """Returns a set combining the points in this Geometry not in other, - and the points in other not in this Geometry.""" + """ + Returns a set combining the points in this Geometry not in other, + and the points in other not in this Geometry. + """ return self._binary_topology(lgeos.GEOSSymDifference, other) def intersection(self, other): @@ -483,9 +522,11 @@ class GEOSGeometry(object): else: return a.value def distance(self, other): - """Returns the distance between the closest points on this Geometry - and the other. Units will be in those of the coordinate system. of - the Geometry.""" + """ + Returns the distance between the closest points on this Geometry + and the other. Units will be in those of the coordinate system of + the Geometry. + """ if not isinstance(other, GEOSGeometry): raise TypeError, 'distance() works only on other GEOS Geometries.' dist = c_double() @@ -495,8 +536,10 @@ class GEOSGeometry(object): @property def length(self): - """Returns the length of this Geometry (e.g., 0 for point, or the - circumfrence of a Polygon).""" + """ + Returns the length of this Geometry (e.g., 0 for point, or the + circumfrence of a Polygon). + """ l = c_double() status = lgeos.GEOSLength(self._ptr(), byref(l)) if status != 1: return None diff --git a/django/contrib/gis/geos/collections.py b/django/contrib/gis/geos/collections.py index bf5dd76158..2575e5b5df 100644 --- a/django/contrib/gis/geos/collections.py +++ b/django/contrib/gis/geos/collections.py @@ -1,13 +1,14 @@ """ - This module houses the Geometry Collection objects: - GeometryCollection, MultiPoint, MultiLineString, and MultiPolygon + This module houses the Geometry Collection objects: + GeometryCollection, MultiPoint, MultiLineString, and MultiPolygon """ from ctypes import c_int, c_uint, byref, cast from types import TupleType, ListType -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 +from django.contrib.gis.geos.libgeos import lgeos, get_pointer_arr, GEOM_PTR +from django.contrib.gis.geos.pointer import GEOSPointer class GeometryCollection(GEOSGeometry): _allowed = (Point, LineString, LinearRing, Polygon) @@ -15,16 +16,14 @@ class GeometryCollection(GEOSGeometry): 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 = None # Checking the arguments if not args: 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 len(args) == 1: + # If only one geometry provided or a list of geometries is provided + # in the first argument. if isinstance(args[0], (TupleType, ListType)): init_geoms = args[0] else: @@ -36,40 +35,47 @@ class GeometryCollection(GEOSGeometry): 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 + # Creating the geometry pointer array, and populating each element in + # the array with the address of the Geometry returned by _nullify(). ngeom = len(init_geoms) geoms = get_pointer_arr(ngeom) - - # Incrementing through each input geometry. for i in xrange(ngeom): 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) + # Calling the parent class, using the pointer returned from the + # GEOS createCollection() factory. + addr = lgeos.GEOSGeom_createCollection(c_int(self._typeid), + byref(geoms), c_uint(ngeom)) + super(GeometryCollection, self).__init__(addr, **kwargs) def __del__(self): "Overloaded deletion method for Geometry Collections." - #print 'collection: 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._ptr.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() + # Nullifying pointers to internal Geometries, preventing any + # attempted future access. + for g in self._ptr: g.nullify() 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() + # Internal memory has become part of other Geometry objects; must + # delete the internal objects which are still valid individually, + # because calling the destructor on the entire geometry will result + # in an attempted deletion of NULL pointers for the missing + # components (which may crash Python). + for g in self._ptr: + if len(g) > 0: + # The collection geometry is a Polygon, destroy any leftover + # LinearRings. + for r in g: r.destroy() + g.destroy() + super(GeometryCollection, self).__del__() def __getitem__(self, index): "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) + return GEOSGeometry(self._ptr[index], srid=self.srid) def __setitem__(self, index, geom): "Sets the Geometry at the specified index." @@ -105,14 +111,23 @@ class GeometryCollection(GEOSGeometry): 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() + for g in self._ptr: g.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))) + "Internal routine that populates the internal children geometries list." + ptr_list = [] + for i in xrange(len(self)): + # Getting the geometry pointer for the geometry at the index. + geom_ptr = lgeos.GEOSGetGeometryN(self._ptr(), c_int(i)) + + # Adding the coordinate sequence to the list, or using None if the + # collection Geometry doesn't support coordinate sequences. + if lgeos.GEOSGeomTypeId(geom_ptr) in (0, 1, 2): + ptr_list.append((geom_ptr, lgeos.GEOSGeom_getCoordSeq(geom_ptr))) + else: + ptr_list.append((geom_ptr, None)) + self._ptr.set_children(ptr_list) @property def kml(self): diff --git a/django/contrib/gis/geos/coordseq.py b/django/contrib/gis/geos/coordseq.py index 54a6abf009..33beac5548 100644 --- a/django/contrib/gis/geos/coordseq.py +++ b/django/contrib/gis/geos/coordseq.py @@ -1,15 +1,15 @@ -from django.contrib.gis.geos.libgeos import lgeos, GEOSPointer, HAS_NUMPY +""" + This module houses the GEOSCoordSeq object, and is used internally + by GEOSGeometry to house the actual coordinates of the Point, + LineString, and LinearRing geometries. +""" from django.contrib.gis.geos.error import GEOSException, GEOSGeometryIndexError +from django.contrib.gis.geos.libgeos import lgeos, HAS_NUMPY +from django.contrib.gis.geos.pointer import GEOSPointer 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 - by GEOSGeometry to house the actual coordinates of the Point, - LineString, and LinearRing geometries. -""" - class GEOSCoordSeq(object): "The internal representation of a list of coordinates inside a Geometry." @@ -31,18 +31,18 @@ class GEOSCoordSeq(object): return int(self.size) def __str__(self): - "The string representation of the coordinate sequence." + "Returns the string representation of the coordinate sequence." return str(self.tuple) def __getitem__(self, index): - "Can use the index [] operator to get coordinate sequence at an index." + "Returns the coordinate sequence value at the given index." coords = [self.getX(index), self.getY(index)] if self.dims == 3 and self._z: coords.append(self.getZ(index)) return tuple(coords) def __setitem__(self, index, value): - "Can use the index [] operator to set coordinate sequence at an index." + "Sets the coordinate sequence value at the given index." # Checking the input value if isinstance(value, (ListType, TupleType)): pass @@ -66,7 +66,7 @@ class GEOSCoordSeq(object): #### Internal Routines #### def _checkindex(self, index): - "Checks the index." + "Checks the given index." sz = self.size if (sz < 1) or (index < 0) or (index >= sz): raise GEOSGeometryIndexError, 'invalid GEOS Geometry index: %s' % str(index) @@ -78,7 +78,7 @@ class GEOSCoordSeq(object): #### Ordinate getting and setting routines #### def getOrdinate(self, dimension, index): - "Gets the value for the given dimension and index." + "Returns the value for the given dimension and index." self._checkindex(index) self._checkdim(dimension) @@ -152,7 +152,10 @@ class GEOSCoordSeq(object): @property def hasz(self): - "Inherits this from the parent geometry." + """ + Returns whether this coordinate sequence is 3D. This property value is + inherited from the parent Geometry. + """ return self._z ### Other Methods ### diff --git a/django/contrib/gis/geos/error.py b/django/contrib/gis/geos/error.py index 219a71929c..460db5e57b 100644 --- a/django/contrib/gis/geos/error.py +++ b/django/contrib/gis/geos/error.py @@ -1,15 +1,20 @@ +""" + This module houses the GEOS exceptions, specifically, GEOSException and + GEOSGeometryIndexError. +""" class GEOSException(Exception): "The base GEOS exception, indicates a GEOS-related error." pass class GEOSGeometryIndexError(GEOSException, KeyError): - """This exception is raised when an invalid index is encountered, and has + """ + This exception is raised when an invalid index is encountered, and has the 'silent_variable_feature' attribute set to true. This ensures that django's templates proceed to use the next lookup type gracefully when an Exception is raised. Fixes ticket #4740. """ # "If, during the method lookup, a method raises an exception, the exception - # will be propagated, unless the exception has an attribute silent_variable_failure - # whose value is True." -- django template docs. + # will be propagated, unless the exception has an attribute + # `silent_variable_failure` whose value is True." -- Django template docs. silent_variable_failure = True diff --git a/django/contrib/gis/geos/geometries.py b/django/contrib/gis/geos/geometries.py index 2a5b5df600..cc4bdc58ea 100644 --- a/django/contrib/gis/geos/geometries.py +++ b/django/contrib/gis/geos/geometries.py @@ -3,36 +3,33 @@ 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, get_pointer_arr, GEOM_PTR, HAS_NUMPY from django.contrib.gis.geos.base import GEOSGeometry +from django.contrib.gis.geos.coordseq import GEOSCoordSeq, create_cs +from django.contrib.gis.geos.libgeos import lgeos, get_pointer_arr, GEOM_PTR, HAS_NUMPY +from django.contrib.gis.geos.pointer import GEOSPointer from django.contrib.gis.geos.error import GEOSException, GEOSGeometryIndexError if HAS_NUMPY: from numpy import ndarray, array class Point(GEOSGeometry): def __init__(self, x, y=None, z=None, srid=None): - """The Point object may be initialized with either a tuple, or individual - parameters. For example: + """ + The Point object may be initialized with either a tuple, or individual + parameters. For example: >>> p = Point((5, 23)) # 2D point, passed in as a tuple >>> 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. + # Here a tuple or list was passed in under the `x` parameter. ndim = len(x) if ndim < 2 or ndim > 3: raise TypeError, 'Invalid sequence parameter: %s' % str(x) coords = x elif isinstance(x, (IntType, FloatType)) and isinstance(y, (IntType, FloatType)): - # Here X, Y, and (optionally) Z were passed in individually as parameters. + # Here X, Y, and (optionally) Z were passed in individually, as parameters. if isinstance(z, (IntType, FloatType)): ndim = 3 coords = [x, y, z] @@ -42,22 +39,17 @@ class Point(GEOSGeometry): else: raise TypeError, 'Invalid parameters given for Point initialization.' - # Creating the coordinate sequence + # Creating the coordinate sequence, and setting X, Y, [Z] cs = create_cs(c_uint(1), c_uint(ndim)) - - # Setting the X status = lgeos.GEOSCoordSeq_setX(cs, c_uint(0), c_double(coords[0])) if not status: raise GEOSException, 'Could not set X during Point initialization.' - - # Setting the Y status = lgeos.GEOSCoordSeq_setY(cs, c_uint(0), c_double(coords[1])) if not status: raise GEOSException, 'Could not set Y during Point initialization.' - - # Setting the Z if ndim == 3: status = lgeos.GEOSCoordSeq_setZ(cs, c_uint(0), c_double(coords[2])) - # Initializing from the geometry, and getting a Python object + # Initializing using the address returned from the GEOS + # createPoint factory. super(Point, self).__init__(lgeos.GEOSGeom_createPoint(cs), srid=srid) def __len__(self): @@ -125,9 +117,10 @@ class LineString(GEOSGeometry): #### Python 'magic' routines #### 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. + """ + 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)) @@ -135,11 +128,7 @@ 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 only one argument provided, set the coords array appropriately if len(args) == 1: coords = args[0] else: coords = args @@ -166,10 +155,9 @@ class LineString(GEOSGeometry): else: raise TypeError, 'Invalid initialization input for LineStrings.' - # Creating the coordinate sequence + # Creating a coordinate sequence object because it is easier to + # set the points using GEOSCoordSeq.__setitem__(). cs = GEOSCoordSeq(GEOSPointer(0, create_cs(c_uint(ncoords), c_uint(ndim))), z=bool(ndim==3)) - - # 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 @@ -184,7 +172,8 @@ class LineString(GEOSGeometry): # If SRID was passed in with the keyword arguments srid = kwargs.get('srid', None) - # Calling the base geometry initialization with the returned pointer from the function. + # Calling the base geometry initialization with the returned pointer + # from the function. super(LineString, self).__init__(func(cs._ptr.coordseq()), srid=srid) def __getitem__(self, index): @@ -214,9 +203,11 @@ class LineString(GEOSGeometry): 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(len(self))] # constructing the list, using the function + """ + 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(len(self))] if HAS_NUMPY: return array(lst) # ARRRR! else: return lst @@ -251,16 +242,16 @@ class LinearRing(LineString): class Polygon(GEOSGeometry): def __init__(self, *args, **kwargs): - """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._parent = None - self._rings = {} + 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. + + Below are some examples of initialization, where shell, hole1, and + hole2 are valid LinearRing geometries: + >>> poly = Polygon(shell, hole1, hole2) + >>> poly = Polygon(shell, (hole1, hole2)) + """ if not args: raise TypeError, 'Must provide at list one LinearRing instance to initialize Polygon.' @@ -293,26 +284,28 @@ class Polygon(GEOSGeometry): def __del__(self): "Overloaded deletion method for Polygons." - #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() - 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. 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() + #print 'polygon: Deleting %s (parent=%s, valid=%s)' % (self.__class__.__name__, self._ptr.parent, self._ptr.valid) + # Not performed on children Polygons from MultiPolygon or GeometryCollection objects. + if not self._ptr.child: + # 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 r in self._ptr: r.nullify() + else: + # Internal memory has become part of other Geometry objects; must + # delete the internal objects which are still valid individually, + # because calling the destructor on entire geometry will result + # in an attempted deletion of NULL pointers for the missing + # components (which may crash Python). + for r in self._ptr: r.destroy() 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.""" + """ + 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: return self.exterior_ring else: @@ -353,31 +346,36 @@ class Polygon(GEOSGeometry): 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() + for r in self._ptr: r.nullify() return super(Polygon, self)._nullify() 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))) + "Internal routine for populating the internal ring pointers." + # Only populate if there aren't already children pointers. + if len(self._ptr) == 0: + # Getting the exterior ring pointer address. + ring_list = [lgeos.GEOSGetExteriorRing(self._ptr())] + # Getting the interior ring pointer addresses. + ring_list += [lgeos.GEOSGetInteriorRingN(self._ptr(), c_int(i)) for i in xrange(self.num_interior_rings)] + # Getting the coordinate sequence pointer address for each of the rings. + ptr_list = [(ring_ptr, lgeos.GEOSGeom_getCoordSeq(ring_ptr)) for ring_ptr in ring_list] + # Setting the children pointers. + self._ptr.set_children(ptr_list) 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.""" + """ + Gets the interior ring at the specified index, 0 is for the first + interior ring, not the exterior ring. + """ # 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) + return GEOSGeometry(self._ptr[ring_i+1], srid=self.srid) #### Polygon Properties #### @property def num_interior_rings(self): "Returns the number of interior rings." - # Getting the number of rings n = lgeos.GEOSGetNumInteriorRings(self._ptr()) @@ -387,7 +385,7 @@ class Polygon(GEOSGeometry): def get_ext_ring(self): "Gets the exterior ring of the Polygon." - return GEOSGeometry(self._rings[0], parent=self._ptr, srid=self.srid) + return GEOSGeometry(self._ptr[0], srid=self.srid) def set_ext_ring(self, ring): "Sets the exterior ring of the Polygon." diff --git a/django/contrib/gis/geos/libgeos.py b/django/contrib/gis/geos/libgeos.py index 1a373e8682..65e80b40de 100644 --- a/django/contrib/gis/geos/libgeos.py +++ b/django/contrib/gis/geos/libgeos.py @@ -1,15 +1,15 @@ """ This module houses the ctypes initialization procedures, as well - as the notice and error handler function callbacks (get called - when an error occurs in GEOS). + as the notice and error handler function callbacks (get called + when an error occurs in GEOS). - This module also houses GEOS Pointer utilities, including the - GEOSPointer class, get_pointer_arr(), GEOM_PTR, and init_from_geom(). + This module also houses GEOS Pointer utilities, including + get_pointer_arr(), and GEOM_PTR. """ from django.contrib.gis.geos.error import GEOSException -from ctypes import c_char_p, c_int, pointer, CDLL, CFUNCTYPE, POINTER, Structure -import os, sys +from ctypes import c_char_p, c_int, string_at, CDLL, CFUNCTYPE, POINTER, Structure +import atexit, os, sys # NumPy supported? try: @@ -18,7 +18,7 @@ try: except ImportError: HAS_NUMPY = False -# Are psycopg2 and GeoDjango models being used? +# Is psycopg2 available? try: from psycopg2.extensions import ISQLQuote except (ImportError, EnvironmentError): @@ -67,11 +67,11 @@ error_h = ERRORFUNC(error_h) # The initGEOS routine should be called first, however, that routine takes # the notice and error functions as parameters. Here is the C code that -# we need to wrap: +# is wrapped: # "extern void GEOS_DLL initGEOS(GEOSMessageHandler notice_function, GEOSMessageHandler error_function);" lgeos.initGEOS(notice_h, error_h) -#### GEOS Geometry Pointer object, related C data structures, and functions. #### +#### GEOS Geometry C data structures, and utility functions. #### # Opaque GEOS geometry structure class GEOSGeom_t(Structure): @@ -81,87 +81,16 @@ class GEOSGeom_t(Structure): # Pointer to opaque geometry structure GEOM_PTR = POINTER(GEOSGeom_t) -# Used specifically by the GEOSGeom_createPolygon and GEOSGeom_createCollection GEOS routines +# 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() -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, - 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.""" +def geos_version(): + "Returns the string version of GEOS." + return string_at(lgeos.GEOSversion()) - ### Python 'magic' routines ### - 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 (an integer) will be returned.""" - 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 __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): - "Returns the address of the GEOSPointer (represented as an integer)." - return self._geom.contents.value - - @property - def valid(self): - "Tests whether this GEOSPointer is valid." - if bool(self.address): return True - else: return False - - ### Coordinate Sequence routines and 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, 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).' - 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). 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) +# Calling the finishGEOS() upon exit of the interpreter. +atexit.register(lgeos.finishGEOS) diff --git a/django/contrib/gis/geos/pointer.py b/django/contrib/gis/geos/pointer.py new file mode 100644 index 0000000000..50c0970d95 --- /dev/null +++ b/django/contrib/gis/geos/pointer.py @@ -0,0 +1,265 @@ +""" + This module houses the GEOSPointer class, used by GEOS Geometry objects + internally for memory management. Do not modify unless you _really_ + know what you're doing. +""" +from ctypes import cast, c_int, c_void_p, pointer, POINTER, Structure +from django.contrib.gis.geos.error import GEOSException +from django.contrib.gis.geos.libgeos import lgeos + +# This C data structure houses the memory addresses (integers) of the +# pointers returned from the GEOS C routines. +class GEOSStruct(Structure): pass +GEOSStruct._fields_ = [("geom", POINTER(c_int)), # for the geometry + ("cs", POINTER(c_int)), # for geometries w/coordinate sequences + ("parent", POINTER(GEOSStruct)), # points to the GEOSStruct of the parent + ("child", c_void_p), # points to an array of GEOSStructs + ("nchild", c_int), # the number of children + ] + +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, which allows parent geometries to be 'nullified' + when a child's memory is used in construction of another geometry. + + This object is also used to store pointers for any associated coordinate + sequence and may store the pointers for any children geometries. + """ + + #### Python 'magic' routines #### + def __init__(self, address, coordseq=0): + """ + Initializes on an address (an integer), another GEOSPointer, or a + GEOSStruct object. + """ + if isinstance(address, int): + self._struct = GEOSStruct() + # Integer addresses passed in, use the 'set' methods. + self.set(address) + if coordseq: self.set_coordseq(coordseq) + elif isinstance(address, GEOSPointer): + # Initializing from another GEOSPointer + self._struct = address._struct + elif isinstance(address, GEOSStruct): + # GEOSStruct passed directly in as a parameter + self._struct = address + else: + raise TypeError, 'GEOSPointer object must initialize with an integer.' + + def __call__(self): + """ + Returns the address value (an integer) of the GEOS Geometry pointer. + If the pointer is NULL, a GEOSException will be raised, thus preventing + an invalid memory address from being passed to a C routine. + """ + 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 __getitem__(self, index): + "Returns a GEOSpointer object at the given child index." + n_child = len(self) + if n_child: + if index < 0 or index >= n_child: + raise IndexError, 'invalid child index' + else: + CHILD_PTR = POINTER(GEOSStruct * len(self)) + return GEOSPointer(cast(self._struct.child, CHILD_PTR).contents[index]) + else: + raise GEOSException, 'This GEOSPointer is not a parent' + + def __iter__(self): + """ + Iterates over the children Geometry pointers, return as GEOSPointer + objects to the caller. + """ + for i in xrange(len(self)): + yield self[i] + + def __len__(self): + "Returns the number of children Geometry pointers (or 0 if no children)." + return self._struct.nchild + + def __nonzero__(self): + "Returns True when the GEOSPointer is valid." + return self.valid + + def __str__(self): + "Returns the string representation of this GEOSPointer." + # First getting the address for the Geometry pointer. + if self.valid: geom_addr = self.address + else: geom_addr = 0 + # If there's a coordinate sequence, construct accoringly. + if self.coordseq_valid: + return 'GEOSPointer(%s, %s)' % (geom_addr, self.coordseq_address) + else: + return 'GEOSPointer(%s)' % geom_addr + + def __repr__(self): + return str(self) + + #### GEOSPointer Properties #### + @property + def address(self): + "Returns the address of the GEOSPointer (represented as an integer)." + return self._struct.geom.contents.value + + @property + def valid(self): + "Tests whether this GEOSPointer is valid." + return bool(self._struct.geom) + + #### Parent & Child Properties #### + @property + def parent(self): + "Returns the GEOSPointer for the parent or None." + if self.child: + return GEOSPointer(self._struct.parent.contents) + else: + return None + + @property + def child(self): + "Returns True if the GEOSPointer has a parent." + return bool(self._struct.parent) + + #### Coordinate Sequence routines and properties #### + def coordseq(self): + """ + If the coordinate sequence pointer is NULL or 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._struct.cs.contents.value + + @property + def coordseq_valid(self): + "Returns True if the coordinate sequence address is valid, False otherwise." + return bool(self._struct.cs) + + #### GEOSPointer Methods #### + def destroy(self): + """ + Calls GEOSGeom_destroy on the address of this pointer, and nullifies + this pointer. Use VERY carefully, as trying to destroy an address that + no longer holds a valid GEOS Geometry may crash Python. + """ + if self.valid: + # ONLY on valid geometries. + lgeos.GEOSGeom_destroy(self.address) + self.nullify() + + def set(self, address): + """ + Sets the address of this pointer with the given address (represented + as an integer). Using 0 or None will set the pointer to NULL. + """ + if address in (0, None): + self._struct.geom = None + else: + self._struct.geom.contents = c_int(address) + + def set_coordseq(self, address): + """ + Sets the address of the coordinate sequence associated with + this pointer. + """ + if address in (0, None): + self._struct.cs = None + else: + self._struct.cs.contents = c_int(address) + + def set_children(self, ptr_list): + """ + Sets children pointers with the given pointer list (of integers). + Alternatively, a list of tuples for the geometry and coordinate + sequence pointers of the children may be used. + """ + # The number of children geometries is the number of pointers (or + # tuples) passed in via the `ptr_list`. + n_child = len(ptr_list) + + # Determining whether coordinate sequences pointers were passed in. + if isinstance(ptr_list[0], (tuple, list)): + self._child_cs = True + else: + self._child_cs = False + + # Dynamically creating the C types for the children array (CHILD_ARR), + # initializing with the created type, and creating a parent pointer + # for the children. + CHILD_ARR = GEOSStruct * n_child + children = CHILD_ARR() + parent = pointer(self._struct) + + # Incrementing through each of the children, and setting the + # pointers in the array of GEOSStructs. + for i in xrange(n_child): + if self._child_cs: + geom_ptr, cs_ptr = ptr_list[i] + if cs_ptr is not None: + children[i].cs.contents = c_int(cs_ptr) + else: + geom_ptr = ptr_list[i] + children[i].geom.contents = c_int(geom_ptr) + children[i].parent = parent + + # Casting the CHILD_ARR to the contents of the void pointer, and + # setting the number of children within the struct (used by __len__). + self._struct.child = cast(pointer(children), c_void_p) + self._struct.nchild = c_int(n_child) + + def nullify(self): + """ + Nullify the geometry and coordinate sequence pointers (sets the + pointers to NULL). This does not delete any memory (destroy() + handles that), rather, it sets the GEOS pointers to a NULL address, + preventing access to deleted objects. + """ + # Nullifying both the geometry and coordinate sequence pointer. + self.set(None) + self.set_coordseq(None) + + def summary(self): + """ + Returns a summary string containing information about the pointer, + including the geometry address, any associated coordinate sequence + address, and child geometry addresses. + """ + sum = '%s\n' % str(self) + for p1 in self: + sum += ' * %s\n' % p1 + for p2 in p1: sum += ' - %s\n' % p2 + if bool(self._struct.parent): + sum += 'Parent: %s\n' % self.parent + return sum + +class NullGEOSPointer(GEOSPointer): + "The NullGEOSPointer is always NULL, and cannot be set to anything else." + def __init__(self): + self._struct = GEOSStruct() + + @property + def valid(self): + return False + + @property + def coordseq_valid(self): + return False + + def set(self, *args): + pass + + def set_coordseq(self, *args): + pass + + def set_children(self, *args): + pass + +NULL_GEOM = NullGEOSPointer() diff --git a/django/contrib/gis/tests/test_geos.py b/django/contrib/gis/tests/test_geos.py index 81525a868c..be0da42e5a 100644 --- a/django/contrib/gis/tests/test_geos.py +++ b/django/contrib/gis/tests/test_geos.py @@ -13,13 +13,13 @@ class GEOSTest(unittest.TestCase): def test01a_wkt(self): "Testing WKT output." for g in wkt_out: - geom = GEOSGeometry(g.wkt) + geom = fromstr(g.wkt) self.assertEqual(g.ewkt, geom.wkt) def test01b_hex(self): "Testing HEX output." for g in hex_wkt: - geom = GEOSGeometry(g.wkt) + geom = fromstr(g.wkt) self.assertEqual(g.hex, geom.hex) def test01c_kml(self): @@ -34,22 +34,22 @@ class GEOSTest(unittest.TestCase): print "\nBEGIN - expecting GEOS_ERROR; safe to ignore.\n" for err in errors: if err.hex: - self.assertRaises(GEOSException, GEOSGeometry, err.wkt) + self.assertRaises(GEOSException, fromstr, err.wkt) else: - self.assertRaises(GEOSException, GEOSGeometry, err.wkt) + self.assertRaises(GEOSException, fromstr, err.wkt) print "\nEND - expecting GEOS_ERROR; safe to ignore.\n" def test02a_points(self): "Testing Point objects." - prev = GEOSGeometry('POINT(0 0)') + prev = fromstr('POINT(0 0)') for p in points: # Creating the point from the WKT - pnt = GEOSGeometry(p.wkt) + pnt = fromstr(p.wkt) self.assertEqual(pnt.geom_type, 'Point') self.assertEqual(pnt.geom_typeid, 0) self.assertEqual(p.x, pnt.x) self.assertEqual(p.y, pnt.y) - self.assertEqual(True, pnt == GEOSGeometry(p.wkt)) + self.assertEqual(True, pnt == fromstr(p.wkt)) self.assertEqual(False, pnt == prev) # Making sure that the point's X, Y components are what we expect @@ -97,7 +97,7 @@ class GEOSTest(unittest.TestCase): def test02b_multipoints(self): "Testing MultiPoint objects." for mp in multipoints: - mpnt = GEOSGeometry(mp.wkt) + mpnt = fromstr(mp.wkt) self.assertEqual(mpnt.geom_type, 'MultiPoint') self.assertEqual(mpnt.geom_typeid, 4) @@ -115,9 +115,9 @@ class GEOSTest(unittest.TestCase): def test03a_linestring(self): "Testing LineString objects." - prev = GEOSGeometry('POINT(0 0)') + prev = fromstr('POINT(0 0)') for l in linestrings: - ls = GEOSGeometry(l.wkt) + ls = fromstr(l.wkt) self.assertEqual(ls.geom_type, 'LineString') self.assertEqual(ls.geom_typeid, 1) self.assertEqual(ls.empty, False) @@ -127,7 +127,7 @@ class GEOSTest(unittest.TestCase): if hasattr(l, 'tup'): self.assertEqual(l.tup, ls.tuple) - self.assertEqual(True, ls == GEOSGeometry(l.wkt)) + self.assertEqual(True, ls == fromstr(l.wkt)) self.assertEqual(False, ls == prev) self.assertRaises(GEOSGeometryIndexError, ls.__getitem__, len(ls)) prev = ls @@ -141,16 +141,16 @@ class GEOSTest(unittest.TestCase): def test03b_multilinestring(self): "Testing MultiLineString objects." - prev = GEOSGeometry('POINT(0 0)') + prev = fromstr('POINT(0 0)') for l in multilinestrings: - ml = GEOSGeometry(l.wkt) + ml = fromstr(l.wkt) self.assertEqual(ml.geom_type, 'MultiLineString') self.assertEqual(ml.geom_typeid, 5) self.assertAlmostEqual(l.centroid[0], ml.centroid.x, 9) self.assertAlmostEqual(l.centroid[1], ml.centroid.y, 9) - self.assertEqual(True, ml == GEOSGeometry(l.wkt)) + self.assertEqual(True, ml == fromstr(l.wkt)) self.assertEqual(False, ml == prev) prev = ml @@ -166,7 +166,7 @@ class GEOSTest(unittest.TestCase): def test04_linearring(self): "Testing LinearRing objects." for rr in linearrings: - lr = GEOSGeometry(rr.wkt) + lr = fromstr(rr.wkt) self.assertEqual(lr.geom_type, 'LinearRing') self.assertEqual(lr.geom_typeid, 2) self.assertEqual(rr.n_p, len(lr)) @@ -181,10 +181,10 @@ class GEOSTest(unittest.TestCase): def test05a_polygons(self): "Testing Polygon objects." - prev = GEOSGeometry('POINT(0 0)') + prev = fromstr('POINT(0 0)') for p in polygons: # Creating the Polygon, testing its properties. - poly = GEOSGeometry(p.wkt) + poly = fromstr(p.wkt) self.assertEqual(poly.geom_type, 'Polygon') self.assertEqual(poly.geom_typeid, 3) self.assertEqual(poly.empty, False) @@ -199,7 +199,7 @@ class GEOSTest(unittest.TestCase): self.assertAlmostEqual(p.centroid[1], poly.centroid.tuple[1], 9) # Testing the geometry equivalence - self.assertEqual(True, poly == GEOSGeometry(p.wkt)) + self.assertEqual(True, poly == fromstr(p.wkt)) self.assertEqual(False, poly == prev) # Should not be equal to previous geometry self.assertEqual(True, poly != prev) @@ -222,9 +222,8 @@ class GEOSTest(unittest.TestCase): self.assertEqual(r.geom_typeid, 2) # Testing polygon construction. - self.assertRaises(TypeError, Polygon, 0, [1, 2, 3]) - self.assertRaises(TypeError, Polygon, 'foo') - + self.assertRaises(TypeError, Polygon.__init__, 0, [1, 2, 3]) + self.assertRaises(TypeError, Polygon.__init__, '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) @@ -246,9 +245,9 @@ class GEOSTest(unittest.TestCase): def test05b_multipolygons(self): "Testing MultiPolygon objects." print "\nBEGIN - expecting GEOS_NOTICE; safe to ignore.\n" - prev = GEOSGeometry('POINT (0 0)') + prev = fromstr('POINT (0 0)') for mp in multipolygons: - mpoly = GEOSGeometry(mp.wkt) + mpoly = fromstr(mp.wkt) self.assertEqual(mpoly.geom_type, 'MultiPolygon') self.assertEqual(mpoly.geom_typeid, 6) self.assertEqual(mp.valid, mpoly.valid) @@ -266,14 +265,14 @@ class GEOSTest(unittest.TestCase): print "\nEND - expecting GEOS_NOTICE; safe to ignore.\n" - def test06_memory_hijinks(self): - "Testing Geometry __del__() in different scenarios" + def test06a_memory_hijinks(self): + "Testing Geometry __del__() on rings and polygons." #### Memory issues with rings and polygons # These tests are needed to ensure sanity with writable geometries. # Getting a polygon with interior rings, and pulling out the interior rings - poly = GEOSGeometry(polygons[1].wkt) + poly = fromstr(polygons[1].wkt) ring1 = poly[0] ring2 = poly[1] @@ -292,6 +291,8 @@ class GEOSTest(unittest.TestCase): self.assertRaises(GEOSException, str, ring1) self.assertRaises(GEOSException, str, ring2) + def test06b_memory_hijinks(self): + "Testing Geometry __del__() on collections." #### Memory issues with geometries from Geometry Collections mp = fromstr('MULTIPOINT(85 715, 235 1400, 4620 1711)') @@ -315,16 +316,17 @@ class GEOSTest(unittest.TestCase): # Should raise GEOSException when trying to get geometries from the multipoint # after it has been deleted. + parr1 = [ptr for ptr in mp._ptr] + parr2 = [p._ptr for p in mp] del mp + for p in pts: self.assertRaises(GEOSException, str, p) # tests p's geometry pointer self.assertRaises(GEOSException, p.get_coords) # 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) + poly = fromstr(polygons[3].wkt) # a 'real life' polygon. + linring = fromstr(linearrings[0].wkt) # a 'real life' linear ring # Pulling out the shell and cloning our initial geometries for later comparison. shell = poly.shell @@ -338,33 +340,42 @@ class GEOSTest(unittest.TestCase): self.assertRaises(GEOSException, str, shell) self.assertRaises(GEOSException, str, linring) - r1 = gc[1] # pulling out the ring + # Deep-indexing delete should be 'harmless.' + tmpr1 = gc[0][0] + del tmpr1 + + r1 = gc[0][0] # pulling out shell from polygon -- deep indexing + r2 = 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) + self.assertEqual(linringc.wkt, mpoly[1][0].wkt) # deep-indexed ring # 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. + # BUT, should still be able to access the Point we obtained earlier, + # however, 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) + for tmpr in [r1, r2]: + # __len__ is called on the coordinate sequence pointer -- + # making sure its nullified as well + self.assertRaises(GEOSException, len, tmpr) + self.assertRaises(GEOSException, str, tmpr) # Can't access point after deletion of parent geometry. del gc self.assertRaises(GEOSException, str, pnt) # Cleaning up. - del polyc - del mpoly + del polyc, mpoly + def test06c_memory_hijinks(self): + "Testing __init__ using other Geometries as parameters." #### Memory issues with creating geometries from coordinate sequences within other geometries # Creating the initial polygon from the following tuples, and then pulling out @@ -388,18 +399,42 @@ class GEOSTest(unittest.TestCase): # 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 + del poly1, poly2 self.assertRaises(GEOSException, str, hole2) + + #### Testing creating Geometries w/"deep-indexed" rings #### + mpoly = MultiPolygon(Polygon(LinearRing(ext_tup), LinearRing(itup1), LinearRing(itup2)), + Polygon(LinearRing(itup1))) + + r0 = mpoly[0][1] # itup1, left alone + r1 = mpoly[0][0] # ext_tup + r2 = mpoly[1][0] # itup1 + r3 = mpoly[0][2] # itup2 + + # Using the rings of the multipolygon to create a new Polygon, should + # no longer be able to access the MultiPolygon or the ring objects + # used in initialization. + p1 = Polygon(r1, r2, r3) + for g in [mpoly, r1, r2, r3]: + self.assertRaises(GEOSException, len, g) + self.assertRaises(GEOSException, str, g) + # However, the middle ring of the first Polygon was not used. + self.assertEqual(r0.tuple, itup1) + self.assertEqual(r0, p1[1]) + + # This deletes the leftover ring (or whenever mpoly is garbage collected) + del mpoly + def test08_coord_seq(self): "Testing Coordinate Sequence objects." for p in polygons: if p.ext_ring_cs: # Constructing the polygon and getting the coordinate sequence - poly = GEOSGeometry(p.wkt) + poly = fromstr(p.wkt) cs = poly.exterior_ring.coord_seq self.assertEqual(p.ext_ring_cs, cs.tuple) # done in the Polygon test too. @@ -423,13 +458,13 @@ class GEOSTest(unittest.TestCase): def test09_relate_pattern(self): "Testing relate() and relate_pattern()." - g = GEOSGeometry('POINT (0 0)') + g = fromstr('POINT (0 0)') self.assertRaises(GEOSException, g.relate_pattern, 0, 'invalid pattern, yo') for i in xrange(len(relate_geoms)): g_tup = relate_geoms[i] - a = GEOSGeometry(g_tup[0].wkt) - b = GEOSGeometry(g_tup[1].wkt) + a = fromstr(g_tup[0].wkt) + b = fromstr(g_tup[1].wkt) pat = g_tup[2] result = g_tup[3] self.assertEqual(result, a.relate_pattern(b, pat)) @@ -439,9 +474,9 @@ class GEOSTest(unittest.TestCase): "Testing intersects() and intersection()." for i in xrange(len(topology_geoms)): g_tup = topology_geoms[i] - a = GEOSGeometry(g_tup[0].wkt) - b = GEOSGeometry(g_tup[1].wkt) - i1 = GEOSGeometry(intersect_geoms[i].wkt) + a = fromstr(g_tup[0].wkt) + b = fromstr(g_tup[1].wkt) + i1 = fromstr(intersect_geoms[i].wkt) self.assertEqual(True, a.intersects(b)) i2 = a.intersection(b) self.assertEqual(i1, i2) @@ -453,9 +488,9 @@ class GEOSTest(unittest.TestCase): "Testing union()." for i in xrange(len(topology_geoms)): g_tup = topology_geoms[i] - a = GEOSGeometry(g_tup[0].wkt) - b = GEOSGeometry(g_tup[1].wkt) - u1 = GEOSGeometry(union_geoms[i].wkt) + a = fromstr(g_tup[0].wkt) + b = fromstr(g_tup[1].wkt) + u1 = fromstr(union_geoms[i].wkt) u2 = a.union(b) self.assertEqual(u1, u2) self.assertEqual(u1, a | b) # __or__ is union operator @@ -466,9 +501,9 @@ class GEOSTest(unittest.TestCase): "Testing 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(diff_geoms[i].wkt) + a = fromstr(g_tup[0].wkt) + b = fromstr(g_tup[1].wkt) + d1 = fromstr(diff_geoms[i].wkt) d2 = a.difference(b) self.assertEqual(d1, d2) self.assertEqual(d1, a - b) # __sub__ is difference operator @@ -479,9 +514,9 @@ class GEOSTest(unittest.TestCase): "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) + a = fromstr(g_tup[0].wkt) + b = fromstr(g_tup[1].wkt) + d1 = fromstr(sdiff_geoms[i].wkt) d2 = a.sym_difference(b) self.assertEqual(d1, d2) self.assertEqual(d1, a ^ b) # __xor__ is symmetric difference operator @@ -492,10 +527,10 @@ class GEOSTest(unittest.TestCase): "Testing buffer()." for i in xrange(len(buffer_geoms)): g_tup = buffer_geoms[i] - g = GEOSGeometry(g_tup[0].wkt) + g = fromstr(g_tup[0].wkt) # The buffer we expect - exp_buf = GEOSGeometry(g_tup[1].wkt) + exp_buf = fromstr(g_tup[1].wkt) # Can't use a floating-point for the number of quadsegs. self.assertRaises(TypeError, g.buffer, g_tup[2], float(g_tup[3])) @@ -575,8 +610,8 @@ class GEOSTest(unittest.TestCase): self.assertNotEqual(pnt, mp[i]) del mp - # Multipolygons involve much more memory management because each - # polygon w/in the collection has its own rings. + # 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)): @@ -592,12 +627,17 @@ class GEOSTest(unittest.TestCase): self.assertRaises(GEOSException, str, tmp) self.assertEqual(mpoly[i], new) self.assertNotEqual(poly, mpoly[i]) - del mpoly + + # Extreme (!!) __setitem__ + mpoly[0][0][0] = (3.14, 2.71) + self.assertEqual((3.14, 2.71), mpoly[0][0][0]) + # Doing it more slowly.. + self.assertEqual((3.14, 2.71), mpoly[0].shell[0]) + del mpoly def test17_threed(self): "Testing three-dimensional geometries." - # Testing a 3D Point pnt = Point(2, 3, 8) self.assertEqual((2.,3.,8.), pnt.coords) @@ -632,7 +672,6 @@ class GEOSTest(unittest.TestCase): def test19_length(self): "Testing the length property." - # Points have 0 length. pnt = Point(0, 0) self.assertEqual(0.0, pnt.length)