diff --git a/django/contrib/gis/geos/LICENSE b/django/contrib/gis/geos/LICENSE index 84cf485d004..0479b0773e4 100644 --- a/django/contrib/gis/geos/LICENSE +++ b/django/contrib/gis/geos/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2007, Justin Bronn +Copyright (c) 2007-2009 Justin Bronn All rights reserved. Redistribution and use in source and binary forms, with or without modification, diff --git a/django/contrib/gis/geos/__init__.py b/django/contrib/gis/geos/__init__.py index 90e26052406..5885a30bef4 100644 --- a/django/contrib/gis/geos/__init__.py +++ b/django/contrib/gis/geos/__init__.py @@ -1,69 +1,14 @@ """ - The goal of this module is to be a ctypes wrapper around the GEOS library - that will work on both *NIX and Windows systems. Specifically, this uses - the GEOS C api. - - I have several motivations for doing this: - (1) The GEOS SWIG wrapper is no longer maintained, and requires the - installation of SWIG. - (2) The PCL implementation is over 2K+ lines of C and would make - PCL a requisite package for the GeoDjango application stack. - (3) Windows and Mac compatibility becomes substantially easier, and does not - require the additional compilation of PCL or GEOS and SWIG -- all that - is needed is a Win32 or Mac compiled GEOS C library (dll or dylib) - in a location that Python can read (e.g. 'C:\Python25'). - - In summary, I wanted to wrap GEOS in a more maintainable and portable way using - only Python and the excellent ctypes library (now standard in Python 2.5). - - In the spirit of loose coupling, this library does not require Django or - GeoDjango. Only the GEOS C library and ctypes are needed for the platform - of your choice. - - For more information about GEOS: - http://geos.refractions.net - - For more info about PCL and the discontinuation of the Python GEOS - library see Sean Gillies' writeup (and subsequent update) at: - http://zcologia.com/news/150/geometries-for-python/ - http://zcologia.com/news/429/geometries-for-python-update/ +The GeoDjango GEOS module. Please consult the GeoDjango documentation +for more details: + http://geodjango.org/docs/geos.html """ -from django.contrib.gis.geos.base import GEOSGeometry, wkt_regex, hex_regex -from django.contrib.gis.geos.geometries import Point, LineString, LinearRing, Polygon, HAS_NUMPY +from django.contrib.gis.geos.geometry import GEOSGeometry, wkt_regex, hex_regex +from django.contrib.gis.geos.point import Point +from django.contrib.gis.geos.linestring import LineString, LinearRing +from django.contrib.gis.geos.polygon import Polygon from django.contrib.gis.geos.collections import GeometryCollection, MultiPoint, MultiLineString, MultiPolygon from django.contrib.gis.geos.error import GEOSException, GEOSIndexError -from django.contrib.gis.geos.libgeos import geos_version, geos_version_info - -def fromfile(file_name): - """ - Given a string file name, returns a GEOSGeometry. The file may contain WKB, - WKT, or HEX. - """ - fh = open(file_name, 'rb') - buf = fh.read() - fh.close() - if wkt_regex.match(buf) or hex_regex.match(buf): - return GEOSGeometry(buf) - else: - return GEOSGeometry(buffer(buf)) - -def fromstr(wkt_or_hex, **kwargs): - "Given a string value (wkt or hex), returns a GEOSGeometry object." - return GEOSGeometry(wkt_or_hex, **kwargs) - -def hex_to_wkt(hex): - "Converts HEXEWKB into WKT." - return GEOSGeometry(hex).wkt - -def wkt_to_hex(wkt): - "Converts WKT into HEXEWKB." - return GEOSGeometry(wkt).hex - -def centroid(input): - "Returns the centroid of the geometry (given in HEXEWKB)." - return GEOSGeometry(input).centroid.wkt - -def area(input): - "Returns the area of the geometry (given in HEXEWKB)." - return GEOSGeometry(input).area - +from django.contrib.gis.geos.io import WKTReader, WKTWriter, WKBReader, WKBWriter +from django.contrib.gis.geos.factory import fromfile, fromstr +from django.contrib.gis.geos.libgeos import geos_version, geos_version_info, GEOS_PREPARE diff --git a/django/contrib/gis/geos/base.py b/django/contrib/gis/geos/base.py index a56b19364e1..48a3e3f978e 100644 --- a/django/contrib/gis/geos/base.py +++ b/django/contrib/gis/geos/base.py @@ -1,608 +1,54 @@ -""" - This module contains the 'base' GEOSGeometry object -- all GEOS Geometries - inherit from this object. -""" -# Python, ctypes and types dependencies. -import re -from ctypes import addressof, byref, c_double, c_size_t -from types import UnicodeType - -# GEOS-related dependencies. -from django.contrib.gis.geos.coordseq import GEOSCoordSeq -from django.contrib.gis.geos.error import GEOSException -from django.contrib.gis.geos.libgeos import GEOM_PTR - -# All other functions in this module come from the ctypes -# prototypes module -- which handles all interaction with -# the underlying GEOS library. -from django.contrib.gis.geos.prototypes import * +from ctypes import c_void_p +from types import NoneType +from django.contrib.gis.geos.error import GEOSException, GEOSIndexError # Trying to import GDAL libraries, if available. Have to place in # try/except since this package may be used outside GeoDjango. try: - from django.contrib.gis.gdal import OGRGeometry, SpatialReference, GEOJSON - from django.contrib.gis.gdal.geometries import json_regex - HAS_GDAL = True -except: - HAS_GDAL, GEOJSON = False, False + from django.contrib.gis import gdal +except ImportError: + # A 'dummy' gdal module. + class GDALInfo(object): + HAS_GDAL = False + GEOJSON = False + gdal = GDALInfo() -# Regular expression for recognizing HEXEWKB and WKT. A prophylactic measure -# to prevent potentially malicious input from reaching the underlying C -# library. Not a substitute for good web security programming practices. -hex_regex = re.compile(r'^[0-9A-F]+$', re.I) -wkt_regex = re.compile(r'^(SRID=(?P\d+);)?(?P(POINT|LINESTRING|LINEARRING|POLYGON|MULTIPOINT|MULTILINESTRING|MULTIPOLYGON|GEOMETRYCOLLECTION)[ACEGIMLONPSRUTY\d,\.\-\(\) ]+)$', re.I) +# NumPy supported? +try: + import numpy +except ImportError: + numpy = False -class GEOSGeometry(object): - "A class that, generally, encapsulates a GEOS geometry." - - # Initially, the geometry pointer is NULL +class GEOSBase(object): + """ + Base object for GEOS objects that has a pointer access property + that controls access to the underlying C pointer. + """ + # Initially the pointer is NULL. _ptr = None - #### Python 'magic' routines #### - def __init__(self, geo_input, srid=None): - """ - The base constructor for GEOS geometry objects, and may take the - following inputs: - - * string: WKT - * string: HEXEWKB (a PostGIS-specific canonical form) - * buffer: WKB - - 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, basestring): - if isinstance(geo_input, UnicodeType): - # Encoding to ASCII, WKT or HEXEWKB doesn't need any more. - geo_input = geo_input.encode('ascii') - - wkt_m = wkt_regex.match(geo_input) - if wkt_m: - # Handling WKT input. - if wkt_m.group('srid'): srid = int(wkt_m.group('srid')) - g = from_wkt(wkt_m.group('wkt')) - elif hex_regex.match(geo_input): - # Handling HEXEWKB input. - g = from_hex(geo_input, len(geo_input)) - elif GEOJSON and json_regex.match(geo_input): - # Handling GeoJSON input. - wkb_input = str(OGRGeometry(geo_input).wkb) - g = from_wkb(wkb_input, len(wkb_input)) - else: - raise ValueError('String or unicode input unrecognized as WKT EWKT, and HEXEWKB.') - elif isinstance(geo_input, GEOM_PTR): - # When the input is a pointer to a geomtry (GEOM_PTR). - g = geo_input - elif isinstance(geo_input, buffer): - # When the input is a buffer (WKB). - wkb_input = str(geo_input) - g = from_wkb(wkb_input, len(wkb_input)) + # Default allowed pointer type. + ptr_type = c_void_p + + # Pointer access property. + def _get_ptr(self): + # Raise an exception if the pointer isn't valid don't + # want to be passing NULL pointers to routines -- + # that's very bad. + if self._ptr: return self._ptr + else: raise GEOSException('NULL GEOS %s pointer encountered.' % self.__class__.__name__) + + def _set_ptr(self, ptr): + # Only allow the pointer to be set with pointers of the + # compatible type or None (NULL). + if isinstance(ptr, int): + self._ptr = self.ptr_type(ptr) + elif isinstance(ptr, (self.ptr_type, NoneType)): + self._ptr = ptr else: - # Invalid geometry type. - raise TypeError('Improper geometry input type: %s' % str(type(geo_input))) + raise TypeError('Incompatible pointer type') - if bool(g): - # Setting the pointer object with a valid pointer. - self._ptr = g - else: - raise GEOSException('Could not initialize GEOS Geometry with given input.') - - # Post-initialization setup. - self._post_init(srid) - - def _post_init(self, srid): - "Helper routine for performing post-initialization setup." - # 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_typeid] - - # Setting the coordinate sequence for the geometry (will be None on - # geometries that do not have coordinate sequences) - self._set_cs() - - @property - def ptr(self): - """ - Property for controlling access to the GEOS geometry pointer. Using - this raises an exception when the pointer is NULL, thus preventing - the C library from attempting to access an invalid memory location. - """ - if self._ptr: - return self._ptr - else: - raise GEOSException('NULL GEOS pointer encountered; was this geometry modified?') - - def __del__(self): - """ - Destroys this Geometry; in other words, frees the memory used by the - GEOS C++ object. - """ - if self._ptr: destroy_geom(self._ptr) - - def __copy__(self): - """ - Returns a clone because the copy of a GEOSGeometry may contain an - invalid pointer location if the original is garbage collected. - """ - return self.clone() - - def __deepcopy__(self, memodict): - """ - The `deepcopy` routine is used by the `Node` class of django.utils.tree; - thus, the protocol routine needs to be implemented to return correct - copies (clones) of these GEOS objects, which use C pointers. - """ - return self.clone() - - def __str__(self): - "WKT is used for the string representation." - return self.wkt - - def __repr__(self): - "Short-hand representation because WKT may be very large." - return '<%s object at %s>' % (self.geom_type, hex(addressof(self.ptr))) - - # Pickling support - def __getstate__(self): - # The pickled state is simply a tuple of the WKB (in string form) - # and the SRID. - return str(self.wkb), self.srid - - def __setstate__(self, state): - # Instantiating from the tuple state that was pickled. - wkb, srid = state - ptr = from_wkb(wkb, len(wkb)) - if not ptr: raise GEOSException('Invalid Geometry loaded from pickled state.') - self._ptr = ptr - self._post_init(srid) - - # Comparison operators - def __eq__(self, other): - """ - Equivalence testing, a Geometry may be compared with another Geometry - or a WKT representation. - """ - if isinstance(other, basestring): - return self.wkt == other - elif isinstance(other, GEOSGeometry): - return self.equals_exact(other) - else: - return False - - def __ne__(self, other): - "The not equals operator." - return not (self == 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 #### - @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 _set_cs(self): - "Sets the coordinate sequence for this Geometry." - if self.has_cs: - self._cs = GEOSCoordSeq(get_cs(self.ptr), self.hasz) - else: - self._cs = None - - @property - def coord_seq(self): - "Returns a clone of the coordinate sequence for this Geometry." - if self.has_cs: - return self._cs.clone() - - #### Geometry Info #### - @property - def geom_type(self): - "Returns a string representing the Geometry type, e.g. 'Polygon'" - return geos_type(self.ptr) - - @property - def geom_typeid(self): - "Returns an integer representing the Geometry type." - return geos_typeid(self.ptr) - - @property - def num_geom(self): - "Returns the number of geometries in the Geometry." - return get_num_geoms(self.ptr) - - @property - def num_coords(self): - "Returns the number of coordinates in the Geometry." - return get_num_coords(self.ptr) - - @property - def num_points(self): - "Returns the number points, or coordinates, in the Geometry." - return self.num_coords - - @property - def dims(self): - "Returns the dimension of this Geometry (0=point, 1=line, 2=surface)." - return get_dims(self.ptr) - - def normalize(self): - "Converts this Geometry to normal form (or canonical form)." - return geos_normalize(self.ptr) - - #### Unary predicates #### - @property - def empty(self): - """ - Returns a boolean indicating whether the set of points in this Geometry - are empty. - """ - return geos_isempty(self.ptr) - - @property - def hasz(self): - "Returns whether the geometry has a 3D dimension." - return geos_hasz(self.ptr) - - @property - def ring(self): - "Returns whether or not the geometry is a ring." - return geos_isring(self.ptr) - - @property - def simple(self): - "Returns false if the Geometry not simple." - return geos_issimple(self.ptr) - - @property - def valid(self): - "This property tests the validity of this Geometry." - return geos_isvalid(self.ptr) - - #### Binary predicates. #### - def contains(self, other): - "Returns true if other.within(this) returns true." - return geos_contains(self.ptr, other.ptr) - - 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). - """ - return geos_crosses(self.ptr, other.ptr) - - def disjoint(self, other): - """ - Returns true if the DE-9IM intersection matrix for the two Geometries - is FF*FF****. - """ - return geos_disjoint(self.ptr, other.ptr) - - def equals(self, other): - """ - Returns true if the DE-9IM intersection matrix for the two Geometries - is T*F**FFF*. - """ - return geos_equals(self.ptr, other.ptr) - - def equals_exact(self, other, tolerance=0): - """ - Returns true if the two Geometries are exactly equal, up to a - specified tolerance. - """ - return geos_equalsexact(self.ptr, other.ptr, float(tolerance)) - - def intersects(self, other): - "Returns true if disjoint returns false." - return geos_intersects(self.ptr, other.ptr) - - 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). - """ - return geos_overlaps(self.ptr, other.ptr) - - 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. - """ - if not isinstance(pattern, str) or len(pattern) > 9: - raise GEOSException('invalid intersection matrix pattern') - return geos_relatepattern(self.ptr, other.ptr, pattern) - - def touches(self, other): - """ - Returns true if the DE-9IM intersection matrix for the two Geometries - is FT*******, F**T***** or F***T****. - """ - return geos_touches(self.ptr, other.ptr) - - def within(self, other): - """ - Returns true if the DE-9IM intersection matrix for the two Geometries - is T*F**F***. - """ - return geos_within(self.ptr, other.ptr) - - #### SRID Routines #### - def get_srid(self): - "Gets the SRID for the geometry, returns None if no SRID is set." - s = geos_get_srid(self.ptr) - if s == 0: return None - else: return s - - def set_srid(self, srid): - "Sets the SRID for the geometry." - geos_set_srid(self.ptr, srid) - srid = property(get_srid, set_srid) - - #### Output Routines #### - @property - def ewkt(self): - "Returns the EWKT (WKT + SRID) of the Geometry." - if self.get_srid(): return 'SRID=%s;%s' % (self.srid, self.wkt) - else: return self.wkt - - @property - def wkt(self): - "Returns the WKT (Well-Known Text) of the Geometry." - return to_wkt(self.ptr) - - @property - def hex(self): - """ - Returns the HEX of the Geometry -- please note that the SRID is not - included in this representation, because the GEOS C library uses - -1 by default, even if the SRID is set. - """ - # A possible faster, all-python, implementation: - # str(self.wkb).encode('hex') - return to_hex(self.ptr, byref(c_size_t())) - - @property - def json(self): - """ - Returns GeoJSON representation of this Geometry if GDAL 1.5+ - is installed. - """ - if GEOJSON: return self.ogr.json - geojson = json - - @property - def wkb(self): - "Returns the WKB of the Geometry as a buffer." - bin = to_wkb(self.ptr, byref(c_size_t())) - return buffer(bin) - - @property - def kml(self): - "Returns the KML representation of this Geometry." - gtype = self.geom_type - return '<%s>%s' % (gtype, self.coord_seq.kml, gtype) - - #### GDAL-specific output routines #### - @property - def ogr(self): - "Returns the OGR Geometry for this Geometry." - if HAS_GDAL: - if self.srid: - return OGRGeometry(self.wkb, self.srid) - else: - return OGRGeometry(self.wkb) - else: - return None - - @property - def srs(self): - "Returns the OSR SpatialReference for SRID of this Geometry." - if HAS_GDAL and self.srid: - return SpatialReference(self.srid) - else: - return None - - @property - def crs(self): - "Alias for `srs` property." - return self.srs - - def transform(self, ct, clone=False): - """ - Requires GDAL. Transforms the geometry according to the given - transformation object, which may be an integer SRID, and WKT or - PROJ.4 string. By default, the geometry is transformed in-place and - nothing is returned. However if the `clone` keyword is set, then this - geometry will not be modified and a transformed clone will be returned - instead. - """ - srid = self.srid - if HAS_GDAL and srid: - g = OGRGeometry(self.wkb, srid) - g.transform(ct) - wkb = str(g.wkb) - ptr = from_wkb(wkb, len(wkb)) - if clone: - # User wants a cloned transformed geometry returned. - return GEOSGeometry(ptr, srid=g.srid) - if ptr: - # Reassigning pointer, and performing post-initialization setup - # again due to the reassignment. - destroy_geom(self.ptr) - self._ptr = ptr - self._post_init(g.srid) - else: - raise GEOSException('Transformed WKB was invalid.') - - #### Topology Routines #### - def _topology(self, gptr): - "Helper routine to return Geometry from the given pointer." - return GEOSGeometry(gptr, srid=self.srid) - - @property - def boundary(self): - "Returns the boundary as a newly allocated Geometry object." - return self._topology(geos_boundary(self.ptr)) - - 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) - """ - return self._topology(geos_buffer(self.ptr, width, quadsegs)) - - @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). - """ - return self._topology(geos_centroid(self.ptr)) - - @property - def convex_hull(self): - """ - Returns the smallest convex Polygon that contains all the points - in the Geometry. - """ - return self._topology(geos_convexhull(self.ptr)) - - def difference(self, other): - """ - Returns a Geometry representing the points making up this Geometry - that do not make up other. - """ - return self._topology(geos_difference(self.ptr, other.ptr)) - - @property - def envelope(self): - "Return the envelope for this geometry (a polygon)." - return self._topology(geos_envelope(self.ptr)) - - def intersection(self, other): - "Returns a Geometry representing the points shared by this Geometry and other." - return self._topology(geos_intersection(self.ptr, other.ptr)) - - @property - def point_on_surface(self): - "Computes an interior point of this Geometry." - return self._topology(geos_pointonsurface(self.ptr)) - - def relate(self, other): - "Returns the DE-9IM intersection matrix for this Geometry and the other." - return geos_relate(self.ptr, other.ptr) - - def simplify(self, tolerance=0.0, preserve_topology=False): - """ - Returns the Geometry, simplified using the Douglas-Peucker algorithm - to the specified tolerance (higher tolerance => less points). If no - tolerance provided, defaults to 0. - - By default, this function does not preserve topology - e.g. polygons can - be split, collapse to lines or disappear holes can be created or - disappear, and lines can cross. By specifying preserve_topology=True, - the result will have the same dimension and number of components as the - input. This is significantly slower. - """ - if preserve_topology: - return self._topology(geos_preservesimplify(self.ptr, tolerance)) - else: - return self._topology(geos_simplify(self.ptr, tolerance)) - - 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. - """ - return self._topology(geos_symdifference(self.ptr, other.ptr)) - - def union(self, other): - "Returns a Geometry representing all the points in this Geometry and other." - return self._topology(geos_union(self.ptr, other.ptr)) - - #### Other Routines #### - @property - def area(self): - "Returns the area of the Geometry." - return geos_area(self.ptr, byref(c_double())) - - 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. - """ - if not isinstance(other, GEOSGeometry): - raise TypeError('distance() works only on other GEOS Geometries.') - return geos_distance(self.ptr, other.ptr, byref(c_double())) - - @property - def extent(self): - """ - Returns the extent of this geometry as a 4-tuple, consisting of - (xmin, ymin, xmax, ymax). - """ - env = self.envelope - if isinstance(env, Point): - xmin, ymin = env.tuple - xmax, ymax = xmin, ymin - else: - xmin, ymin = env[0][0] - xmax, ymax = env[0][2] - return (xmin, ymin, xmax, ymax) - - @property - def length(self): - """ - Returns the length of this Geometry (e.g., 0 for point, or the - circumfrence of a Polygon). - """ - return geos_length(self.ptr, byref(c_double())) - - def clone(self): - "Clones this Geometry." - return GEOSGeometry(geom_clone(self.ptr), srid=self.srid) - -# 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 -GEOS_CLASSES = {0 : Point, - 1 : LineString, - 2 : LinearRing, - 3 : Polygon, - 4 : MultiPoint, - 5 : MultiLineString, - 6 : MultiPolygon, - 7 : GeometryCollection, - } + # Property for controlling access to the GEOS object pointers. Using + # this raises an exception when the pointer is NULL, thus preventing + # the C library from attempting to access an invalid memory location. + ptr = property(_get_ptr, _set_ptr) diff --git a/django/contrib/gis/geos/collections.py b/django/contrib/gis/geos/collections.py index a69b2e7c847..e0a616d67ee 100644 --- a/django/contrib/gis/geos/collections.py +++ b/django/contrib/gis/geos/collections.py @@ -3,16 +3,17 @@ GeometryCollection, MultiPoint, MultiLineString, and MultiPolygon """ from ctypes import c_int, c_uint, byref -from types import TupleType, ListType -from django.contrib.gis.geos.base import GEOSGeometry from django.contrib.gis.geos.error import GEOSException, GEOSIndexError -from django.contrib.gis.geos.geometries import Point, LineString, LinearRing, Polygon -from django.contrib.gis.geos.libgeos import get_pointer_arr, GEOM_PTR -from django.contrib.gis.geos.prototypes import create_collection, destroy_geom, geom_clone, geos_typeid, get_cs, get_geomn +from django.contrib.gis.geos.geometry import GEOSGeometry +from django.contrib.gis.geos.libgeos import get_pointer_arr, GEOM_PTR, GEOS_PREPARE +from django.contrib.gis.geos.linestring import LineString, LinearRing +from django.contrib.gis.geos.point import Point +from django.contrib.gis.geos.polygon import Polygon +from django.contrib.gis.geos import prototypes as capi class GeometryCollection(GEOSGeometry): - _allowed = (Point, LineString, LinearRing, Polygon) _typeid = 7 + _minlength = 0 def __init__(self, *args, **kwargs): "Initializes a Geometry Collection from a sequence of Geometry objects." @@ -21,10 +22,10 @@ class GeometryCollection(GEOSGeometry): if not args: raise TypeError, 'Must provide at least one Geometry to initialize %s.' % self.__class__.__name__ - if len(args) == 1: + 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)): + if isinstance(args[0], (tuple, list)): init_geoms = args[0] else: init_geoms = args @@ -32,55 +33,55 @@ class GeometryCollection(GEOSGeometry): 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.') + # this is moved to list mixin super class + self._check_allowed(init_geoms) # Creating the geometry pointer array. - ngeoms = len(init_geoms) - geoms = get_pointer_arr(ngeoms) - for i in xrange(ngeoms): geoms[i] = geom_clone(init_geoms[i].ptr) - super(GeometryCollection, self).__init__(create_collection(c_int(self._typeid), byref(geoms), c_uint(ngeoms)), **kwargs) - - 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(geom_clone(get_geomn(self.ptr, index)), srid=self.srid) - - def __setitem__(self, index, geom): - "Sets the Geometry at the specified index." - self._checkindex(index) - if not isinstance(geom, self._allowed): - raise TypeError('Incompatible Geometry for collection.') - - ngeoms = len(self) - geoms = get_pointer_arr(ngeoms) - for i in xrange(ngeoms): - if i == index: - geoms[i] = geom_clone(geom.ptr) - else: - geoms[i] = geom_clone(get_geomn(self.ptr, i)) - - # Creating a new collection, and destroying the contents of the previous poiner. - prev_ptr = self.ptr - srid = self.srid - self._ptr = create_collection(c_int(self._typeid), byref(geoms), c_uint(ngeoms)) - if srid: self.srid = srid - destroy_geom(prev_ptr) + collection = self._create_collection(len(init_geoms), iter(init_geoms)) + super(GeometryCollection, self).__init__(collection, **kwargs) def __iter__(self): "Iterates over each Geometry in the Collection." for i in xrange(len(self)): - yield self.__getitem__(i) + yield self[i] def __len__(self): "Returns the number of geometries in this Collection." return self.num_geom - def _checkindex(self, index): - "Checks the given geometry index." - if index < 0 or index >= self.num_geom: - raise GEOSIndexError('invalid GEOS Geometry index: %s' % str(index)) + ### Methods for compatibility with ListMixin ### + @classmethod + def _create_collection(cls, length, items): + # Creating the geometry pointer array. + geoms = get_pointer_arr(length) + for i, g in enumerate(items): + # this is a little sloppy, but makes life easier + # allow GEOSGeometry types (python wrappers) or pointer types + geoms[i] = capi.geom_clone(getattr(g, 'ptr', g)) + + return capi.create_collection(c_int(cls._typeid), byref(geoms), c_uint(length)) + + def _getitem_internal(self, index): + return capi.get_geomn(self.ptr, index) + + def _getitem_external(self, index): + "Returns the Geometry from this Collection at the given index (0-based)." + # Checking the index and returning the corresponding GEOS geometry. + return GEOSGeometry(capi.geom_clone(self._getitem_internal(index)), srid=self.srid) + + def _set_collection(self, length, items): + "Create a new collection, and destroy the contents of the previous pointer." + prev_ptr = self.ptr + srid = self.srid + self.ptr = self._create_collection(length, items) + if srid: self.srid = srid + capi.destroy_geom(prev_ptr) + + # Because GeometryCollections need to be rebuilt upon the changing of a + # component geometry, these routines are set to their counterparts that + # rebuild the entire geometry. + _set_single = GEOSGeometry._set_single_rebuild + _assign_extended_slice = GEOSGeometry._assign_extended_slice_rebuild @property def kml(self): @@ -94,12 +95,34 @@ class GeometryCollection(GEOSGeometry): coords = tuple # MultiPoint, MultiLineString, and MultiPolygon class definitions. -class MultiPoint(GeometryCollection): +class MultiPoint(GeometryCollection): _allowed = Point _typeid = 4 -class MultiLineString(GeometryCollection): + +class MultiLineString(GeometryCollection): _allowed = (LineString, LinearRing) _typeid = 5 -class MultiPolygon(GeometryCollection): + + @property + def merged(self): + """ + Returns a LineString representing the line merge of this + MultiLineString. + """ + return self._topology(capi.geos_linemerge(self.ptr)) + +class MultiPolygon(GeometryCollection): _allowed = Polygon _typeid = 6 + + @property + def cascaded_union(self): + "Returns a cascaded union of this MultiPolygon." + if GEOS_PREPARE: + return GEOSGeometry(capi.geos_cascaded_union(self.ptr), self.srid) + else: + raise GEOSException('The cascaded union operation requires GEOS 3.1+.') + +# Setting the allowed types here since GeometryCollection is defined before +# its subclasses. +GeometryCollection._allowed = (Point, LineString, LinearRing, Polygon, MultiPoint, MultiLineString, MultiPolygon) diff --git a/django/contrib/gis/geos/coordseq.py b/django/contrib/gis/geos/coordseq.py index bc0c4794d49..027d34e740b 100644 --- a/django/contrib/gis/geos/coordseq.py +++ b/django/contrib/gis/geos/coordseq.py @@ -4,15 +4,16 @@ LineString, and LinearRing geometries. """ from ctypes import c_double, c_uint, byref -from types import ListType, TupleType +from django.contrib.gis.geos.base import GEOSBase, numpy from django.contrib.gis.geos.error import GEOSException, GEOSIndexError -from django.contrib.gis.geos.libgeos import CS_PTR, HAS_NUMPY -from django.contrib.gis.geos.prototypes import cs_clone, cs_getdims, cs_getordinate, cs_getsize, cs_setordinate -if HAS_NUMPY: from numpy import ndarray +from django.contrib.gis.geos.libgeos import CS_PTR +from django.contrib.gis.geos import prototypes as capi -class GEOSCoordSeq(object): +class GEOSCoordSeq(GEOSBase): "The internal representation of a list of coordinates inside a Geometry." + ptr_type = CS_PTR + #### Python 'magic' routines #### def __init__(self, ptr, z=False): "Initializes from a GEOS pointer." @@ -44,9 +45,9 @@ class GEOSCoordSeq(object): def __setitem__(self, index, value): "Sets the coordinate sequence value at the given index." # Checking the input value - if isinstance(value, (ListType, TupleType)): + if isinstance(value, (list, tuple)): pass - elif HAS_NUMPY and isinstance(value, ndarray): + elif numpy and isinstance(value, numpy.ndarray): pass else: raise TypeError('Must set coordinate with a sequence (list, tuple, or numpy array).') @@ -76,27 +77,18 @@ class GEOSCoordSeq(object): if dim < 0 or dim > 2: raise GEOSException('invalid ordinate dimension "%d"' % dim) - @property - def ptr(self): - """ - Property for controlling access to coordinate sequence pointer, - preventing attempted access to a NULL memory location. - """ - if self._ptr: return self._ptr - else: raise GEOSException('NULL coordinate sequence pointer encountered.') - #### Ordinate getting and setting routines #### def getOrdinate(self, dimension, index): "Returns the value for the given dimension and index." self._checkindex(index) self._checkdim(dimension) - return cs_getordinate(self.ptr, index, dimension, byref(c_double())) + return capi.cs_getordinate(self.ptr, index, dimension, byref(c_double())) def setOrdinate(self, dimension, index, value): "Sets the value for the given dimension and index." self._checkindex(index) self._checkdim(dimension) - cs_setordinate(self.ptr, index, dimension, value) + capi.cs_setordinate(self.ptr, index, dimension, value) def getX(self, index): "Get the X value at the index." @@ -126,12 +118,12 @@ class GEOSCoordSeq(object): @property def size(self): "Returns the size of this coordinate sequence." - return cs_getsize(self.ptr, byref(c_uint())) + return capi.cs_getsize(self.ptr, byref(c_uint())) @property def dims(self): "Returns the dimensions of this coordinate sequence." - return cs_getdims(self.ptr, byref(c_uint())) + return capi.cs_getdims(self.ptr, byref(c_uint())) @property def hasz(self): @@ -144,7 +136,7 @@ class GEOSCoordSeq(object): ### Other Methods ### def clone(self): "Clones this coordinate sequence." - return GEOSCoordSeq(cs_clone(self.ptr), self.hasz) + return GEOSCoordSeq(capi.cs_clone(self.ptr), self.hasz) @property def kml(self): diff --git a/django/contrib/gis/geos/factory.py b/django/contrib/gis/geos/factory.py new file mode 100644 index 00000000000..df2997695e7 --- /dev/null +++ b/django/contrib/gis/geos/factory.py @@ -0,0 +1,23 @@ +from django.contrib.gis.geos.geometry import GEOSGeometry, wkt_regex, hex_regex + +def fromfile(file_h): + """ + Given a string file name, returns a GEOSGeometry. The file may contain WKB, + WKT, or HEX. + """ + # If given a file name, get a real handle. + if isinstance(file_h, basestring): + file_h = open(file_h, 'rb') + + # Reading in the file's contents, + buf = file_h.read() + + # If we get WKB need to wrap in buffer(), so run through regexes. + if wkt_regex.match(buf) or hex_regex.match(buf): + return GEOSGeometry(buf) + else: + return GEOSGeometry(buffer(buf)) + +def fromstr(string, **kwargs): + "Given a string value, returns a GEOSGeometry object." + return GEOSGeometry(string, **kwargs) diff --git a/django/contrib/gis/geos/geometries.py b/django/contrib/gis/geos/geometries.py deleted file mode 100644 index c5420e93af4..00000000000 --- a/django/contrib/gis/geos/geometries.py +++ /dev/null @@ -1,391 +0,0 @@ -""" - 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_uint, byref -from django.contrib.gis.geos.base import GEOSGeometry -from django.contrib.gis.geos.coordseq import GEOSCoordSeq -from django.contrib.gis.geos.error import GEOSException, GEOSIndexError -from django.contrib.gis.geos.libgeos import get_pointer_arr, GEOM_PTR, HAS_NUMPY -from django.contrib.gis.geos.prototypes import * -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: - >>> p = Point((5, 23)) # 2D point, passed in as a tuple - >>> p = Point(5, 23, 8) # 3D point, passed in with individual parameters - """ - - if isinstance(x, (tuple, list)): - # 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, (int, float, long)) and isinstance(y, (int, float, long)): - # Here X, Y, and (optionally) Z were passed in individually, as parameters. - if isinstance(z, (int, float, long)): - ndim = 3 - coords = [x, y, z] - else: - ndim = 2 - coords = [x, y] - else: - raise TypeError('Invalid parameters given for Point initialization.') - - # Creating the coordinate sequence, and setting X, Y, [Z] - cs = create_cs(c_uint(1), c_uint(ndim)) - cs_setx(cs, 0, coords[0]) - cs_sety(cs, 0, coords[1]) - if ndim == 3: cs_setz(cs, 0, coords[2]) - - # Initializing using the address returned from the GEOS - # createPoint factory. - super(Point, self).__init__(create_point(cs), srid=srid) - - def __len__(self): - "Returns the number of dimensions for this Point (either 0, 2 or 3)." - if self.empty: return 0 - if self.hasz: return 3 - else: return 2 - - def get_x(self): - "Returns the X component of the Point." - return self._cs.getOrdinate(0, 0) - - def set_x(self, value): - "Sets the X component of the Point." - self._cs.setOrdinate(0, 0, value) - - def get_y(self): - "Returns the Y component of the Point." - return self._cs.getOrdinate(1, 0) - - def set_y(self, value): - "Sets the Y component of the Point." - self._cs.setOrdinate(1, 0, value) - - def get_z(self): - "Returns the Z component of the Point." - if self.hasz: - return self._cs.getOrdinate(2, 0) - else: - return None - - def set_z(self, value): - "Sets the Z component of the Point." - if self.hasz: - self._cs.setOrdinate(2, 0, value) - else: - raise GEOSException('Cannot set Z on 2D Point.') - - # X, Y, Z properties - x = property(get_x, set_x) - y = property(get_y, set_y) - z = property(get_z, set_z) - - ### Tuple setting and retrieval routines. ### - def get_coords(self): - "Returns a tuple of the point." - return self._cs.tuple - - def set_coords(self, tup): - "Sets the coordinates of the point with the given tuple." - self._cs[0] = tup - - # The tuple and coords properties - tuple = property(get_coords, set_coords) - coords = tuple - -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. - - 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)) - """ - # If only one argument provided, set the coords array appropriately - if len(args) == 1: coords = args[0] - else: coords = args - - if isinstance(coords, (tuple, list)): - # 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) - 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], (tuple, list, Point)): - raise TypeError('each coordinate should be a sequence (list or tuple)') - if len(coords[i]) != ndim: raise TypeError('Dimension mismatch.') - numpy_coords = False - elif HAS_NUMPY and isinstance(coords, ndarray): - shape = coords.shape # Using numpy's shape. - if len(shape) != 2: raise TypeError('Too many dimensions.') - self._checkdim(shape[1]) - ncoords = shape[0] - ndim = shape[1] - numpy_coords = True - else: - raise TypeError('Invalid initialization input for LineStrings.') - - # Creating a coordinate sequence object because it is easier to - # set the points using GEOSCoordSeq.__setitem__(). - cs = GEOSCoordSeq(create_cs(ncoords, ndim), z=bool(ndim==3)) - 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 correct initialization function - if kwargs.get('ring', False): - func = create_linearring - else: - func = create_linestring - - # 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. - super(LineString, self).__init__(func(cs.ptr), srid=srid) - - def __getitem__(self, index): - "Gets the point at the specified index." - return self._cs[index] - - def __setitem__(self, index, value): - "Sets the point at the specified index, e.g., line_str[0] = (1, 2)." - self._cs[index] = value - - def __iter__(self): - "Allows iteration over this LineString." - for i in xrange(len(self)): - yield self[i] - - def __len__(self): - "Returns the number of points in this LineString." - return len(self._cs) - - def _checkdim(self, dim): - if dim not in (2, 3): raise TypeError('Dimension mismatch.') - - #### Sequence Properties #### - @property - def tuple(self): - "Returns a tuple version of the geometry from the coordinate sequence." - return self._cs.tuple - coords = 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))] - if HAS_NUMPY: return array(lst) # ARRRR! - else: return lst - - @property - def array(self): - "Returns a numpy array for the LineString." - return self._listarr(self._cs.__getitem__) - - @property - def x(self): - "Returns a list or numpy array of the X variable." - return self._listarr(self._cs.getX) - - @property - def y(self): - "Returns a list or numpy array of the Y variable." - return self._listarr(self._cs.getY) - - @property - def z(self): - "Returns a list or numpy array of the Z variable." - 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, *args, **kwargs): - "Overriding the initialization function to set the ring keyword." - kwargs['ring'] = True # Setting the ring keyword argument to True - super(LinearRing, self).__init__(*args, **kwargs) - -class Polygon(GEOSGeometry): - - def __init__(self, *args, **kwargs): - """ - Initializes on an exterior ring and a sequence of holes (both - instances may be either LinearRing instances, or a tuple/list - that may be constructed into a LinearRing). - - Examples of initialization, where shell, hole1, and hole2 are - valid LinearRing geometries: - >>> poly = Polygon(shell, hole1, hole2) - >>> poly = Polygon(shell, (hole1, hole2)) - - Example where a tuple parameters are used: - >>> poly = Polygon(((0, 0), (0, 10), (10, 10), (0, 10), (0, 0)), - ((4, 4), (4, 6), (6, 6), (6, 4), (4, 4))) - """ - 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:] - n_holes = len(init_holes) - - # If initialized as Polygon(shell, (LinearRing, LinearRing)) [for backward-compatibility] - if n_holes == 1 and isinstance(init_holes[0], (tuple, list)) and \ - (len(init_holes[0]) == 0 or isinstance(init_holes[0][0], LinearRing)): - init_holes = init_holes[0] - n_holes = len(init_holes) - - # Ensuring the exterior ring and holes parameters are LinearRing objects - # or may be instantiated into LinearRings. - ext_ring = self._construct_ring(ext_ring, 'Exterior parameter must be a LinearRing or an object that can initialize a LinearRing.') - holes_list = [] # Create new list, cause init_holes is a tuple. - for i in xrange(n_holes): - holes_list.append(self._construct_ring(init_holes[i], 'Holes parameter must be a sequence of LinearRings or objects that can initialize to LinearRings')) - - # Why another loop? Because if a TypeError is raised, cloned pointers will - # be around that can't be cleaned up. - holes = get_pointer_arr(n_holes) - for i in xrange(n_holes): holes[i] = geom_clone(holes_list[i].ptr) - - # Getting the shell pointer address. - shell = geom_clone(ext_ring.ptr) - - # Calling with the GEOS createPolygon factory. - super(Polygon, self).__init__(create_polygon(shell, byref(holes), c_uint(n_holes)), **kwargs) - - 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 at the given index (e.g., poly[1] and poly[2] would - return the first and second interior ring, respectively). - """ - if index == 0: - return self.exterior_ring - else: - # Getting the interior ring, have to subtract 1 from the index. - return self.get_interior_ring(index-1) - - def __setitem__(self, index, ring): - "Sets the ring at the specified index with the given ring." - # Checking the index and ring parameters. - self._checkindex(index) - if not isinstance(ring, LinearRing): - raise TypeError('must set Polygon index with a LinearRing object') - - # Getting the shell - if index == 0: - shell = geom_clone(ring.ptr) - else: - shell = geom_clone(get_extring(self.ptr)) - - # Getting the interior rings (holes) - nholes = len(self)-1 - if nholes > 0: - holes = get_pointer_arr(nholes) - for i in xrange(nholes): - if i == (index-1): - holes[i] = geom_clone(ring.ptr) - else: - holes[i] = geom_clone(get_intring(self.ptr, i)) - holes_param = byref(holes) - else: - holes_param = None - - # Getting the current pointer, replacing with the newly constructed - # geometry, and destroying the old geometry. - prev_ptr = self.ptr - srid = self.srid - self._ptr = create_polygon(shell, holes_param, c_uint(nholes)) - if srid: self.srid = srid - destroy_geom(prev_ptr) - - def __iter__(self): - "Iterates over each ring in the polygon." - for i in xrange(len(self)): - yield self[i] - - def __len__(self): - "Returns the number of rings in this Polygon." - return self.num_interior_rings + 1 - - def _checkindex(self, index): - "Internal routine for checking the given ring index." - if index < 0 or index >= len(self): - raise GEOSIndexError('invalid Polygon ring index: %s' % index) - - def _construct_ring(self, param, msg=''): - "Helper routine for trying to construct a ring from the given parameter." - if isinstance(param, LinearRing): return param - try: - ring = LinearRing(param) - return ring - except TypeError: - raise TypeError(msg) - - 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. - """ - self._checkindex(ring_i+1) - return GEOSGeometry(geom_clone(get_intring(self.ptr, ring_i)), srid=self.srid) - - #### Polygon Properties #### - @property - def num_interior_rings(self): - "Returns the number of interior rings." - # Getting the number of rings - return get_nrings(self.ptr) - - def get_ext_ring(self): - "Gets the exterior ring of the Polygon." - return GEOSGeometry(geom_clone(get_extring(self.ptr)), srid=self.srid) - - def set_ext_ring(self, ring): - "Sets the exterior ring of the Polygon." - self[0] = ring - - # properties for the exterior ring/shell - exterior_ring = property(get_ext_ring, set_ext_ring) - shell = exterior_ring - - @property - def tuple(self): - "Gets the tuple for each ring in this Polygon." - return tuple([self[i].tuple for i in xrange(len(self))]) - coords = tuple - - @property - def kml(self): - "Returns the KML representation of this Polygon." - inner_kml = ''.join(["%s" % self[i+1].kml - for i in xrange(self.num_interior_rings)]) - return "%s%s" % (self[0].kml, inner_kml) diff --git a/django/contrib/gis/geos/geometry.py b/django/contrib/gis/geos/geometry.py new file mode 100644 index 00000000000..418dc10c678 --- /dev/null +++ b/django/contrib/gis/geos/geometry.py @@ -0,0 +1,622 @@ +""" + This module contains the 'base' GEOSGeometry object -- all GEOS Geometries + inherit from this object. +""" +# Python, ctypes and types dependencies. +import re +from ctypes import addressof, byref, c_double, c_size_t + +# super-class for mutable list behavior +from django.contrib.gis.geos.mutable_list import ListMixin + +# GEOS-related dependencies. +from django.contrib.gis.geos.base import GEOSBase, gdal +from django.contrib.gis.geos.coordseq import GEOSCoordSeq +from django.contrib.gis.geos.error import GEOSException, GEOSIndexError +from django.contrib.gis.geos.libgeos import GEOM_PTR, GEOS_PREPARE +from django.contrib.gis.geos.mutable_list import ListMixin + +# All other functions in this module come from the ctypes +# prototypes module -- which handles all interaction with +# the underlying GEOS library. +from django.contrib.gis.geos import prototypes as capi +from django.contrib.gis.geos import io + +# Regular expression for recognizing HEXEWKB and WKT. A prophylactic measure +# to prevent potentially malicious input from reaching the underlying C +# library. Not a substitute for good web security programming practices. +hex_regex = re.compile(r'^[0-9A-F]+$', re.I) +wkt_regex = re.compile(r'^(SRID=(?P\d+);)?(?P(POINT|LINESTRING|LINEARRING|POLYGON|MULTIPOINT|MULTILINESTRING|MULTIPOLYGON|GEOMETRYCOLLECTION)[ACEGIMLONPSRUTY\d,\.\-\(\) ]+)$', re.I) + +class GEOSGeometry(GEOSBase, ListMixin): + "A class that, generally, encapsulates a GEOS geometry." + + # Raise GEOSIndexError instead of plain IndexError + # (see ticket #4740 and GEOSIndexError docstring) + _IndexError = GEOSIndexError + + ptr_type = GEOM_PTR + + #### Python 'magic' routines #### + def __init__(self, geo_input, srid=None): + """ + The base constructor for GEOS geometry objects, and may take the + following inputs: + + * strings: + - WKT + - HEXEWKB (a PostGIS-specific canonical form) + - GeoJSON (requires GDAL) + * buffer: + - WKB + + 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, basestring): + if isinstance(geo_input, unicode): + # Encoding to ASCII, WKT or HEXEWKB doesn't need any more. + geo_input = geo_input.encode('ascii') + + wkt_m = wkt_regex.match(geo_input) + if wkt_m: + # Handling WKT input. + if wkt_m.group('srid'): srid = int(wkt_m.group('srid')) + g = io.wkt_r.read(wkt_m.group('wkt')) + elif hex_regex.match(geo_input): + # Handling HEXEWKB input. + g = io.wkb_r.read(geo_input) + elif gdal.GEOJSON and gdal.geometries.json_regex.match(geo_input): + # Handling GeoJSON input. + g = io.wkb_r.read(gdal.OGRGeometry(geo_input).wkb) + else: + raise ValueError('String or unicode input unrecognized as WKT EWKT, and HEXEWKB.') + elif isinstance(geo_input, GEOM_PTR): + # When the input is a pointer to a geomtry (GEOM_PTR). + g = geo_input + elif isinstance(geo_input, buffer): + # When the input is a buffer (WKB). + g = io.wkb_r.read(geo_input) + elif isinstance(geo_input, GEOSGeometry): + g = capi.geom_clone(geo_input.ptr) + else: + # Invalid geometry type. + raise TypeError('Improper geometry input type: %s' % str(type(geo_input))) + + if bool(g): + # Setting the pointer object with a valid pointer. + self.ptr = g + else: + raise GEOSException('Could not initialize GEOS Geometry with given input.') + + # Post-initialization setup. + self._post_init(srid) + + def _post_init(self, srid): + "Helper routine for performing post-initialization setup." + # 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_typeid] + + # Setting the coordinate sequence for the geometry (will be None on + # geometries that do not have coordinate sequences) + self._set_cs() + + def __del__(self): + """ + Destroys this Geometry; in other words, frees the memory used by the + GEOS C++ object. + """ + if self._ptr: capi.destroy_geom(self._ptr) + + def __copy__(self): + """ + Returns a clone because the copy of a GEOSGeometry may contain an + invalid pointer location if the original is garbage collected. + """ + return self.clone() + + def __deepcopy__(self, memodict): + """ + The `deepcopy` routine is used by the `Node` class of django.utils.tree; + thus, the protocol routine needs to be implemented to return correct + copies (clones) of these GEOS objects, which use C pointers. + """ + return self.clone() + + def __str__(self): + "WKT is used for the string representation." + return self.wkt + + def __repr__(self): + "Short-hand representation because WKT may be very large." + return '<%s object at %s>' % (self.geom_type, hex(addressof(self.ptr))) + + # Pickling support + def __getstate__(self): + # The pickled state is simply a tuple of the WKB (in string form) + # and the SRID. + return str(self.wkb), self.srid + + def __setstate__(self, state): + # Instantiating from the tuple state that was pickled. + wkb, srid = state + ptr = capi.from_wkb(wkb, len(wkb)) + if not ptr: raise GEOSException('Invalid Geometry loaded from pickled state.') + self.ptr = ptr + self._post_init(srid) + + # Comparison operators + def __eq__(self, other): + """ + Equivalence testing, a Geometry may be compared with another Geometry + or a WKT representation. + """ + if isinstance(other, basestring): + return self.wkt == other + elif isinstance(other, GEOSGeometry): + return self.equals_exact(other) + else: + return False + + def __ne__(self, other): + "The not equals operator." + return not (self == 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 #### + @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 _set_cs(self): + "Sets the coordinate sequence for this Geometry." + if self.has_cs: + self._cs = GEOSCoordSeq(capi.get_cs(self.ptr), self.hasz) + else: + self._cs = None + + @property + def coord_seq(self): + "Returns a clone of the coordinate sequence for this Geometry." + if self.has_cs: + return self._cs.clone() + + #### Geometry Info #### + @property + def geom_type(self): + "Returns a string representing the Geometry type, e.g. 'Polygon'" + return capi.geos_type(self.ptr) + + @property + def geom_typeid(self): + "Returns an integer representing the Geometry type." + return capi.geos_typeid(self.ptr) + + @property + def num_geom(self): + "Returns the number of geometries in the Geometry." + return capi.get_num_geoms(self.ptr) + + @property + def num_coords(self): + "Returns the number of coordinates in the Geometry." + return capi.get_num_coords(self.ptr) + + @property + def num_points(self): + "Returns the number points, or coordinates, in the Geometry." + return self.num_coords + + @property + def dims(self): + "Returns the dimension of this Geometry (0=point, 1=line, 2=surface)." + return capi.get_dims(self.ptr) + + def normalize(self): + "Converts this Geometry to normal form (or canonical form)." + return capi.geos_normalize(self.ptr) + + #### Unary predicates #### + @property + def empty(self): + """ + Returns a boolean indicating whether the set of points in this Geometry + are empty. + """ + return capi.geos_isempty(self.ptr) + + @property + def hasz(self): + "Returns whether the geometry has a 3D dimension." + return capi.geos_hasz(self.ptr) + + @property + def ring(self): + "Returns whether or not the geometry is a ring." + return capi.geos_isring(self.ptr) + + @property + def simple(self): + "Returns false if the Geometry not simple." + return capi.geos_issimple(self.ptr) + + @property + def valid(self): + "This property tests the validity of this Geometry." + return capi.geos_isvalid(self.ptr) + + #### Binary predicates. #### + def contains(self, other): + "Returns true if other.within(this) returns true." + return capi.geos_contains(self.ptr, other.ptr) + + 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). + """ + return capi.geos_crosses(self.ptr, other.ptr) + + def disjoint(self, other): + """ + Returns true if the DE-9IM intersection matrix for the two Geometries + is FF*FF****. + """ + return capi.geos_disjoint(self.ptr, other.ptr) + + def equals(self, other): + """ + Returns true if the DE-9IM intersection matrix for the two Geometries + is T*F**FFF*. + """ + return capi.geos_equals(self.ptr, other.ptr) + + def equals_exact(self, other, tolerance=0): + """ + Returns true if the two Geometries are exactly equal, up to a + specified tolerance. + """ + return capi.geos_equalsexact(self.ptr, other.ptr, float(tolerance)) + + def intersects(self, other): + "Returns true if disjoint returns false." + return capi.geos_intersects(self.ptr, other.ptr) + + 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). + """ + return capi.geos_overlaps(self.ptr, other.ptr) + + 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. + """ + if not isinstance(pattern, basestring) or len(pattern) > 9: + raise GEOSException('invalid intersection matrix pattern') + return capi.geos_relatepattern(self.ptr, other.ptr, pattern) + + def touches(self, other): + """ + Returns true if the DE-9IM intersection matrix for the two Geometries + is FT*******, F**T***** or F***T****. + """ + return capi.geos_touches(self.ptr, other.ptr) + + def within(self, other): + """ + Returns true if the DE-9IM intersection matrix for the two Geometries + is T*F**F***. + """ + return capi.geos_within(self.ptr, other.ptr) + + #### SRID Routines #### + def get_srid(self): + "Gets the SRID for the geometry, returns None if no SRID is set." + s = capi.geos_get_srid(self.ptr) + if s == 0: return None + else: return s + + def set_srid(self, srid): + "Sets the SRID for the geometry." + capi.geos_set_srid(self.ptr, srid) + srid = property(get_srid, set_srid) + + #### Output Routines #### + @property + def ewkt(self): + "Returns the EWKT (WKT + SRID) of the Geometry." + if self.get_srid(): return 'SRID=%s;%s' % (self.srid, self.wkt) + else: return self.wkt + + @property + def wkt(self): + "Returns the WKT (Well-Known Text) of the Geometry." + return io.wkt_w.write(self.ptr) + + @property + def hex(self): + """ + Returns the HEX of the Geometry -- please note that the SRID is not + included in this representation, because the GEOS C library uses + -1 by default, even if the SRID is set. + """ + # A possible faster, all-python, implementation: + # str(self.wkb).encode('hex') + return io.wkb_w.write_hex(self.ptr) + + @property + def json(self): + """ + Returns GeoJSON representation of this Geometry if GDAL 1.5+ + is installed. + """ + if gdal.GEOJSON: + return self.ogr.json + else: + raise GEOSException('GeoJSON output only supported on GDAL 1.5+.') + geojson = json + + @property + def wkb(self): + "Returns the WKB of the Geometry as a buffer." + return io.wkb_w.write(self.ptr) + + @property + def kml(self): + "Returns the KML representation of this Geometry." + gtype = self.geom_type + return '<%s>%s' % (gtype, self.coord_seq.kml, gtype) + + @property + def prepared(self): + """ + Returns a PreparedGeometry corresponding to this geometry -- it is + optimized for the contains, intersects, and covers operations. + """ + if GEOS_PREPARE: + return PreparedGeometry(self) + else: + raise GEOSException('GEOS 3.1+ required for prepared geometry support.') + + #### GDAL-specific output routines #### + @property + def ogr(self): + "Returns the OGR Geometry for this Geometry." + if gdal.HAS_GDAL: + if self.srid: + return gdal.OGRGeometry(self.wkb, self.srid) + else: + return gdal.OGRGeometry(self.wkb) + else: + raise GEOSException('GDAL required to convert to an OGRGeometry.') + + @property + def srs(self): + "Returns the OSR SpatialReference for SRID of this Geometry." + if gdal.HAS_GDAL: + if self.srid: + return gdal.SpatialReference(self.srid) + else: + return None + else: + raise GEOSException('GDAL required to return a SpatialReference object.') + + @property + def crs(self): + "Alias for `srs` property." + return self.srs + + def transform(self, ct, clone=False): + """ + Requires GDAL. Transforms the geometry according to the given + transformation object, which may be an integer SRID, and WKT or + PROJ.4 string. By default, the geometry is transformed in-place and + nothing is returned. However if the `clone` keyword is set, then this + geometry will not be modified and a transformed clone will be returned + instead. + """ + srid = self.srid + if gdal.HAS_GDAL and srid: + # Creating an OGR Geometry, which is then transformed. + g = gdal.OGRGeometry(self.wkb, srid) + g.transform(ct) + # Getting a new GEOS pointer + ptr = io.wkb_r.read(g.wkb) + if clone: + # User wants a cloned transformed geometry returned. + return GEOSGeometry(ptr, srid=g.srid) + if ptr: + # Reassigning pointer, and performing post-initialization setup + # again due to the reassignment. + capi.destroy_geom(self.ptr) + self.ptr = ptr + self._post_init(g.srid) + else: + raise GEOSException('Transformed WKB was invalid.') + + #### Topology Routines #### + def _topology(self, gptr): + "Helper routine to return Geometry from the given pointer." + return GEOSGeometry(gptr, srid=self.srid) + + @property + def boundary(self): + "Returns the boundary as a newly allocated Geometry object." + return self._topology(capi.geos_boundary(self.ptr)) + + 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) + """ + return self._topology(capi.geos_buffer(self.ptr, width, quadsegs)) + + @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). + """ + return self._topology(capi.geos_centroid(self.ptr)) + + @property + def convex_hull(self): + """ + Returns the smallest convex Polygon that contains all the points + in the Geometry. + """ + return self._topology(capi.geos_convexhull(self.ptr)) + + def difference(self, other): + """ + Returns a Geometry representing the points making up this Geometry + that do not make up other. + """ + return self._topology(capi.geos_difference(self.ptr, other.ptr)) + + @property + def envelope(self): + "Return the envelope for this geometry (a polygon)." + return self._topology(capi.geos_envelope(self.ptr)) + + def intersection(self, other): + "Returns a Geometry representing the points shared by this Geometry and other." + return self._topology(capi.geos_intersection(self.ptr, other.ptr)) + + @property + def point_on_surface(self): + "Computes an interior point of this Geometry." + return self._topology(capi.geos_pointonsurface(self.ptr)) + + def relate(self, other): + "Returns the DE-9IM intersection matrix for this Geometry and the other." + return capi.geos_relate(self.ptr, other.ptr) + + def simplify(self, tolerance=0.0, preserve_topology=False): + """ + Returns the Geometry, simplified using the Douglas-Peucker algorithm + to the specified tolerance (higher tolerance => less points). If no + tolerance provided, defaults to 0. + + By default, this function does not preserve topology - e.g. polygons can + be split, collapse to lines or disappear holes can be created or + disappear, and lines can cross. By specifying preserve_topology=True, + the result will have the same dimension and number of components as the + input. This is significantly slower. + """ + if preserve_topology: + return self._topology(capi.geos_preservesimplify(self.ptr, tolerance)) + else: + return self._topology(capi.geos_simplify(self.ptr, tolerance)) + + 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. + """ + return self._topology(capi.geos_symdifference(self.ptr, other.ptr)) + + def union(self, other): + "Returns a Geometry representing all the points in this Geometry and other." + return self._topology(capi.geos_union(self.ptr, other.ptr)) + + #### Other Routines #### + @property + def area(self): + "Returns the area of the Geometry." + return capi.geos_area(self.ptr, byref(c_double())) + + 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. + """ + if not isinstance(other, GEOSGeometry): + raise TypeError('distance() works only on other GEOS Geometries.') + return capi.geos_distance(self.ptr, other.ptr, byref(c_double())) + + @property + def extent(self): + """ + Returns the extent of this geometry as a 4-tuple, consisting of + (xmin, ymin, xmax, ymax). + """ + env = self.envelope + if isinstance(env, Point): + xmin, ymin = env.tuple + xmax, ymax = xmin, ymin + else: + xmin, ymin = env[0][0] + xmax, ymax = env[0][2] + return (xmin, ymin, xmax, ymax) + + @property + def length(self): + """ + Returns the length of this Geometry (e.g., 0 for point, or the + circumfrence of a Polygon). + """ + return capi.geos_length(self.ptr, byref(c_double())) + + def clone(self): + "Clones this Geometry." + return GEOSGeometry(capi.geom_clone(self.ptr), srid=self.srid) + +# Class mapping dictionary. Has to be at the end to avoid import +# conflicts with GEOSGeometry. +from django.contrib.gis.geos.linestring import LineString, LinearRing +from django.contrib.gis.geos.point import Point +from django.contrib.gis.geos.polygon import Polygon +from django.contrib.gis.geos.collections import GeometryCollection, MultiPoint, MultiLineString, MultiPolygon +GEOS_CLASSES = {0 : Point, + 1 : LineString, + 2 : LinearRing, + 3 : Polygon, + 4 : MultiPoint, + 5 : MultiLineString, + 6 : MultiPolygon, + 7 : GeometryCollection, + } + +# If supported, import the PreparedGeometry class. +if GEOS_PREPARE: + from django.contrib.gis.geos.prepared import PreparedGeometry diff --git a/django/contrib/gis/geos/io.py b/django/contrib/gis/geos/io.py new file mode 100644 index 00000000000..d50989854de --- /dev/null +++ b/django/contrib/gis/geos/io.py @@ -0,0 +1,109 @@ +""" +Module that holds classes for performing I/O operations on GEOS geometry +objects. Specifically, this has Python implementations of WKB/WKT +reader and writer classes. +""" +from ctypes import byref, c_size_t +from django.contrib.gis.geos.base import GEOSBase +from django.contrib.gis.geos.error import GEOSException +from django.contrib.gis.geos.libgeos import GEOM_PTR +from django.contrib.gis.geos.prototypes import io as capi + +class IOBase(GEOSBase): + "Base class for IO objects that that have `destroy` method." + def __init__(self): + # Getting the pointer with the constructor. + self.ptr = self.constructor() + + def __del__(self): + # Cleaning up with the appropriate destructor. + if self._ptr: self.destructor(self._ptr) + + def _get_geom_ptr(self, geom): + if hasattr(geom, 'ptr'): geom = geom.ptr + if not isinstance(geom, GEOM_PTR): raise TypeError + return geom + +### WKT Reading and Writing objects ### +class WKTReader(IOBase): + constructor = capi.wkt_reader_create + destructor = capi.wkt_reader_destroy + ptr_type = capi.WKT_READ_PTR + + def read(self, wkt, ptr=False): + if not isinstance(wkt, basestring): raise TypeError + return capi.wkt_reader_read(self.ptr, wkt) + +class WKTWriter(IOBase): + constructor = capi.wkt_writer_create + destructor = capi.wkt_writer_destroy + ptr_type = capi.WKT_WRITE_PTR + + def write(self, geom): + return capi.wkt_writer_write(self.ptr, self._get_geom_ptr(geom)) + +### WKB Reading and Writing objects ### +class WKBReader(IOBase): + constructor = capi.wkb_reader_create + destructor = capi.wkb_reader_destroy + ptr_type = capi.WKB_READ_PTR + + def read(self, wkb): + if isinstance(wkb, buffer): + wkb_s = str(wkb) + return capi.wkb_reader_read(self.ptr, wkb_s, len(wkb_s)) + elif isinstance(wkb, basestring): + return capi.wkb_reader_read_hex(self.ptr, wkb, len(wkb)) + else: + raise TypeError + +class WKBWriter(IOBase): + constructor = capi.wkb_writer_create + destructor = capi.wkb_writer_destroy + ptr_type = capi.WKB_READ_PTR + + def write(self, geom): + return buffer(capi.wkb_writer_write(self.ptr, self._get_geom_ptr(geom), byref(c_size_t()))) + + def write_hex(self, geom): + return capi.wkb_writer_write_hex(self.ptr, self._get_geom_ptr(geom), byref(c_size_t())) + + ### WKBWriter Properties ### + + # Property for getting/setting the byteorder. + def _get_byteorder(self): + return capi.wkb_writer_get_byteorder(self.ptr) + + def _set_byteorder(self, order): + if not order in (0, 1): raise ValueError('Byte order parameter must be 0 (Big Endian) or 1 (Little Endian).') + capi.wkb_writer_set_byteorder(self.ptr, order) + + byteorder = property(_get_byteorder, _set_byteorder) + + # Property for getting/setting the output dimension. + def _get_outdim(self): + return capi.wkb_writer_get_outdim(self.ptr) + + def _set_outdim(self, new_dim): + if not new_dim in (2, 3): raise ValueError('WKB output dimension must be 2 or 3') + capi.wkb_writer_set_outdim(self.ptr, new_dim) + + outdim = property(_get_outdim, _set_outdim) + + # Property for getting/setting the include srid flag. + def _get_include_srid(self): + return bool(ord(capi.wkb_writer_get_include_srid(self.ptr))) + + def _set_include_srid(self, include): + if bool(include): flag = chr(1) + else: flag = chr(0) + capi.wkb_writer_set_include_srid(self.ptr, flag) + + srid = property(_get_include_srid, _set_include_srid) + +# Instances of the WKT and WKB reader/writer objects. +wkt_r = WKTReader() +wkt_w = WKTWriter() +wkb_r = WKBReader() +wkb_w = WKBWriter() + diff --git a/django/contrib/gis/geos/libgeos.py b/django/contrib/gis/geos/libgeos.py index c8e938cb00d..0839a620c78 100644 --- a/django/contrib/gis/geos/libgeos.py +++ b/django/contrib/gis/geos/libgeos.py @@ -11,13 +11,6 @@ from ctypes import c_char_p, Structure, CDLL, CFUNCTYPE, POINTER from ctypes.util import find_library from django.contrib.gis.geos.error import GEOSException -# NumPy supported? -try: - from numpy import array, ndarray - HAS_NUMPY = True -except ImportError: - HAS_NUMPY = False - # Custom library path set? try: from django.conf import settings @@ -37,18 +30,18 @@ elif os.name == 'posix': else: raise ImportError('Unsupported OS "%s"' % os.name) -# Using the ctypes `find_library` utility to find the the path to the GEOS -# shared library. This is better than manually specifiying each library name +# Using the ctypes `find_library` utility to find the the path to the GEOS +# shared library. This is better than manually specifiying each library name # and extension (e.g., libgeos_c.[so|so.1|dylib].). -if lib_names: +if lib_names: for lib_name in lib_names: lib_path = find_library(lib_name) if not lib_path is None: break # No GEOS library could be found. -if lib_path is None: +if lib_path is None: raise ImportError('Could not find the GEOS library (tried "%s"). ' - 'Try setting GEOS_LIBRARY_PATH in your settings.' % + 'Try setting GEOS_LIBRARY_PATH in your settings.' % '", "'.join(lib_names)) # Getting the GEOS C library. The C interface (CDLL) is used for @@ -65,7 +58,7 @@ def notice_h(fmt, lst, output_h=sys.stdout): try: warn_msg = fmt % lst except: - warn_msg = fmt + warn_msg = fmt output_h.write('GEOS_NOTICE: %s\n' % warn_msg) notice_h = NOTICEFUNC(notice_h) @@ -88,28 +81,30 @@ lgeos.initGEOS(notice_h, error_h) # Opaque GEOS geometry structures, used for GEOM_PTR and CS_PTR class GEOSGeom_t(Structure): pass +class GEOSPrepGeom_t(Structure): pass class GEOSCoordSeq_t(Structure): pass # Pointers to opaque GEOS geometry structures. GEOM_PTR = POINTER(GEOSGeom_t) +PREPGEOM_PTR = POINTER(GEOSPrepGeom_t) CS_PTR = POINTER(GEOSCoordSeq_t) -# Used specifically by the GEOSGeom_createPolygon and GEOSGeom_createCollection +# 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() -# Returns the string version of the GEOS library. Have to set the restype +# Returns the string version of the GEOS library. Have to set the restype # explicitly to c_char_p to ensure compatibility accross 32 and 64-bit platforms. geos_version = lgeos.GEOSversion -geos_version.argtypes = None +geos_version.argtypes = None geos_version.restype = c_char_p # Regular expression should be able to parse version strings such as # '3.0.0rc4-CAPI-1.3.3', or '3.0.0-CAPI-1.4.1' -version_regex = re.compile(r'^(?P\d+\.\d+\.\d+)(rc(?P\d+))?-CAPI-(?P\d+\.\d+\.\d+)$') +version_regex = re.compile(r'^(?P(?P\d+)\.(?P\d+)\.\d+)(rc(?P\d+))?-CAPI-(?P\d+\.\d+\.\d+)$') def geos_version_info(): """ Returns a dictionary containing the various version metadata parsed from @@ -120,7 +115,14 @@ def geos_version_info(): ver = geos_version() m = version_regex.match(ver) if not m: raise GEOSException('Could not parse version info string "%s"' % ver) - return dict((key, m.group(key)) for key in ('version', 'release_candidate', 'capi_version')) + return dict((key, m.group(key)) for key in ('version', 'release_candidate', 'capi_version', 'major', 'minor')) + +# Version numbers and whether or not prepared geometry support is available. +_verinfo = geos_version_info() +GEOS_MAJOR_VERSION = int(_verinfo['major']) +GEOS_MINOR_VERSION = int(_verinfo['minor']) +del _verinfo +GEOS_PREPARE = GEOS_MAJOR_VERSION > 3 or GEOS_MAJOR_VERSION == 3 and GEOS_MINOR_VERSION >= 1 # Calling the finishGEOS() upon exit of the interpreter. atexit.register(lgeos.finishGEOS) diff --git a/django/contrib/gis/geos/linestring.py b/django/contrib/gis/geos/linestring.py new file mode 100644 index 00000000000..db5e358425c --- /dev/null +++ b/django/contrib/gis/geos/linestring.py @@ -0,0 +1,152 @@ +from django.contrib.gis.geos.base import numpy +from django.contrib.gis.geos.coordseq import GEOSCoordSeq +from django.contrib.gis.geos.error import GEOSException +from django.contrib.gis.geos.geometry import GEOSGeometry +from django.contrib.gis.geos.point import Point +from django.contrib.gis.geos import prototypes as capi + +class LineString(GEOSGeometry): + _init_func = capi.create_linestring + _minlength = 2 + + #### 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. + + 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)) + """ + # If only one argument provided, set the coords array appropriately + if len(args) == 1: coords = args[0] + else: coords = args + + if isinstance(coords, (tuple, list)): + # 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) + 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], (tuple, list, Point)): + raise TypeError('each coordinate should be a sequence (list or tuple)') + if len(coords[i]) != ndim: raise TypeError('Dimension mismatch.') + numpy_coords = False + elif numpy and isinstance(coords, numpy.ndarray): + shape = coords.shape # Using numpy's shape. + if len(shape) != 2: raise TypeError('Too many dimensions.') + self._checkdim(shape[1]) + ncoords = shape[0] + ndim = shape[1] + numpy_coords = True + else: + raise TypeError('Invalid initialization input for LineStrings.') + + # Creating a coordinate sequence object because it is easier to + # set the points using GEOSCoordSeq.__setitem__(). + cs = GEOSCoordSeq(capi.create_cs(ncoords, ndim), z=bool(ndim==3)) + + 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] + + # 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. + super(LineString, self).__init__(self._init_func(cs.ptr), srid=srid) + + def __iter__(self): + "Allows iteration over this LineString." + for i in xrange(len(self)): + yield self[i] + + def __len__(self): + "Returns the number of points in this LineString." + return len(self._cs) + + def _getitem_external(self, index): + self._checkindex(index) + return self._cs[index] + _getitem_internal = _getitem_external + + def _set_collection(self, length, items): + ndim = self._cs.dims # + hasz = self._cs.hasz # I don't understand why these are different + + # create a new coordinate sequence and populate accordingly + cs = GEOSCoordSeq(capi.create_cs(length, ndim), z=hasz) + for i, c in enumerate(items): + cs[i] = c + + ptr = self._init_func(cs.ptr) + if ptr: + capi.destroy_geom(self.ptr) + self.ptr = ptr + self._post_init(self.srid) + else: + # can this happen? + raise GEOSException('Geometry resulting from slice deletion was invalid.') + + def _set_single(self, index, value): + self._checkindex(index) + self._cs[index] = value + + def _checkdim(self, dim): + if dim not in (2, 3): raise TypeError('Dimension mismatch.') + + #### Sequence Properties #### + @property + def tuple(self): + "Returns a tuple version of the geometry from the coordinate sequence." + return self._cs.tuple + coords = 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))] + if numpy: return numpy.array(lst) # ARRRR! + else: return lst + + @property + def array(self): + "Returns a numpy array for the LineString." + return self._listarr(self._cs.__getitem__) + + @property + def merged(self): + "Returns the line merge of this LineString." + return self._topology(capi.geos_linemerge(self.ptr)) + + @property + def x(self): + "Returns a list or numpy array of the X variable." + return self._listarr(self._cs.getX) + + @property + def y(self): + "Returns a list or numpy array of the Y variable." + return self._listarr(self._cs.getY) + + @property + def z(self): + "Returns a list or numpy array of the Z variable." + if not self.hasz: return None + else: return self._listarr(self._cs.getZ) + +# LinearRings are LineStrings used within Polygons. +class LinearRing(LineString): + _minLength = 4 + _init_func = capi.create_linearring diff --git a/django/contrib/gis/geos/mutable_list.py b/django/contrib/gis/geos/mutable_list.py new file mode 100644 index 00000000000..9ed4b5c39db --- /dev/null +++ b/django/contrib/gis/geos/mutable_list.py @@ -0,0 +1,236 @@ +# Copyright (c) 2008-2009 Aryeh Leib Taurog, all rights reserved. +# Released under the New BSD license. +""" +This module contains a base type which provides list-style mutations +This is akin to UserList, but without specific data storage methods. +Possible candidate for a more general position in the source tree, +perhaps django.utils + +Author: Aryeh Leib Taurog. +""" +class ListMixin(object): + """ + A base class which provides complete list interface + derived classes should implement the following: + + function _getitem_external(self, i): + Return single item with index i for general use + + function _getitem_internal(self, i): + Same as above, but for use within the class [Optional] + + function _set_collection(self, length, items): + Recreate the entire object + + function _set_single(self, i, value): + Set the single item at index i to value [Optional] + If left undefined, all mutations will result in rebuilding + the object using _set_collection. + + function __len__(self): + Return the length + + function __iter__(self): + Return an iterator for the object + + int _minlength: + The minimum legal length [Optional] + + int _maxlength: + The maximum legal length [Optional] + + iterable _allowed: + A list of allowed item types [Optional] + + class _IndexError: + The type of exception to be raise on invalid index [Optional] + """ + + _minlength = 0 + _maxlength = None + _IndexError = IndexError + + ### Special methods for Python list interface ### + def __getitem__(self, index): + "Gets the coordinates of the point(s) at the specified index/slice." + if isinstance(index, slice): + return [self._getitem_external(i) for i in xrange(*index.indices(len(self)))] + else: + index = self._checkindex(index) + return self._getitem_external(index) + + def __delitem__(self, index): + "Delete the point(s) at the specified index/slice." + if not isinstance(index, (int, long, slice)): + raise TypeError("%s is not a legal index" % index) + + # calculate new length and dimensions + origLen = len(self) + if isinstance(index, (int, long)): + index = self._checkindex(index) + indexRange = [index] + else: + indexRange = range(*index.indices(origLen)) + + newLen = origLen - len(indexRange) + newItems = ( self._getitem_internal(i) + for i in xrange(origLen) + if i not in indexRange ) + + self._rebuild(newLen, newItems) + + def __setitem__(self, index, val): + "Sets the Geometry at the specified index." + if isinstance(index, slice): + self._set_slice(index, val) + else: + index = self._checkindex(index) + self._check_allowed((val,)) + self._set_single(index, val) + + ### Public list interface Methods ### + def append(self, val): + "Standard list append method" + self[len(self):] = [val] + + def extend(self, vals): + "Standard list extend method" + self[len(self):] = vals + + def insert(self, index, val): + "Standard list insert method" + if not isinstance(index, (int, long)): + raise TypeError("%s is not a legal index" % index) + self[index:index] = [val] + + def pop(self, index=-1): + "Standard list pop method" + result = self[index] + del self[index] + return result + + def index(self, val): + "Standard list index method" + for i in xrange(0, len(self)): + if self[i] == val: return i + raise ValueError('%s not found in object' % str(val)) + + def remove(self, val): + "Standard list remove method" + del self[self.index(val)] + + def count(self, val): + "Standard list count method" + count = 0 + for i in self: + if val == i: count += 1 + return count + + ### Private API routines unique to ListMixin ### + + def __init__(self, *args, **kwargs): + if not hasattr(self, '_getitem_internal'): + self._getitem_internal = self._getitem_external + + if hasattr(self, '_set_single'): + self._canSetSingle = True + else: + self._canSetSingle = False + self._set_single = self._set_single_rebuild + self._assign_extended_slice = self._assign_extended_slice_rebuild + + super(ListMixin, self).__init__(*args, **kwargs) + + def _rebuild(self, newLen, newItems): + if newLen < self._minlength: + raise ValueError('Must have at least %d items' % self._minlength) + if self._maxlength is not None and newLen > self._maxlength: + raise ValueError('Cannot have more than %d items' % self._maxlength) + + self._set_collection(newLen, newItems) + + def _set_single_rebuild(self, index, value): + self._set_slice(slice(index, index + 1, 1), [value]) + + def _checkindex(self, index, correct=True): + length = len(self) + if 0 <= index < length: + return index + if correct and -length <= index < 0: + return index + length + raise self._IndexError('invalid index: %s' % str(index)) + + def _check_allowed(self, items): + if hasattr(self, '_allowed'): + if False in [isinstance(val, self._allowed) for val in items]: + raise TypeError('Invalid type encountered in the arguments.') + + def _set_slice(self, index, values): + "Assign values to a slice of the object" + try: + iter(values) + except TypeError: + raise TypeError('can only assign an iterable to a slice') + + self._check_allowed(values) + + origLen = len(self) + valueList = list(values) + start, stop, step = index.indices(origLen) + + # CAREFUL: index.step and step are not the same! + # step will never be None + if index.step is None: + self._assign_simple_slice(start, stop, valueList) + else: + self._assign_extended_slice(start, stop, step, valueList) + + def _assign_extended_slice_rebuild(self, start, stop, step, valueList): + 'Assign an extended slice by rebuilding entire list' + indexList = range(start, stop, step) + # extended slice, only allow assigning slice of same size + if len(valueList) != len(indexList): + raise ValueError('attempt to assign sequence of size %d ' + 'to extended slice of size %d' + % (len(valueList), len(indexList))) + + # we're not changing the length of the sequence + newLen = len(self) + newVals = dict(zip(indexList, valueList)) + def newItems(): + for i in xrange(newLen): + if i in newVals: + yield newVals[i] + else: + yield self._getitem_internal(i) + + self._rebuild(newLen, newItems()) + + def _assign_extended_slice(self, start, stop, step, valueList): + 'Assign an extended slice by re-assigning individual items' + indexList = range(start, stop, step) + # extended slice, only allow assigning slice of same size + if len(valueList) != len(indexList): + raise ValueError('attempt to assign sequence of size %d ' + 'to extended slice of size %d' + % (len(valueList), len(indexList))) + + for i, val in zip(indexList, valueList): + self._set_single(i, val) + + def _assign_simple_slice(self, start, stop, valueList): + 'Assign a simple slice; Can assign slice of any length' + origLen = len(self) + stop = max(start, stop) + newLen = origLen - stop + start + len(valueList) + def newItems(): + for i in xrange(origLen + 1): + if i == start: + for val in valueList: + yield val + + if i < origLen: + if i < start or i >= stop: + yield self._getitem_internal(i) + + self._rebuild(newLen, newItems()) diff --git a/django/contrib/gis/geos/point.py b/django/contrib/gis/geos/point.py new file mode 100644 index 00000000000..6ab873fe8fd --- /dev/null +++ b/django/contrib/gis/geos/point.py @@ -0,0 +1,138 @@ +from ctypes import c_uint +from django.contrib.gis.geos.error import GEOSException +from django.contrib.gis.geos.geometry import GEOSGeometry +from django.contrib.gis.geos import prototypes as capi + +class Point(GEOSGeometry): + _minlength = 2 + + 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: + >>> p = Point((5, 23)) # 2D point, passed in as a tuple + >>> p = Point(5, 23, 8) # 3D point, passed in with individual parameters + """ + if isinstance(x, (tuple, list)): + # Here a tuple or list was passed in under the `x` parameter. + ndim = len(x) + coords = x + elif isinstance(x, (int, float, long)) and isinstance(y, (int, float, long)): + # Here X, Y, and (optionally) Z were passed in individually, as parameters. + if isinstance(z, (int, float, long)): + ndim = 3 + coords = [x, y, z] + else: + ndim = 2 + coords = [x, y] + else: + raise TypeError('Invalid parameters given for Point initialization.') + + point = self._create_point(ndim, coords) + + # Initializing using the address returned from the GEOS + # createPoint factory. + super(Point, self).__init__(point, srid=srid) + + @classmethod + def _create_point(self, ndim, coords): + """ + Create a coordinate sequence, set X, Y, [Z], and create point + """ + if ndim < 2 or ndim > 3: + raise TypeError('Invalid point dimension: %s' % str(ndim)) + + cs = capi.create_cs(c_uint(1), c_uint(ndim)) + i = iter(coords) + capi.cs_setx(cs, 0, i.next()) + capi.cs_sety(cs, 0, i.next()) + if ndim == 3: capi.cs_setz(cs, 0, i.next()) + + return capi.create_point(cs) + + def _getitem_external(self, index): + return self._cs.getOrdinate(index, 0) + + def _set_collection(self, length, items): + ptr = self._create_point(length, items) + if ptr: + capi.destroy_geom(self.ptr) + self._ptr = ptr + self._set_cs() + else: + # can this happen? + raise GEOSException('Geometry resulting from slice deletion was invalid.') + + def _set_single(self, index, value): + self._cs.setOrdinate(index, 0, value) + + def __iter__(self): + "Allows iteration over coordinates of this Point." + for i in xrange(len(self)): + yield self[i] + + def __len__(self): + "Returns the number of dimensions for this Point (either 0, 2 or 3)." + if self.empty: return 0 + if self.hasz: return 3 + else: return 2 + + def _getitem_external(self, index): + self._checkindex(index) + if index == 0: + return self.x + elif index == 1: + return self.y + elif index == 2: + return self.z + _getitem_internal = _getitem_external + + def get_x(self): + "Returns the X component of the Point." + return self._cs.getOrdinate(0, 0) + + def set_x(self, value): + "Sets the X component of the Point." + self._cs.setOrdinate(0, 0, value) + + def get_y(self): + "Returns the Y component of the Point." + return self._cs.getOrdinate(1, 0) + + def set_y(self, value): + "Sets the Y component of the Point." + self._cs.setOrdinate(1, 0, value) + + def get_z(self): + "Returns the Z component of the Point." + if self.hasz: + return self._cs.getOrdinate(2, 0) + else: + return None + + def set_z(self, value): + "Sets the Z component of the Point." + if self.hasz: + self._cs.setOrdinate(2, 0, value) + else: + raise GEOSException('Cannot set Z on 2D Point.') + + # X, Y, Z properties + x = property(get_x, set_x) + y = property(get_y, set_y) + z = property(get_z, set_z) + + ### Tuple setting and retrieval routines. ### + def get_coords(self): + "Returns a tuple of the point." + return self._cs.tuple + + def set_coords(self, tup): + "Sets the coordinates of the point with the given tuple." + self._cs[0] = tup + + # The tuple and coords properties + tuple = property(get_coords, set_coords) + coords = tuple diff --git a/django/contrib/gis/geos/polygon.py b/django/contrib/gis/geos/polygon.py new file mode 100644 index 00000000000..0974822712c --- /dev/null +++ b/django/contrib/gis/geos/polygon.py @@ -0,0 +1,172 @@ +from ctypes import c_uint, byref +from django.contrib.gis.geos.error import GEOSIndexError +from django.contrib.gis.geos.geometry import GEOSGeometry +from django.contrib.gis.geos.libgeos import get_pointer_arr, GEOM_PTR +from django.contrib.gis.geos.linestring import LinearRing +from django.contrib.gis.geos import prototypes as capi + +class Polygon(GEOSGeometry): + _minlength = 1 + + def __init__(self, *args, **kwargs): + """ + Initializes on an exterior ring and a sequence of holes (both + instances may be either LinearRing instances, or a tuple/list + that may be constructed into a LinearRing). + + Examples of initialization, where shell, hole1, and hole2 are + valid LinearRing geometries: + >>> poly = Polygon(shell, hole1, hole2) + >>> poly = Polygon(shell, (hole1, hole2)) + + Example where a tuple parameters are used: + >>> poly = Polygon(((0, 0), (0, 10), (10, 10), (0, 10), (0, 0)), + ((4, 4), (4, 6), (6, 6), (6, 4), (4, 4))) + """ + if not args: + raise TypeError('Must provide at least one LinearRing, or a tuple, to initialize a Polygon.') + + # Getting the ext_ring and init_holes parameters from the argument list + ext_ring = args[0] + init_holes = args[1:] + n_holes = len(init_holes) + + # If initialized as Polygon(shell, (LinearRing, LinearRing)) [for backward-compatibility] + if n_holes == 1 and isinstance(init_holes[0], (tuple, list)): + if len(init_holes[0]) == 0: + init_holes = () + n_holes = 0 + elif isinstance(init_holes[0][0], LinearRing): + init_holes = init_holes[0] + n_holes = len(init_holes) + + polygon = self._create_polygon(n_holes + 1, (ext_ring,) + init_holes) + super(Polygon, self).__init__(polygon, **kwargs) + + def __iter__(self): + "Iterates over each ring in the polygon." + for i in xrange(len(self)): + yield self[i] + + def __len__(self): + "Returns the number of rings in this Polygon." + return self.num_interior_rings + 1 + + @classmethod + def from_bbox(cls, bbox): + "Constructs a Polygon from a bounding box (4-tuple)." + x0, y0, x1, y1 = bbox + return GEOSGeometry( 'POLYGON((%s %s, %s %s, %s %s, %s %s, %s %s))' % ( + x0, y0, x0, y1, x1, y1, x1, y0, x0, y0) ) + + ### These classmethods and routines are needed for list-like operation w/ListMixin ### + @classmethod + def _create_polygon(cls, length, items): + # Instantiate LinearRing objects if necessary, but don't clone them yet + # _construct_ring will throw a TypeError if a parameter isn't a valid ring + # If we cloned the pointers here, we wouldn't be able to clean up + # in case of error. + rings = [] + for r in items: + if isinstance(r, GEOM_PTR): + rings.append(r) + else: + rings.append(cls._construct_ring(r)) + + shell = cls._clone(rings.pop(0)) + + n_holes = length - 1 + if n_holes: + holes = get_pointer_arr(n_holes) + for i, r in enumerate(rings): + holes[i] = cls._clone(r) + holes_param = byref(holes) + else: + holes_param = None + + return capi.create_polygon(shell, holes_param, c_uint(n_holes)) + + @classmethod + def _clone(cls, g): + if isinstance(g, GEOM_PTR): + return capi.geom_clone(g) + else: + return capi.geom_clone(g.ptr) + + @classmethod + def _construct_ring(cls, param, msg='Parameter must be a sequence of LinearRings or objects that can initialize to LinearRings'): + "Helper routine for trying to construct a ring from the given parameter." + if isinstance(param, LinearRing): return param + try: + ring = LinearRing(param) + return ring + except TypeError: + raise TypeError(msg) + + def _set_collection(self, length, items): + # Getting the current pointer, replacing with the newly constructed + # geometry, and destroying the old geometry. + prev_ptr = self.ptr + srid = self.srid + self.ptr = self._create_polygon(length, items) + if srid: self.srid = srid + capi.destroy_geom(prev_ptr) + + def _getitem_internal(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 at the given index (e.g., poly[1] and poly[2] would + return the first and second interior ring, respectively). + + CAREFUL: Internal/External are not the same as Interior/Exterior! + _getitem_internal returns a pointer from the existing geometries for use + internally by the object's methods. _getitem_external returns a clone + of the same geometry for use by external code. + """ + if index == 0: + return capi.get_extring(self.ptr) + else: + # Getting the interior ring, have to subtract 1 from the index. + return capi.get_intring(self.ptr, index-1) + + def _getitem_external(self, index): + return GEOSGeometry(capi.geom_clone(self._getitem_internal(index)), srid=self.srid) + + # Because Polygonss need to be rebuilt upon the changing of a + # component geometry, these routines are set to their counterparts that + # rebuild the entire geometry. + _set_single = GEOSGeometry._set_single_rebuild + _assign_extended_slice = GEOSGeometry._assign_extended_slice_rebuild + + #### Polygon Properties #### + @property + def num_interior_rings(self): + "Returns the number of interior rings." + # Getting the number of rings + return capi.get_nrings(self.ptr) + + def _get_ext_ring(self): + "Gets the exterior ring of the Polygon." + return self[0] + + def _set_ext_ring(self, ring): + "Sets the exterior ring of the Polygon." + self[0] = ring + + # Properties for the exterior ring/shell. + exterior_ring = property(_get_ext_ring, _set_ext_ring) + shell = exterior_ring + + @property + def tuple(self): + "Gets the tuple for each ring in this Polygon." + return tuple([self[i].tuple for i in xrange(len(self))]) + coords = tuple + + @property + def kml(self): + "Returns the KML representation of this Polygon." + inner_kml = ''.join(["%s" % self[i+1].kml + for i in xrange(self.num_interior_rings)]) + return "%s%s" % (self[0].kml, inner_kml) diff --git a/django/contrib/gis/geos/prepared.py b/django/contrib/gis/geos/prepared.py new file mode 100644 index 00000000000..68b812df8aa --- /dev/null +++ b/django/contrib/gis/geos/prepared.py @@ -0,0 +1,30 @@ +from django.contrib.gis.geos.base import GEOSBase +from django.contrib.gis.geos.geometry import GEOSGeometry +from django.contrib.gis.geos.prototypes import prepared as capi + +class PreparedGeometry(GEOSBase): + """ + A geometry that is prepared for performing certain operations. + At the moment this includes the contains covers, and intersects + operations. + """ + ptr_type = capi.PREPGEOM_PTR + + def __init__(self, geom): + if not isinstance(geom, GEOSGeometry): raise TypeError + self.ptr = capi.geos_prepare(geom.ptr) + + def __del__(self): + if self._ptr: capi.prepared_destroy(self._ptr) + + def contains(self, other): + return capi.prepared_contains(self.ptr, other.ptr) + + def contains_properly(self, other): + return capi.prepared_contains_properly(self.ptr, other.ptr) + + def covers(self, other): + return capi.prepared_covers(self.ptr, other.ptr) + + def intersects(self, other): + return capi.prepared_intersects(self.ptr, other.ptr) diff --git a/django/contrib/gis/geos/prototypes/__init__.py b/django/contrib/gis/geos/prototypes/__init__.py index b4639f3d373..23559287745 100644 --- a/django/contrib/gis/geos/prototypes/__init__.py +++ b/django/contrib/gis/geos/prototypes/__init__.py @@ -27,7 +27,4 @@ from django.contrib.gis.geos.prototypes.predicates import geos_hasz, geos_isempt geos_intersects, geos_overlaps, geos_relatepattern, geos_touches, geos_within # Topology routines -from django.contrib.gis.geos.prototypes.topology import \ - geos_boundary, geos_buffer, geos_centroid, geos_convexhull, geos_difference, \ - geos_envelope, geos_intersection, geos_pointonsurface, geos_preservesimplify, \ - geos_simplify, geos_symdifference, geos_union, geos_relate +from django.contrib.gis.geos.prototypes.topology import * diff --git a/django/contrib/gis/geos/prototypes/errcheck.py b/django/contrib/gis/geos/prototypes/errcheck.py index 6fcc1a7a789..d96a1ac7535 100644 --- a/django/contrib/gis/geos/prototypes/errcheck.py +++ b/django/contrib/gis/geos/prototypes/errcheck.py @@ -18,7 +18,7 @@ libc = CDLL(find_library(libc_name)) def last_arg_byref(args): "Returns the last C argument's by reference value." return args[-1]._obj.value - + def check_dbl(result, func, cargs): "Checks the status code and returns the double value passed in by reference." # Checking the status code @@ -28,7 +28,7 @@ def check_dbl(result, func, cargs): def check_geom(result, func, cargs): "Error checking on routines that return Geometries." - if not result: + if not result: raise GEOSException('Error encountered checking Geometry returned from GEOS C function "%s".' % func.__name__) return result @@ -48,22 +48,31 @@ def check_predicate(result, func, cargs): raise GEOSException('Error encountered on GEOS C predicate function "%s".' % func.__name__) def check_sized_string(result, func, cargs): - "Error checking for routines that return explicitly sized strings." + """ + Error checking for routines that return explicitly sized strings. + + This frees the memory allocated by GEOS at the result pointer. + """ if not result: raise GEOSException('Invalid string pointer returned by GEOS C function "%s"' % func.__name__) # A c_size_t object is passed in by reference for the second # argument on these routines, and its needed to determine the # correct size. s = string_at(result, last_arg_byref(cargs)) + # Freeing the memory allocated within GEOS libc.free(result) return s def check_string(result, func, cargs): - "Error checking for routines that return strings." + """ + Error checking for routines that return strings. + + This frees the memory allocated by GEOS at the result pointer. + """ if not result: raise GEOSException('Error encountered checking string return value in GEOS C function "%s".' % func.__name__) # Getting the string value at the pointer address. s = string_at(result) - # Freeing the memory allocated by the GEOS library. + # Freeing the memory allocated within GEOS libc.free(result) return s @@ -73,4 +82,3 @@ def check_zero(result, func, cargs): raise GEOSException('Error encountered in GEOS C function "%s".' % func.__name__) else: return result - diff --git a/django/contrib/gis/geos/prototypes/geom.py b/django/contrib/gis/geos/prototypes/geom.py index d42ceaa2920..a177f0df9f1 100644 --- a/django/contrib/gis/geos/prototypes/geom.py +++ b/django/contrib/gis/geos/prototypes/geom.py @@ -1,5 +1,5 @@ from ctypes import c_char_p, c_int, c_size_t, c_ubyte, c_uint, POINTER -from django.contrib.gis.geos.libgeos import lgeos, CS_PTR, GEOM_PTR +from django.contrib.gis.geos.libgeos import lgeos, CS_PTR, GEOM_PTR, PREPGEOM_PTR, GEOS_PREPARE from django.contrib.gis.geos.prototypes.errcheck import \ check_geom, check_minus_one, check_sized_string, check_string, check_zero @@ -47,7 +47,7 @@ def int_from_geom(func, zero=False): "Argument is a geometry, return type is an integer." func.argtypes = [GEOM_PTR] func.restype = c_int - if zero: + if zero: func.errcheck = check_zero else: func.errcheck = check_minus_one @@ -55,8 +55,6 @@ def int_from_geom(func, zero=False): def string_from_geom(func): "Argument is a Geometry, return type is a string." - # We do _not_ specify an argument type because we want just an - # address returned from the function. func.argtypes = [GEOM_PTR] func.restype = geos_char_p func.errcheck = check_string @@ -64,13 +62,7 @@ def string_from_geom(func): ### ctypes prototypes ### -# TODO: Tell all users to use GEOS 3.0.0, instead of the release -# candidates, and use the new Reader and Writer APIs (e.g., -# GEOSWKT[Reader|Writer], GEOSWKB[Reader|Writer]). A good time -# to do this will be when Refractions releases a Windows PostGIS -# installer using GEOS 3.0.0. - -# Creation routines from WKB, HEX, WKT +# Deprecated creation routines from WKB, HEX, WKT from_hex = bin_constructor(lgeos.GEOSGeomFromHEX_buf) from_wkb = bin_constructor(lgeos.GEOSGeomFromWKB_buf) from_wkt = geom_output(lgeos.GEOSGeomFromWKT, [c_char_p]) @@ -90,7 +82,7 @@ get_num_geoms = int_from_geom(lgeos.GEOSGetNumGeometries) # Geometry creation factories create_point = geom_output(lgeos.GEOSGeom_createPoint, [CS_PTR]) -create_linestring = geom_output(lgeos.GEOSGeom_createLineString, [CS_PTR]) +create_linestring = geom_output(lgeos.GEOSGeom_createLineString, [CS_PTR]) create_linearring = geom_output(lgeos.GEOSGeom_createLinearRing, [CS_PTR]) # Polygon and collection creation routines are special and will not diff --git a/django/contrib/gis/geos/prototypes/io.py b/django/contrib/gis/geos/prototypes/io.py new file mode 100644 index 00000000000..ece1c70cf09 --- /dev/null +++ b/django/contrib/gis/geos/prototypes/io.py @@ -0,0 +1,94 @@ +from ctypes import c_char_p, c_int, c_char, c_size_t, Structure, POINTER +from django.contrib.gis.geos.libgeos import lgeos, GEOM_PTR +from django.contrib.gis.geos.prototypes.errcheck import check_geom, check_string, check_sized_string +from django.contrib.gis.geos.prototypes.geom import c_uchar_p, geos_char_p + +### The WKB/WKT Reader/Writer structures and pointers ### +class WKTReader_st(Structure): pass +class WKTWriter_st(Structure): pass +class WKBReader_st(Structure): pass +class WKBWriter_st(Structure): pass + +WKT_READ_PTR = POINTER(WKTReader_st) +WKT_WRITE_PTR = POINTER(WKTWriter_st) +WKB_READ_PTR = POINTER(WKBReader_st) +WKB_WRITE_PTR = POINTER(WKBReader_st) + +### WKTReader routines ### +wkt_reader_create = lgeos.GEOSWKTReader_create +wkt_reader_create.restype = WKT_READ_PTR + +wkt_reader_destroy = lgeos.GEOSWKTReader_destroy +wkt_reader_destroy.argtypes = [WKT_READ_PTR] + +wkt_reader_read = lgeos.GEOSWKTReader_read +wkt_reader_read.argtypes = [WKT_READ_PTR, c_char_p] +wkt_reader_read.restype = GEOM_PTR +wkt_reader_read.errcheck = check_geom + +### WKTWriter routines ### +wkt_writer_create = lgeos.GEOSWKTWriter_create +wkt_writer_create.restype = WKT_WRITE_PTR + +wkt_writer_destroy = lgeos.GEOSWKTWriter_destroy +wkt_writer_destroy.argtypes = [WKT_WRITE_PTR] + +wkt_writer_write = lgeos.GEOSWKTWriter_write +wkt_writer_write.argtypes = [WKT_WRITE_PTR, GEOM_PTR] +wkt_writer_write.restype = geos_char_p +wkt_writer_write.errcheck = check_string + +### WKBReader routines ### +wkb_reader_create = lgeos.GEOSWKBReader_create +wkb_reader_create.restype = WKB_READ_PTR + +wkb_reader_destroy = lgeos.GEOSWKBReader_destroy +wkb_reader_destroy.argtypes = [WKB_READ_PTR] + +def wkb_read_func(func): + # Although the function definitions take `const unsigned char *` + # as their parameter, we use c_char_p here so the function may + # take Python strings directly as parameters. Inside Python there + # is not a difference between signed and unsigned characters, so + # it is not a problem. + func.argtypes = [WKB_READ_PTR, c_char_p, c_size_t] + func.restype = GEOM_PTR + func.errcheck = check_geom + return func + +wkb_reader_read = wkb_read_func(lgeos.GEOSWKBReader_read) +wkb_reader_read_hex = wkb_read_func(lgeos.GEOSWKBReader_readHEX) + +### WKBWriter routines ### +wkb_writer_create = lgeos.GEOSWKBWriter_create +wkb_writer_create.restype = WKB_WRITE_PTR + +wkb_writer_destroy = lgeos.GEOSWKBWriter_destroy +wkb_writer_destroy.argtypes = [WKB_WRITE_PTR] + +# WKB Writing prototypes. +def wkb_write_func(func): + func.argtypes = [WKB_WRITE_PTR, GEOM_PTR, POINTER(c_size_t)] + func.restype = c_uchar_p + func.errcheck = check_sized_string + return func + +wkb_writer_write = wkb_write_func(lgeos.GEOSWKBWriter_write) +wkb_writer_write_hex = wkb_write_func(lgeos.GEOSWKBWriter_writeHEX) + +# WKBWriter property getter/setter prototypes. +def wkb_writer_get(func, restype=c_int): + func.argtypes = [WKB_WRITE_PTR] + func.restype = restype + return func + +def wkb_writer_set(func, argtype=c_int): + func.argtypes = [WKB_WRITE_PTR, argtype] + return func + +wkb_writer_get_byteorder = wkb_writer_get(lgeos.GEOSWKBWriter_getByteOrder) +wkb_writer_set_byteorder = wkb_writer_set(lgeos.GEOSWKBWriter_setByteOrder) +wkb_writer_get_outdim = wkb_writer_get(lgeos.GEOSWKBWriter_getOutputDimension) +wkb_writer_set_outdim = wkb_writer_set(lgeos.GEOSWKBWriter_setOutputDimension) +wkb_writer_get_include_srid = wkb_writer_get(lgeos.GEOSWKBWriter_getIncludeSRID, restype=c_char) +wkb_writer_set_include_srid = wkb_writer_set(lgeos.GEOSWKBWriter_setIncludeSRID, argtype=c_char) diff --git a/django/contrib/gis/geos/prototypes/predicates.py b/django/contrib/gis/geos/prototypes/predicates.py index 45240d971a3..596df0a7ce4 100644 --- a/django/contrib/gis/geos/prototypes/predicates.py +++ b/django/contrib/gis/geos/prototypes/predicates.py @@ -1,5 +1,5 @@ """ - This module houses the GEOS ctypes prototype functions for the + This module houses the GEOS ctypes prototype functions for the unary and binary predicate operations on geometries. """ from ctypes import c_char, c_char_p, c_double diff --git a/django/contrib/gis/geos/prototypes/prepared.py b/django/contrib/gis/geos/prototypes/prepared.py new file mode 100644 index 00000000000..6fde0cddd7b --- /dev/null +++ b/django/contrib/gis/geos/prototypes/prepared.py @@ -0,0 +1,24 @@ +from ctypes import c_char +from django.contrib.gis.geos.libgeos import lgeos, GEOM_PTR, PREPGEOM_PTR +from django.contrib.gis.geos.prototypes.errcheck import check_predicate + +# Prepared geometry constructor and destructors. +geos_prepare = lgeos.GEOSPrepare +geos_prepare.argtypes = [GEOM_PTR] +geos_prepare.restype = PREPGEOM_PTR + +prepared_destroy = lgeos.GEOSPreparedGeom_destroy +prepared_destroy.argtpes = [PREPGEOM_PTR] +prepared_destroy.restype = None + +# Prepared geometry binary predicate support. +def prepared_predicate(func): + func.argtypes= [PREPGEOM_PTR, GEOM_PTR] + func.restype = c_char + func.errcheck = check_predicate + return func + +prepared_contains = prepared_predicate(lgeos.GEOSPreparedContains) +prepared_contains_properly = prepared_predicate(lgeos.GEOSPreparedContainsProperly) +prepared_covers = prepared_predicate(lgeos.GEOSPreparedCovers) +prepared_intersects = prepared_predicate(lgeos.GEOSPreparedIntersects) diff --git a/django/contrib/gis/geos/prototypes/topology.py b/django/contrib/gis/geos/prototypes/topology.py index 70cf9000988..633340901bb 100644 --- a/django/contrib/gis/geos/prototypes/topology.py +++ b/django/contrib/gis/geos/prototypes/topology.py @@ -1,9 +1,14 @@ """ - This module houses the GEOS ctypes prototype functions for the + This module houses the GEOS ctypes prototype functions for the topological operations on geometries. """ +__all__ = ['geos_boundary', 'geos_buffer', 'geos_centroid', 'geos_convexhull', + 'geos_difference', 'geos_envelope', 'geos_intersection', + 'geos_linemerge', 'geos_pointonsurface', 'geos_preservesimplify', + 'geos_simplify', 'geos_symdifference', 'geos_union', 'geos_relate'] + from ctypes import c_char_p, c_double, c_int -from django.contrib.gis.geos.libgeos import lgeos, GEOM_PTR +from django.contrib.gis.geos.libgeos import lgeos, GEOM_PTR, GEOS_PREPARE from django.contrib.gis.geos.prototypes.errcheck import check_geom, check_string def topology(func, *args): @@ -23,6 +28,7 @@ geos_convexhull = topology(lgeos.GEOSConvexHull) geos_difference = topology(lgeos.GEOSDifference, GEOM_PTR) geos_envelope = topology(lgeos.GEOSEnvelope) geos_intersection = topology(lgeos.GEOSIntersection, GEOM_PTR) +geos_linemerge = topology(lgeos.GEOSLineMerge) geos_pointonsurface = topology(lgeos.GEOSPointOnSurface) geos_preservesimplify = topology(lgeos.GEOSTopologyPreserveSimplify, c_double) geos_simplify = topology(lgeos.GEOSSimplify, c_double) @@ -33,3 +39,10 @@ geos_union = topology(lgeos.GEOSUnion, GEOM_PTR) geos_relate = lgeos.GEOSRelate geos_relate.argtypes = [GEOM_PTR, GEOM_PTR] geos_relate.errcheck = check_string + +# Routines only in GEOS 3.1+ +if GEOS_PREPARE: + geos_cascaded_union = lgeos.GEOSUnionCascaded + geos_cascaded_union.argtypes = [GEOM_PTR] + geos_cascaded_union.restype = GEOM_PTR + __all__.append('geos_cascaded_union') diff --git a/django/contrib/gis/geos/tests/__init__.py b/django/contrib/gis/geos/tests/__init__.py new file mode 100644 index 00000000000..1c7384958bc --- /dev/null +++ b/django/contrib/gis/geos/tests/__init__.py @@ -0,0 +1,24 @@ +""" +GEOS Testing module. +""" +from unittest import TestSuite, TextTestRunner +import test_geos, test_geos_mutation, test_mutable_list + +test_suites = [ + test_geos.suite(), + test_geos_mutation.suite(), + test_mutable_list.suite(), + ] + +def suite(): + "Builds a test suite for the GEOS tests." + s = TestSuite() + map(s.addTest, test_suites) + return s + +def run(verbosity=1): + "Runs the GEOS tests." + TextTestRunner(verbosity=verbosity).run(suite()) + +if __name__ == '__main__': + run(2) diff --git a/django/contrib/gis/geos/tests/pymutable_geometries.py b/django/contrib/gis/geos/tests/pymutable_geometries.py new file mode 100644 index 00000000000..da98766820d --- /dev/null +++ b/django/contrib/gis/geos/tests/pymutable_geometries.py @@ -0,0 +1,184 @@ +from django.contrib.gis.geos import * +from random import random + +SEQ_LENGTH = 10 +SEQ_RANGE = (-1 * SEQ_LENGTH, SEQ_LENGTH) +SEQ_BOUNDS = (-1 * SEQ_LENGTH, -1, 0, SEQ_LENGTH - 1) +SEQ_OUT_OF_BOUNDS = (-1 * SEQ_LENGTH -1 , SEQ_LENGTH) + +def seqrange(): return xrange(*SEQ_RANGE) + +def random_coord(dim = 2, # coordinate dimensions + rng = (-50,50), # coordinate range + num_type = float, + round_coords = True): + + if round_coords: + num = lambda: num_type(round(random() * (rng[1]-rng[0]) + rng[0])) + else: + num = lambda: num_type(random() * (rng[1]-rng[0]) + rng[0]) + + return tuple( num() for axis in xrange(dim) ) + +def random_list(length = SEQ_LENGTH, ring = False, **kwargs): + result = [ random_coord(**kwargs) for index in xrange(length) ] + if ring: + result[-1] = result[0] + + return result + +random_list.single = random_coord + +def random_coll(count = SEQ_LENGTH, **kwargs): + return [ tuple(random_list(**kwargs)) for i in xrange(count) ] + +random_coll.single = random_list + +class PyMutTestGeom: + "The Test Geometry class container." + def __init__(self, geom_type, coords_fcn=random_list, subtype=tuple, **kwargs): + self.geom_type = geom_type + self.subtype = subtype + self.coords_fcn = coords_fcn + self.fcn_args = kwargs + self.coords = self.coords_fcn(**kwargs) + self.geom = self.make_geom() + + def newitem(self, **kwargs): + a = self.coords_fcn.single(**kwargs) + return self.subtype(a), tuple(a) + + @property + def tuple_coords(self): + return tuple(self.coords) + + def make_geom(self): + return self.geom_type(map(self.subtype,self.coords)) + + +def slice_geometries(ring=True): + testgeoms = [ + PyMutTestGeom(LineString), + PyMutTestGeom(MultiPoint, subtype=Point), + PyMutTestGeom(MultiLineString, coords_fcn=random_coll, subtype=LineString), + ] + if ring: + testgeoms.append(PyMutTestGeom(LinearRing, ring=True)) + + return testgeoms + +def getslice_functions(): + def gs_01(x): x[0:4], + def gs_02(x): x[5:-1], + def gs_03(x): x[6:2:-1], + def gs_04(x): x[:], + def gs_05(x): x[:3], + def gs_06(x): x[::2], + def gs_07(x): x[::-4], + def gs_08(x): x[7:7], + def gs_09(x): x[20:], + + # don't really care about ringy-ness here + return mark_ring(vars(), 'gs_') + +def delslice_functions(): + def ds_01(x): del x[0:4] + def ds_02(x): del x[5:-1] + def ds_03(x): del x[6:2:-1] + def ds_04(x): del x[:] # should this be allowed? + def ds_05(x): del x[:3] + def ds_06(x): del x[1:9:2] + def ds_07(x): del x[::-4] + def ds_08(x): del x[7:7] + def ds_09(x): del x[-7:-2] + + return mark_ring(vars(), 'ds_') + +def setslice_extended_functions(g): + a = g.coords_fcn(3, rng=(100,150)) + def maptype(x,a): + if isinstance(x, list): return a + else: return map(g.subtype, a) + + def sse_00(x): x[:3:1] = maptype(x, a) + def sse_01(x): x[0:3:1] = maptype(x, a) + def sse_02(x): x[2:5:1] = maptype(x, a) + def sse_03(x): x[-3::1] = maptype(x, a) + def sse_04(x): x[-4:-1:1] = maptype(x, a) + def sse_05(x): x[8:5:-1] = maptype(x, a) + def sse_06(x): x[-6:-9:-1] = maptype(x, a) + def sse_07(x): x[:8:3] = maptype(x, a) + def sse_08(x): x[1::3] = maptype(x, a) + def sse_09(x): x[-2::-3] = maptype(x, a) + def sse_10(x): x[7:1:-2] = maptype(x, a) + def sse_11(x): x[2:8:2] = maptype(x, a) + + return mark_ring(vars(), 'sse_') + +def setslice_simple_functions(g): + a = g.coords_fcn(3, rng=(100,150)) + def maptype(x,a): + if isinstance(x, list): return a + else: return map(g.subtype, a) + + def ss_00(x): x[:0] = maptype(x, a) + def ss_01(x): x[:1] = maptype(x, a) + def ss_02(x): x[:2] = maptype(x, a) + def ss_03(x): x[:3] = maptype(x, a) + def ss_04(x): x[-4:] = maptype(x, a) + def ss_05(x): x[-3:] = maptype(x, a) + def ss_06(x): x[-2:] = maptype(x, a) + def ss_07(x): x[-1:] = maptype(x, a) + def ss_08(x): x[5:] = maptype(x, a) + def ss_09(x): x[:] = maptype(x, a) + def ss_10(x): x[4:4] = maptype(x, a) + def ss_11(x): x[4:5] = maptype(x, a) + def ss_12(x): x[4:7] = maptype(x, a) + def ss_13(x): x[4:8] = maptype(x, a) + def ss_14(x): x[10:] = maptype(x, a) + def ss_15(x): x[20:30] = maptype(x, a) + def ss_16(x): x[-13:-8] = maptype(x, a) + def ss_17(x): x[-13:-9] = maptype(x, a) + def ss_18(x): x[-13:-10] = maptype(x, a) + def ss_19(x): x[-13:-11] = maptype(x, a) + + return mark_ring(vars(), 'ss_') + +def test_geos_functions(): + + return ( + lambda x: x.num_coords, + lambda x: x.empty, + lambda x: x.valid, + lambda x: x.simple, + lambda x: x.ring, + lambda x: x.boundary, + lambda x: x.convex_hull, + lambda x: x.extend, + lambda x: x.area, + lambda x: x.length, + ) + +def mark_ring(locals, name_pat, length=SEQ_LENGTH): + ''' + Accepts an array of functions which perform slice modifications + and labels each function as to whether or not it preserves ring-ness + ''' + func_array = [ val for name, val in locals.items() + if hasattr(val, '__call__') + and name.startswith(name_pat) ] + + for i in xrange(len(func_array)): + a = range(length) + a[-1] = a[0] + func_array[i](a) + ring = len(a) == 0 or (len(a) > 3 and a[-1] == a[0]) + func_array[i].ring = ring + + return func_array + +def getcoords(o): + if hasattr(o, 'coords'): + return o.coords + else: + return o diff --git a/django/contrib/gis/tests/test_geos.py b/django/contrib/gis/geos/tests/test_geos.py similarity index 88% rename from django/contrib/gis/tests/test_geos.py rename to django/contrib/gis/geos/tests/test_geos.py index 6786ad04b43..f1e9fc65135 100644 --- a/django/contrib/gis/tests/test_geos.py +++ b/django/contrib/gis/geos/tests/test_geos.py @@ -1,11 +1,8 @@ import random, unittest, sys from ctypes import ArgumentError from django.contrib.gis.geos import * -from django.contrib.gis.geos.base import HAS_GDAL +from django.contrib.gis.geos.base import gdal, numpy from django.contrib.gis.tests.geometries import * - -if HAS_NUMPY: from numpy import array -if HAS_GDAL: from django.contrib.gis.gdal import OGRGeometry, SpatialReference, CoordTransform, GEOJSON class GEOSTest(unittest.TestCase): @@ -13,7 +10,7 @@ class GEOSTest(unittest.TestCase): def null_srid(self): """ Returns the proper null SRID depending on the GEOS version. - See the comments in `test15_srid` for more details. + See the comments in `test15_srid` for more details. """ info = geos_version_info() if info['version'] == '3.0.0' and info['release_candidate']: @@ -49,18 +46,20 @@ class GEOSTest(unittest.TestCase): g = fromstr(err.wkt) except (GEOSException, ValueError): pass + + # Bad WKB + self.assertRaises(GEOSException, GEOSGeometry, buffer('0')) + print "\nEND - expecting GEOS_ERROR; safe to ignore.\n" - + class NotAGeometry(object): pass - + # Some other object self.assertRaises(TypeError, GEOSGeometry, NotAGeometry()) # None self.assertRaises(TypeError, GEOSGeometry, None) - # Bad WKB - self.assertRaises(GEOSException, GEOSGeometry, buffer('0')) - + def test01e_wkb(self): "Testing WKB output." from binascii import b2a_hex @@ -96,10 +95,10 @@ class GEOSTest(unittest.TestCase): self.assertEqual(srid, poly.srid) self.assertEqual(srid, poly.shell.srid) self.assertEqual(srid, fromstr(poly.ewkt).srid) # Checking export - + def test01i_json(self): "Testing GeoJSON input/output (via GDAL)." - if not HAS_GDAL or not GEOJSON: return + if not gdal or not gdal.GEOJSON: return for g in json_geoms: geom = GEOSGeometry(g.wkt) if not hasattr(g, 'not_equal'): @@ -107,7 +106,24 @@ class GEOSTest(unittest.TestCase): self.assertEqual(g.json, geom.geojson) self.assertEqual(GEOSGeometry(g.wkt), GEOSGeometry(geom.json)) - def test01j_eq(self): + def test01k_fromfile(self): + "Testing the fromfile() factory." + from StringIO import StringIO + ref_pnt = GEOSGeometry('POINT(5 23)') + + wkt_f = StringIO() + wkt_f.write(ref_pnt.wkt) + wkb_f = StringIO() + wkb_f.write(str(ref_pnt.wkb)) + + # Other tests use `fromfile()` on string filenames so those + # aren't tested here. + for fh in (wkt_f, wkb_f): + fh.seek(0) + pnt = fromfile(fh) + self.assertEqual(ref_pnt, pnt) + + def test01k_eq(self): "Testing equivalence." p = fromstr('POINT(5 23)') self.assertEqual(p, p.wkt) @@ -115,7 +131,7 @@ class GEOSTest(unittest.TestCase): ls = fromstr('LINESTRING(0 0, 1 1, 5 5)') self.assertEqual(ls, ls.wkt) self.assertNotEqual(p, 'bar') - # Error shouldn't be raise on equivalence testing with + # Error shouldn't be raise on equivalence testing with # an invalid type. for g in (p, ls): self.assertNotEqual(g, None) @@ -174,7 +190,7 @@ class GEOSTest(unittest.TestCase): self.assertEqual(set_tup1, pnt.tuple) pnt.coords = set_tup2 self.assertEqual(set_tup2, pnt.coords) - + prev = pnt # setting the previous geometry def test02b_multipoints(self): @@ -209,7 +225,7 @@ class GEOSTest(unittest.TestCase): self.assertEqual(l.centroid, ls.centroid.tuple) if hasattr(l, 'tup'): self.assertEqual(l.tup, ls.tuple) - + self.assertEqual(True, ls == fromstr(l.wkt)) self.assertEqual(False, ls == prev) self.assertRaises(GEOSIndexError, ls.__getitem__, len(ls)) @@ -220,7 +236,7 @@ class GEOSTest(unittest.TestCase): 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 + if numpy: self.assertEqual(ls, LineString(numpy.array(ls.tuple))) # as numpy array def test03b_multilinestring(self): "Testing MultiLineString objects." @@ -260,10 +276,16 @@ class GEOSTest(unittest.TestCase): self.assertEqual(lr, LinearRing(lr.tuple)) self.assertEqual(lr, LinearRing(*lr.tuple)) self.assertEqual(lr, LinearRing([list(tup) for tup in lr.tuple])) - if HAS_NUMPY: self.assertEqual(lr, LinearRing(array(lr.tuple))) - + if numpy: self.assertEqual(lr, LinearRing(numpy.array(lr.tuple))) + def test05a_polygons(self): "Testing Polygon objects." + + # Testing `from_bbox` class method + bbox = (-180, -90, 180, 90) + p = Polygon.from_bbox( bbox ) + self.assertEqual(bbox, p.extent) + prev = fromstr('POINT(0 0)') for p in polygons: # Creating the Polygon, testing its properties. @@ -297,9 +319,9 @@ class GEOSTest(unittest.TestCase): # Testing __getitem__ and __setitem__ on invalid indices self.assertRaises(GEOSIndexError, poly.__getitem__, len(poly)) self.assertRaises(GEOSIndexError, poly.__setitem__, len(poly), False) - self.assertRaises(GEOSIndexError, poly.__getitem__, -1) + self.assertRaises(GEOSIndexError, poly.__getitem__, -1 * len(poly) - 1) - # Testing __iter__ + # Testing __iter__ for r in poly: self.assertEqual(r.geom_type, 'LinearRing') self.assertEqual(r.geom_typeid, 2) @@ -307,11 +329,11 @@ class GEOSTest(unittest.TestCase): # Testing polygon construction. self.assertRaises(TypeError, Polygon.__init__, 0, [1, 2, 3]) self.assertRaises(TypeError, Polygon.__init__, 'foo') - + # Polygon(shell, (hole1, ... holeN)) rings = tuple(r for r in poly) self.assertEqual(poly, Polygon(rings[0], rings[1:])) - + # Polygon(shell_tuple, hole_tuple1, ... , hole_tupleN) ring_tuples = tuple(r.tuple for r in poly) self.assertEqual(poly, Polygon(*ring_tuples)) @@ -341,7 +363,7 @@ class GEOSTest(unittest.TestCase): 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" + print "\nEND - expecting GEOS_NOTICE; safe to ignore.\n" def test06a_memory_hijinks(self): "Testing Geometry __del__() on rings and polygons." @@ -355,7 +377,7 @@ class GEOSTest(unittest.TestCase): ring2 = poly[1] # These deletes should be 'harmless' since they are done on child geometries - del ring1 + del ring1 del ring2 ring1 = poly[0] ring2 = poly[1] @@ -366,7 +388,7 @@ class GEOSTest(unittest.TestCase): # Access to these rings is OK since they are clones. s1, s2 = str(ring1), str(ring2) - # The previous hijinks tests are now moot because only clones are + # The previous hijinks tests are now moot because only clones are # now used =) def test08_coord_seq(self): @@ -390,7 +412,7 @@ class GEOSTest(unittest.TestCase): if len(c1) == 2: tset = (5, 23) else: tset = (5, 23, 8) cs[i] = tset - + # Making sure every set point matches what we expect for j in range(len(tset)): cs[i] = tset @@ -415,7 +437,7 @@ class GEOSTest(unittest.TestCase): g_tup = topology_geoms[i] a = fromstr(g_tup[0].wkt) b = fromstr(g_tup[1].wkt) - i1 = fromstr(intersect_geoms[i].wkt) + i1 = fromstr(intersect_geoms[i].wkt) self.assertEqual(True, a.intersects(b)) i2 = a.intersection(b) self.assertEqual(i1, i2) @@ -434,7 +456,7 @@ class GEOSTest(unittest.TestCase): self.assertEqual(u1, u2) self.assertEqual(u1, a | b) # __or__ is union operator a |= b # testing __ior__ - self.assertEqual(u1, a) + self.assertEqual(u1, a) def test12_difference(self): "Testing difference()." @@ -519,7 +541,7 @@ class GEOSTest(unittest.TestCase): # In GEOS 3.0.0rc1-4 when the EWKB and/or HEXEWKB is exported, # the SRID information is lost and set to -1 -- this is not a - # problem on the 3.0.0 version (another reason to upgrade). + # problem on the 3.0.0 version (another reason to upgrade). exp_srid = self.null_srid p2 = fromstr(p1.hex) @@ -574,7 +596,7 @@ class GEOSTest(unittest.TestCase): r = poly[j] for k in xrange(len(r)): r[k] = (r[k][0] + 500., r[k][1] + 500.) poly[j] = r - + self.assertNotEqual(mpoly[i], poly) # Testing the assignment mpoly[i] = poly @@ -590,7 +612,7 @@ class GEOSTest(unittest.TestCase): # 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 @@ -606,13 +628,13 @@ class GEOSTest(unittest.TestCase): self.assertRaises(TypeError, ls.__setitem__, 0, (1.,2.)) ls[0] = (1.,2.,3.) self.assertEqual((1.,2.,3.), ls[0]) - + def test18_distance(self): "Testing the distance() function." - # Distance to self should be 0. + # Distance to self should be 0. pnt = Point(0, 0) self.assertEqual(0.0, pnt.distance(Point(0, 0))) - + # Distance should be 1 self.assertEqual(1.0, pnt.distance(Point(0, 1))) @@ -630,7 +652,7 @@ class GEOSTest(unittest.TestCase): # Points have 0 length. pnt = Point(0, 0) self.assertEqual(0.0, pnt.length) - + # Should be ~ sqrt(2) ls = LineString((0, 0), (1, 1)) self.assertAlmostEqual(1.41421356237, ls.length, 11) @@ -643,7 +665,7 @@ class GEOSTest(unittest.TestCase): mpoly = MultiPolygon(poly.clone(), poly) self.assertEqual(8.0, mpoly.length) - def test20_emptyCollections(self): + def test20a_emptyCollections(self): "Testing empty geometries and collections." gc1 = GeometryCollection([]) gc2 = fromstr('GEOMETRYCOLLECTION EMPTY') @@ -681,16 +703,35 @@ class GEOSTest(unittest.TestCase): else: self.assertRaises(GEOSIndexError, g.__getitem__, 0) + def test20b_collections_of_collections(self): + "Testing GeometryCollection handling of other collections." + # Creating a GeometryCollection WKT string composed of other + # collections and polygons. + coll = [mp.wkt for mp in multipolygons if mp.valid] + coll.extend([mls.wkt for mls in multilinestrings]) + coll.extend([p.wkt for p in polygons]) + coll.extend([mp.wkt for mp in multipoints]) + gc_wkt = 'GEOMETRYCOLLECTION(%s)' % ','.join(coll) + + # Should construct ok from WKT + gc1 = GEOSGeometry(gc_wkt) + + # Should also construct ok from individual geometry arguments. + gc2 = GeometryCollection(*tuple(g for g in gc1)) + + # And, they should be equal. + self.assertEqual(gc1, gc2) + def test21_test_gdal(self): "Testing `ogr` and `srs` properties." - if not HAS_GDAL: return + if not gdal.HAS_GDAL: return g1 = fromstr('POINT(5 23)') - self.assertEqual(True, isinstance(g1.ogr, OGRGeometry)) + self.assertEqual(True, isinstance(g1.ogr, gdal.OGRGeometry)) self.assertEqual(g1.srs, None) - + g2 = fromstr('LINESTRING(0 0, 5 5, 23 23)', srid=4326) - self.assertEqual(True, isinstance(g2.ogr, OGRGeometry)) - self.assertEqual(True, isinstance(g2.srs, SpatialReference)) + self.assertEqual(True, isinstance(g2.ogr, gdal.OGRGeometry)) + self.assertEqual(True, isinstance(g2.srs, gdal.SpatialReference)) self.assertEqual(g2.hex, g2.ogr.hex) self.assertEqual('WGS 84', g2.srs.name) @@ -705,7 +746,7 @@ class GEOSTest(unittest.TestCase): def test23_transform(self): "Testing `transform` method." - if not HAS_GDAL: return + if not gdal.HAS_GDAL: return orig = GEOSGeometry('POINT (-104.609 38.255)', 4326) trans = GEOSGeometry('POINT (992385.4472045 481455.4944650)', 2774) @@ -713,8 +754,8 @@ class GEOSTest(unittest.TestCase): # for transformations. t1, t2, t3 = orig.clone(), orig.clone(), orig.clone() t1.transform(trans.srid) - t2.transform(SpatialReference('EPSG:2774')) - ct = CoordTransform(SpatialReference('WGS84'), SpatialReference(2774)) + t2.transform(gdal.SpatialReference('EPSG:2774')) + ct = gdal.CoordTransform(gdal.SpatialReference('WGS84'), gdal.SpatialReference(2774)) t3.transform(ct) # Testing use of the `clone` keyword. @@ -749,7 +790,7 @@ class GEOSTest(unittest.TestCase): # Using both pickle and cPickle -- just 'cause. import pickle, cPickle - # Creating a list of test geometries for pickling, + # Creating a list of test geometries for pickling, # and setting the SRID on some of them. def get_geoms(lst, srid=None): return [GEOSGeometry(tg.wkt, srid) for tg in lst] @@ -759,7 +800,7 @@ class GEOSTest(unittest.TestCase): tgeoms.extend(get_geoms(multipolygons, 900913)) # The SRID won't be exported in GEOS 3.0 release candidates. - no_srid = self.null_srid == -1 + no_srid = self.null_srid == -1 for geom in tgeoms: s1, s2 = cPickle.dumps(geom), pickle.dumps(geom) g1, g2 = cPickle.loads(s1), pickle.loads(s2) @@ -767,6 +808,33 @@ class GEOSTest(unittest.TestCase): self.assertEqual(geom, tmpg) if not no_srid: self.assertEqual(geom.srid, tmpg.srid) + def test26_prepared(self): + "Testing PreparedGeometry support." + if not GEOS_PREPARE: return + # Creating a simple multipolygon and getting a prepared version. + mpoly = GEOSGeometry('MULTIPOLYGON(((0 0,0 5,5 5,5 0,0 0)),((5 5,5 10,10 10,10 5,5 5)))') + prep = mpoly.prepared + + # A set of test points. + pnts = [Point(5, 5), Point(7.5, 7.5), Point(2.5, 7.5)] + covers = [True, True, False] # No `covers` op for regular GEOS geoms. + for pnt, c in zip(pnts, covers): + # Results should be the same (but faster) + self.assertEqual(mpoly.contains(pnt), prep.contains(pnt)) + self.assertEqual(mpoly.intersects(pnt), prep.intersects(pnt)) + self.assertEqual(c, prep.covers(pnt)) + + def test26_line_merge(self): + "Testing line merge support" + ref_geoms = (fromstr('LINESTRING(1 1, 1 1, 3 3)'), + fromstr('MULTILINESTRING((1 1, 3 3), (3 3, 4 2))'), + ) + ref_merged = (fromstr('LINESTRING(1 1, 3 3)'), + fromstr('LINESTRING (1 1, 3 3, 4 2)'), + ) + for geom, merged in zip(ref_geoms, ref_merged): + self.assertEqual(merged, geom.merged) + def suite(): s = unittest.TestSuite() s.addTest(unittest.makeSuite(GEOSTest)) diff --git a/django/contrib/gis/geos/tests/test_geos_mutation.py b/django/contrib/gis/geos/tests/test_geos_mutation.py new file mode 100644 index 00000000000..97e7d8dbe20 --- /dev/null +++ b/django/contrib/gis/geos/tests/test_geos_mutation.py @@ -0,0 +1,135 @@ +# Copyright (c) 2008-2009 Aryeh Leib Taurog, all rights reserved. +# Modified from original contribution by Aryeh Leib Taurog, which was +# released under the New BSD license. +import unittest +from django.contrib.gis.geos import * +from django.contrib.gis.geos.error import GEOSIndexError +import copy + +def getItem(o,i): return o[i] +def delItem(o,i): del o[i] +def setItem(o,i,v): o[i] = v + +def api_get_distance(x): return x.distance(Point(-200,-200)) +def api_get_buffer(x): return x.buffer(10) +def api_get_geom_typeid(x): return x.geom_typeid +def api_get_num_coords(x): return x.num_coords +def api_get_centroid(x): return x.centroid +def api_get_empty(x): return x.empty +def api_get_valid(x): return x.valid +def api_get_simple(x): return x.simple +def api_get_ring(x): return x.ring +def api_get_boundary(x): return x.boundary +def api_get_convex_hull(x): return x.convex_hull +def api_get_extent(x): return x.extent +def api_get_area(x): return x.area +def api_get_length(x): return x.length + +geos_function_tests = [ val for name, val in vars().items() + if hasattr(val, '__call__') + and name.startswith('api_get_') ] + +class GEOSMutationTest(unittest.TestCase): + """ + Tests Pythonic Mutability of Python GEOS geometry wrappers + get/set/delitem on a slice, normal list methods + """ + + def test00_GEOSIndexException(self): + 'Testing Geometry GEOSIndexError' + p = Point(1,2) + for i in range(-2,2): p._checkindex(i) + self.assertRaises(GEOSIndexError, p._checkindex, 2) + self.assertRaises(GEOSIndexError, p._checkindex, -3) + + def test01_PointMutations(self): + 'Testing Point mutations' + for p in (Point(1,2,3), fromstr('POINT (1 2 3)')): + self.assertEqual(p._getitem_external(1), 2.0, 'Point _getitem_external') + + # _set_single + p._set_single(0,100) + self.assertEqual(p.coords, (100.0,2.0,3.0), 'Point _set_single') + + # _set_collection + p._set_collection(2,(50,3141)) + self.assertEqual(p.coords, (50.0,3141.0), 'Point _set_collection') + + def test02_PointExceptions(self): + 'Testing Point exceptions' + self.assertRaises(TypeError, Point, range(1)) + self.assertRaises(TypeError, Point, range(4)) + + def test03_PointApi(self): + 'Testing Point API' + q = Point(4,5,3) + for p in (Point(1,2,3), fromstr('POINT (1 2 3)')): + p[0:2] = [4,5] + for f in geos_function_tests: + self.assertEqual(f(q), f(p), 'Point ' + f.__name__) + + def test04_LineStringMutations(self): + 'Testing LineString mutations' + for ls in (LineString((1,0),(4,1),(6,-1)), + fromstr('LINESTRING (1 0,4 1,6 -1)')): + self.assertEqual(ls._getitem_external(1), (4.0,1.0), 'LineString _getitem_external') + + # _set_single + ls._set_single(0,(-50,25)) + self.assertEqual(ls.coords, ((-50.0,25.0),(4.0,1.0),(6.0,-1.0)), 'LineString _set_single') + + # _set_collection + ls._set_collection(2, ((-50.0,25.0),(6.0,-1.0))) + self.assertEqual(ls.coords, ((-50.0,25.0),(6.0,-1.0)), 'LineString _set_collection') + + lsa = LineString(ls.coords) + for f in geos_function_tests: + self.assertEqual(f(lsa), f(ls), 'LineString ' + f.__name__) + + def test05_Polygon(self): + 'Testing Polygon mutations' + for pg in (Polygon(((1,0),(4,1),(6,-1),(8,10),(1,0)), + ((5,4),(6,4),(6,3),(5,4))), + fromstr('POLYGON ((1 0,4 1,6 -1,8 10,1 0),(5 4,6 4,6 3,5 4))')): + self.assertEqual(pg._getitem_external(0), + LinearRing((1,0),(4,1),(6,-1),(8,10),(1,0)), + 'Polygon _getitem_external(0)') + self.assertEqual(pg._getitem_external(1), + LinearRing((5,4),(6,4),(6,3),(5,4)), + 'Polygon _getitem_external(1)') + + # _set_collection + pg._set_collection(2, (((1,2),(10,0),(12,9),(-1,15),(1,2)), + ((4,2),(5,2),(5,3),(4,2)))) + self.assertEqual(pg.coords, + (((1.0,2.0),(10.0,0.0),(12.0,9.0),(-1.0,15.0),(1.0,2.0)), + ((4.0,2.0),(5.0,2.0),(5.0,3.0),(4.0,2.0))), + 'Polygon _set_collection') + + lsa = Polygon(*pg.coords) + for f in geos_function_tests: + self.assertEqual(f(lsa), f(pg), 'Polygon ' + f.__name__) + + def test06_Collection(self): + 'Testing Collection mutations' + for mp in (MultiPoint(*map(Point,((3,4),(-1,2),(5,-4),(2,8)))), + fromstr('MULTIPOINT (3 4,-1 2,5 -4,2 8)')): + self.assertEqual(mp._getitem_external(2), Point(5,-4), 'Collection _getitem_external') + + mp._set_collection(3, map(Point,((5,5),(3,-2),(8,1)))) + self.assertEqual(mp.coords, ((5.0,5.0),(3.0,-2.0),(8.0,1.0)), 'Collection _set_collection') + + lsa = MultiPoint(*map(Point,((5,5),(3,-2),(8,1)))) + for f in geos_function_tests: + self.assertEqual(f(lsa), f(mp), 'MultiPoint ' + f.__name__) + +def suite(): + s = unittest.TestSuite() + s.addTest(unittest.makeSuite(GEOSMutationTest)) + return s + +def run(verbosity=2): + unittest.TextTestRunner(verbosity=verbosity).run(suite()) + +if __name__ == '__main__': + run() diff --git a/django/contrib/gis/geos/tests/test_mutable_list.py b/django/contrib/gis/geos/tests/test_mutable_list.py new file mode 100644 index 00000000000..d4d70758c51 --- /dev/null +++ b/django/contrib/gis/geos/tests/test_mutable_list.py @@ -0,0 +1,329 @@ +# Copyright (c) 2008-2009 Aryeh Leib Taurog, all rights reserved. +# Modified from original contribution by Aryeh Leib Taurog, which was +# released under the New BSD license. +import unittest +from django.contrib.gis.geos.mutable_list import ListMixin + +class UserListA(ListMixin): + _mytype = tuple + def __init__(self, i_list, *args, **kwargs): + self._list = self._mytype(i_list) + super(UserListA, self).__init__(*args, **kwargs) + + def __len__(self): return len(self._list) + + def __iter__(self): return iter(self._list) + + def __str__(self): return str(self._list) + + def __repr__(self): return repr(self._list) + + def _set_collection(self, length, items): + # this would work: + # self._list = self._mytype(items) + # but then we wouldn't be testing length parameter + itemList = ['x'] * length + for i, v in enumerate(items): + itemList[i] = v + + self._list = self._mytype(itemList) + + def _getitem_external(self, index): + return self._list[index] + + _getitem_internal = _getitem_external + _set_single = ListMixin._set_single_rebuild + _assign_extended_slice = ListMixin._assign_extended_slice_rebuild + +class UserListB(UserListA): + _mytype = list + + def _set_single(self, index, value): + self._list[index] = value + +def nextRange(length): + nextRange.start += 100 + return range(nextRange.start, nextRange.start + length) + +nextRange.start = 0 + +class ListMixinTest(unittest.TestCase): + """ + Tests base class ListMixin by comparing a list clone which is + a ListMixin subclass with a real Python list. + """ + limit = 3 + listType = UserListA + + @classmethod + def lists_of_len(cls, length=None): + if length is None: length = cls.limit + pl = range(length) + return pl, cls.listType(pl) + + @classmethod + def limits_plus(cls, b): + return range(-cls.limit - b, cls.limit + b) + + @classmethod + def step_range(cls): + return range(-1 - cls.limit, 0) + range(1, 1 + cls.limit) + + def test01_getslice(self): + 'Testing slice retrieval' + pl, ul = self.lists_of_len() + for i in self.limits_plus(1): + self.assertEqual(pl[i:], ul[i:], 'slice [%d:]' % (i)) + self.assertEqual(pl[:i], ul[:i], 'slice [:%d]' % (i)) + + for j in self.limits_plus(1): + self.assertEqual(pl[i:j], ul[i:j], 'slice [%d:%d]' % (i,j)) + for k in self.step_range(): + self.assertEqual(pl[i:j:k], ul[i:j:k], 'slice [%d:%d:%d]' % (i,j,k)) + + for k in self.step_range(): + self.assertEqual(pl[i::k], ul[i::k], 'slice [%d::%d]' % (i,k)) + self.assertEqual(pl[:i:k], ul[:i:k], 'slice [:%d:%d]' % (i,k)) + + for k in self.step_range(): + self.assertEqual(pl[::k], ul[::k], 'slice [::%d]' % (k)) + + def test02_setslice(self): + 'Testing slice assignment' + def setfcn(x,i,j,k,L): x[i:j:k] = range(L) + pl, ul = self.lists_of_len() + for slen in range(self.limit + 1): + ssl = nextRange(slen) + ul[:] = ssl + pl[:] = ssl + self.assertEqual(pl, ul[:], 'set slice [:]') + + for i in self.limits_plus(1): + ssl = nextRange(slen) + ul[i:] = ssl + pl[i:] = ssl + self.assertEqual(pl, ul[:], 'set slice [%d:]' % (i)) + + ssl = nextRange(slen) + ul[:i] = ssl + pl[:i] = ssl + self.assertEqual(pl, ul[:], 'set slice [:%d]' % (i)) + + for j in self.limits_plus(1): + ssl = nextRange(slen) + ul[i:j] = ssl + pl[i:j] = ssl + self.assertEqual(pl, ul[:], 'set slice [%d:%d]' % (i, j)) + + for k in self.step_range(): + ssl = nextRange( len(ul[i:j:k]) ) + ul[i:j:k] = ssl + pl[i:j:k] = ssl + self.assertEqual(pl, ul[:], 'set slice [%d:%d:%d]' % (i, j, k)) + + sliceLen = len(ul[i:j:k]) + self.assertRaises(ValueError, setfcn, ul, i, j, k, sliceLen + 1) + if sliceLen > 2: + self.assertRaises(ValueError, setfcn, ul, i, j, k, sliceLen - 1) + + for k in self.step_range(): + ssl = nextRange( len(ul[i::k]) ) + ul[i::k] = ssl + pl[i::k] = ssl + self.assertEqual(pl, ul[:], 'set slice [%d::%d]' % (i, k)) + + ssl = nextRange( len(ul[:i:k]) ) + ul[:i:k] = ssl + pl[:i:k] = ssl + self.assertEqual(pl, ul[:], 'set slice [:%d:%d]' % (i, k)) + + for k in self.step_range(): + ssl = nextRange(len(ul[::k])) + ul[::k] = ssl + pl[::k] = ssl + self.assertEqual(pl, ul[:], 'set slice [::%d]' % (k)) + + + def test03_delslice(self): + 'Testing delete slice' + for Len in range(self.limit): + pl, ul = self.lists_of_len(Len) + del pl[:] + del ul[:] + self.assertEqual(pl[:], ul[:], 'del slice [:]') + for i in range(-Len - 1, Len + 1): + pl, ul = self.lists_of_len(Len) + del pl[i:] + del ul[i:] + self.assertEqual(pl[:], ul[:], 'del slice [%d:]' % (i)) + pl, ul = self.lists_of_len(Len) + del pl[:i] + del ul[:i] + self.assertEqual(pl[:], ul[:], 'del slice [:%d]' % (i)) + for j in range(-Len - 1, Len + 1): + pl, ul = self.lists_of_len(Len) + del pl[i:j] + del ul[i:j] + self.assertEqual(pl[:], ul[:], 'del slice [%d:%d]' % (i,j)) + for k in range(-Len - 1,0) + range(1,Len): + pl, ul = self.lists_of_len(Len) + del pl[i:j:k] + del ul[i:j:k] + self.assertEqual(pl[:], ul[:], 'del slice [%d:%d:%d]' % (i,j,k)) + + for k in range(-Len - 1,0) + range(1,Len): + pl, ul = self.lists_of_len(Len) + del pl[:i:k] + del ul[:i:k] + self.assertEqual(pl[:], ul[:], 'del slice [:%d:%d]' % (i,k)) + + pl, ul = self.lists_of_len(Len) + del pl[i::k] + del ul[i::k] + self.assertEqual(pl[:], ul[:], 'del slice [%d::%d]' % (i,k)) + + for k in range(-Len - 1,0) + range(1,Len): + pl, ul = self.lists_of_len(Len) + del pl[::k] + del ul[::k] + self.assertEqual(pl[:], ul[:], 'del slice [::%d]' % (k)) + + def test04_get_set_del_single(self): + 'Testing get/set/delete single item' + pl, ul = self.lists_of_len() + for i in self.limits_plus(0): + self.assertEqual(pl[i], ul[i], 'get single item [%d]' % i) + + for i in self.limits_plus(0): + pl, ul = self.lists_of_len() + pl[i] = 100 + ul[i] = 100 + self.assertEqual(pl[:], ul[:], 'set single item [%d]' % i) + + for i in self.limits_plus(0): + pl, ul = self.lists_of_len() + del pl[i] + del ul[i] + self.assertEqual(pl[:], ul[:], 'del single item [%d]' % i) + + def test05_out_of_range_exceptions(self): + 'Testing out of range exceptions' + def setfcn(x, i): x[i] = 20 + def getfcn(x, i): return x[i] + def delfcn(x, i): del x[i] + pl, ul = self.lists_of_len() + for i in (-1 - self.limit, self.limit): + self.assertRaises(IndexError, setfcn, ul, i) # 'set index %d' % i) + self.assertRaises(IndexError, getfcn, ul, i) # 'get index %d' % i) + self.assertRaises(IndexError, delfcn, ul, i) # 'del index %d' % i) + + def test06_list_methods(self): + 'Testing list methods' + pl, ul = self.lists_of_len() + pl.append(40) + ul.append(40) + self.assertEqual(pl[:], ul[:], 'append') + + pl.extend(range(50,55)) + ul.extend(range(50,55)) + self.assertEqual(pl[:], ul[:], 'extend') + + for i in self.limits_plus(1): + pl, ul = self.lists_of_len() + pl.insert(i,50) + ul.insert(i,50) + self.assertEqual(pl[:], ul[:], 'insert at %d' % i) + + for i in self.limits_plus(0): + pl, ul = self.lists_of_len() + self.assertEqual(pl.pop(i), ul.pop(i), 'popped value at %d' % i) + self.assertEqual(pl[:], ul[:], 'after pop at %d' % i) + + pl, ul = self.lists_of_len() + self.assertEqual(pl.pop(), ul.pop(i), 'popped value') + self.assertEqual(pl[:], ul[:], 'after pop') + + pl, ul = self.lists_of_len() + def popfcn(x, i): x.pop(i) + self.assertRaises(IndexError, popfcn, ul, self.limit) + self.assertRaises(IndexError, popfcn, ul, -1 - self.limit) + + pl, ul = self.lists_of_len() + for val in range(self.limit): + self.assertEqual(pl.index(val), ul.index(val), 'index of %d' % val) + + for val in self.limits_plus(2): + self.assertEqual(pl.count(val), ul.count(val), 'count %d' % val) + + for val in range(self.limit): + pl, ul = self.lists_of_len() + pl.remove(val) + ul.remove(val) + self.assertEqual(pl[:], ul[:], 'after remove val %d' % val) + + def indexfcn(x, v): return x.index(v) + def removefcn(x, v): return x.remove(v) + self.assertRaises(ValueError, indexfcn, ul, 40) + self.assertRaises(ValueError, removefcn, ul, 40) + + def test07_allowed_types(self): + 'Testing type-restricted list' + pl, ul = self.lists_of_len() + ul._allowed = (int, long) + ul[1] = 50 + ul[:2] = [60, 70, 80] + def setfcn(x, i, v): x[i] = v + self.assertRaises(TypeError, setfcn, ul, 2, 'hello') + self.assertRaises(TypeError, setfcn, ul, slice(0,3,2), ('hello','goodbye')) + + def test08_min_length(self): + 'Testing length limits' + pl, ul = self.lists_of_len() + ul._minlength = 1 + def delfcn(x,i): del x[:i] + def setfcn(x,i): x[:i] = [] + for i in range(self.limit - ul._minlength + 1, self.limit + 1): + self.assertRaises(ValueError, delfcn, ul, i) + self.assertRaises(ValueError, setfcn, ul, i) + del ul[:ul._minlength] + + ul._maxlength = 4 + for i in range(0, ul._maxlength - len(ul)): + ul.append(i) + self.assertRaises(ValueError, ul.append, 10) + + def test09_iterable_check(self): + 'Testing error on assigning non-iterable to slice' + pl, ul = self.lists_of_len(self.limit + 1) + def setfcn(x, i, v): x[i] = v + self.assertRaises(TypeError, setfcn, ul, slice(0,3,2), 2) + + def test10_checkindex(self): + 'Testing index check' + pl, ul = self.lists_of_len() + for i in self.limits_plus(0): + if i < 0: + self.assertEqual(ul._checkindex(i), i + self.limit, '_checkindex(neg index)') + else: + self.assertEqual(ul._checkindex(i), i, '_checkindex(pos index)') + + for i in (-self.limit - 1, self.limit): + self.assertRaises(IndexError, ul._checkindex, i) + + ul._IndexError = TypeError + self.assertRaises(TypeError, ul._checkindex, -self.limit - 1) + +class ListMixinTestSingle(ListMixinTest): + listType = UserListB + +def suite(): + s = unittest.TestSuite() + s.addTest(unittest.makeSuite(ListMixinTest)) + s.addTest(unittest.makeSuite(ListMixinTestSingle)) + return s + +def run(verbosity=2): + unittest.TextTestRunner(verbosity=verbosity).run(suite()) + +if __name__ == '__main__': + run() diff --git a/django/contrib/gis/tests/__init__.py b/django/contrib/gis/tests/__init__.py index 65e3e96431e..73eceafeeb1 100644 --- a/django/contrib/gis/tests/__init__.py +++ b/django/contrib/gis/tests/__init__.py @@ -16,8 +16,8 @@ def geo_suite(): s = unittest.TestSuite() # Adding the GEOS tests. (__future__) - #from django.contrib.gis.geos import tests as geos_tests - #s.addTest(geos_tests.suite()) + from django.contrib.gis.geos import tests as geos_tests + s.addTest(geos_tests.suite()) # Test apps that require use of a spatial database (e.g., creation of models) test_apps = ['geoapp', 'relatedapp'] @@ -27,7 +27,6 @@ def geo_suite(): # Tests that do not require setting up and tearing down a spatial database # and are modules in `django.contrib.gis.tests`. test_suite_names = [ - 'test_geos', 'test_measure', ] @@ -216,8 +215,8 @@ class _DeprecatedTestModule(object): self.mod, DeprecationWarning) self.tests.run() -#from django.contrib.gis.geos import tests as _tests -#test_geos = _DeprecatedTestModule(_tests, 'geos') +from django.contrib.gis.geos import tests as _tests +test_geos = _DeprecatedTestModule(_tests, 'geos') from django.contrib.gis.gdal import HAS_GDAL if HAS_GDAL: