Refactored the GEOS interface. Improvements include:

* Geometries now allow list-like manipulation, e.g., can add, insert, delete vertexes (or other geometries in collections) like Python lists.  Thanks, Aryeh Leib Taurog.
* Added support for GEOS prepared geometries via `prepared` property.  Prepared geometries significantly speed up certain operations.
* Added support for GEOS cascaded union as `MultiPolygon.cascaded_union` property.
* Added support for GEOS line merge as `merged` property on `LineString`, and `MultiLineString` geometries.  Thanks, Paul Smith.
* No longer use the deprecated C API for serialization to/from WKB and WKT.  Now use the GEOS I/O classes, which are now exposed as `WKTReader`, `WKTWriter`, `WKBReader`, and `WKBWriter` (which supports 3D and SRID inclusion)
* Moved each type of geometry to their own module, eliminating the cluttered `geometries.py`.
* Internally, all C API methods are explicitly called from a module rather than a star import.

Fixed #9557, #9877, #10222


git-svn-id: http://code.djangoproject.com/svn/django/trunk@10131 bcc190cf-cafb-0310-a4f2-bffc1f526a37
This commit is contained in:
Justin Bronn 2009-03-24 00:12:21 +00:00
parent 4246c832b6
commit 66e1670efa
28 changed files with 2586 additions and 1220 deletions

View File

@ -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,

View File

@ -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

View File

@ -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<srid>\d+);)?(?P<wkt>(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:
# Default allowed pointer type.
ptr_type = c_void_p
* string: WKT
* string: HEXEWKB (a PostGIS-specific canonical form)
* buffer: WKB
# 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__)
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))
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()
@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.')
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
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
raise TypeError('Incompatible pointer type')
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</%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)

View File

@ -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."
@ -24,7 +25,7 @@ class GeometryCollection(GEOSGeometry):
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):
@ -97,9 +98,31 @@ class GeometryCollection(GEOSGeometry):
class MultiPoint(GeometryCollection):
_allowed = Point
_typeid = 4
class MultiLineString(GeometryCollection):
_allowed = (LineString, LinearRing)
_typeid = 5
@property
def merged(self):
"""
Returns a LineString representing the line merge of this
MultiLineString.
"""
return self._topology(capi.geos_linemerge(self.ptr))
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)

View File

@ -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):

View File

@ -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)

View File

@ -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(["<innerBoundaryIs>%s</innerBoundaryIs>" % self[i+1].kml
for i in xrange(self.num_interior_rings)])
return "<Polygon><outerBoundaryIs>%s</outerBoundaryIs>%s</Polygon>" % (self[0].kml, inner_kml)

View File

@ -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<srid>\d+);)?(?P<wkt>(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</%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

View File

@ -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()

View File

@ -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
@ -88,10 +81,12 @@ 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
@ -109,7 +104,7 @@ 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<version>\d+\.\d+\.\d+)(rc(?P<release_candidate>\d+))?-CAPI-(?P<capi_version>\d+\.\d+\.\d+)$')
version_regex = re.compile(r'^(?P<version>(?P<major>\d+)\.(?P<minor>\d+)\.\d+)(rc(?P<release_candidate>\d+))?-CAPI-(?P<capi_version>\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)

View File

@ -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

View File

@ -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())

View File

@ -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

View File

@ -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(["<innerBoundaryIs>%s</innerBoundaryIs>" % self[i+1].kml
for i in xrange(self.num_interior_rings)])
return "<Polygon><outerBoundaryIs>%s</outerBoundaryIs>%s</Polygon>" % (self[0].kml, inner_kml)

View File

@ -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)

View File

@ -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 *

View File

@ -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

View File

@ -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
@ -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])

View File

@ -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)

View File

@ -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)

View File

@ -2,8 +2,13 @@
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')

View File

@ -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)

View File

@ -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

View File

@ -1,12 +1,9 @@
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):
@property
@ -49,6 +46,10 @@ 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):
@ -58,8 +59,6 @@ class GEOSTest(unittest.TestCase):
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."
@ -99,7 +98,7 @@ class GEOSTest(unittest.TestCase):
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)
@ -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,7 +319,7 @@ 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__
for r in poly:
@ -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.
@ -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))

View File

@ -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()

View File

@ -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()

View File

@ -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: