Commit 66e1670e authored by Justin Bronn's avatar Justin Bronn
Browse files

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
parent 4246c832
Loading
Loading
Loading
Loading
+1 −1
Original line number Diff line number Diff line
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,
+10 −65
Original line number Diff line number Diff line
"""
 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/
"""
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
The GeoDjango GEOS module.  Please consult the GeoDjango documentation
for more details: 
  http://geodjango.org/docs/geos.html
"""
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
+48 −602

File changed.

Preview size limit exceeded, changes collapsed.

+72 −49
Original line number Diff line number Diff line
@@ -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)
+13 −21
Original line number Diff line number Diff line
@@ -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):
Loading