django/contrib/gis/geos/geometry.py
author Justin Bronn <jbronn@geodjango.org>
Sat Jan 31 15:18:50 2009 -0600 (3 years ago)
branchtrunk
changeset 138 466bece04a15
parent 125 d95f64667c2a
child 352 ed67864049e9
permissions -rw-r--r--
Merged in patch from Aryeh Leib Taurog for #9877, adapting as necessary.
     1 """
     2  This module contains the 'base' GEOSGeometry object -- all GEOS Geometries
     3  inherit from this object.
     4 """
     5 # Python, ctypes and types dependencies.
     6 import re
     7 from ctypes import addressof, byref, c_double, c_size_t
     8 
     9 # GEOS-related dependencies.
    10 from django.contrib.gis.geos.base import GEOSBase, ListMixin, gdal
    11 from django.contrib.gis.geos.coordseq import GEOSCoordSeq
    12 from django.contrib.gis.geos.error import GEOSException
    13 from django.contrib.gis.geos.libgeos import GEOM_PTR, GEOS_PREPARE
    14 
    15 # All other functions in this module come from the ctypes 
    16 # prototypes module -- which handles all interaction with
    17 # the underlying GEOS library.
    18 from django.contrib.gis.geos import prototypes as capi
    19 from django.contrib.gis.geos import io
    20 
    21 # Regular expression for recognizing HEXEWKB and WKT.  A prophylactic measure
    22 # to prevent potentially malicious input from reaching the underlying C
    23 # library.  Not a substitute for good web security programming practices.
    24 hex_regex = re.compile(r'^[0-9A-F]+$', re.I)
    25 wkt_regex = re.compile(r'^(SRID=(?P<srid>\d+);)?(?P<wkt>(POINT|LINESTRING|LINEARRING|POLYGON|MULTIPOINT|MULTILINESTRING|MULTIPOLYGON|GEOMETRYCOLLECTION)[ACEGIMLONPSRUTY\d,\.\-\(\) ]+)$', re.I)
    26 
    27 class GEOSGeometry(GEOSBase, ListMixin):
    28     "A class that, generally, encapsulates a GEOS geometry."
    29 
    30     ptr_type = GEOM_PTR
    31 
    32     #### Python 'magic' routines ####
    33     def __init__(self, geo_input, srid=None):
    34         """
    35         The base constructor for GEOS geometry objects, and may take the 
    36         following inputs:
    37          
    38          * strings: 
    39             - WKT
    40             - HEXEWKB (a PostGIS-specific canonical form)
    41             - GeoJSON (requires GDAL)
    42          * buffer: 
    43             - WKB
    44         
    45         The `srid` keyword is used to specify the Source Reference Identifier
    46         (SRID) number for this Geometry.  If not set, the SRID will be None.
    47         """ 
    48         if isinstance(geo_input, basestring):
    49             if isinstance(geo_input, unicode):
    50                 # Encoding to ASCII, WKT or HEXEWKB doesn't need any more.
    51                 geo_input = geo_input.encode('ascii')
    52                             
    53             wkt_m = wkt_regex.match(geo_input)
    54             if wkt_m:
    55                 # Handling WKT input.
    56                 if wkt_m.group('srid'): srid = int(wkt_m.group('srid'))
    57                 g = io.wkt_r.read(wkt_m.group('wkt'))
    58             elif hex_regex.match(geo_input):
    59                 # Handling HEXEWKB input.
    60                 g = io.wkb_r.read(geo_input)
    61             elif gdal.GEOJSON and gdal.geometries.json_regex.match(geo_input):
    62                 # Handling GeoJSON input.
    63                 g = io.wkb_r.read(gdal.OGRGeometry(geo_input).wkb)
    64             else:
    65                 raise ValueError('String or unicode input unrecognized as WKT EWKT, and HEXEWKB.')
    66         elif isinstance(geo_input, GEOM_PTR):
    67             # When the input is a pointer to a geomtry (GEOM_PTR).
    68             g = geo_input
    69         elif isinstance(geo_input, buffer):
    70             # When the input is a buffer (WKB).
    71             g = io.wkb_r.read(geo_input)
    72         elif isinstance(geo_input, GEOSGeometry):
    73             g = capi.geom_clone(geo_input.ptr)
    74         else:
    75             # Invalid geometry type.
    76             raise TypeError('Improper geometry input type: %s' % str(type(geo_input)))
    77 
    78         if bool(g):
    79             # Setting the pointer object with a valid pointer.
    80             self.ptr = g
    81         else:
    82             raise GEOSException('Could not initialize GEOS Geometry with given input.')
    83 
    84         # Post-initialization setup.
    85         self._post_init(srid)
    86 
    87     def _post_init(self, srid):
    88         "Helper routine for performing post-initialization setup."
    89         # Setting the SRID, if given.
    90         if srid and isinstance(srid, int): self.srid = srid
    91         
    92         # Setting the class type (e.g., Point, Polygon, etc.)
    93         self.__class__ = GEOS_CLASSES[self.geom_typeid]
    94 
    95         # Setting the coordinate sequence for the geometry (will be None on 
    96         # geometries that do not have coordinate sequences)
    97         self._set_cs()
    98 
    99     def __del__(self):
   100         """
   101         Destroys this Geometry; in other words, frees the memory used by the
   102         GEOS C++ object.
   103         """
   104         if self._ptr: capi.destroy_geom(self._ptr)
   105 
   106     def __copy__(self):
   107         """
   108         Returns a clone because the copy of a GEOSGeometry may contain an
   109         invalid pointer location if the original is garbage collected.
   110         """
   111         return self.clone()
   112 
   113     def __deepcopy__(self, memodict):
   114         """
   115         The `deepcopy` routine is used by the `Node` class of django.utils.tree;
   116         thus, the protocol routine needs to be implemented to return correct 
   117         copies (clones) of these GEOS objects, which use C pointers.
   118         """
   119         return self.clone()
   120 
   121     def __str__(self):
   122         "WKT is used for the string representation."
   123         return self.wkt
   124 
   125     def __repr__(self):
   126         "Short-hand representation because WKT may be very large."
   127         return '<%s object at %s>' % (self.geom_type, hex(addressof(self.ptr)))
   128 
   129     # Pickling support
   130     def __getstate__(self):
   131         # The pickled state is simply a tuple of the WKB (in string form)
   132         # and the SRID.
   133         return str(self.wkb), self.srid
   134 
   135     def __setstate__(self, state):
   136         # Instantiating from the tuple state that was pickled.
   137         wkb, srid = state
   138         ptr = capi.from_wkb(wkb, len(wkb))
   139         if not ptr: raise GEOSException('Invalid Geometry loaded from pickled state.')
   140         self.ptr = ptr
   141         self._post_init(srid)
   142 
   143     # Comparison operators
   144     def __eq__(self, other):
   145         """
   146         Equivalence testing, a Geometry may be compared with another Geometry
   147         or a WKT representation.
   148         """
   149         if isinstance(other, basestring):
   150             return self.wkt == other
   151         elif isinstance(other, GEOSGeometry):
   152             return self.equals_exact(other)
   153         else:
   154             return False
   155 
   156     def __ne__(self, other):
   157         "The not equals operator."
   158         return not (self == other)
   159 
   160     ### Geometry set-like operations ###
   161     # Thanks to Sean Gillies for inspiration:
   162     #  http://lists.gispython.org/pipermail/community/2007-July/001034.html
   163     # g = g1 | g2
   164     def __or__(self, other):
   165         "Returns the union of this Geometry and the other."
   166         return self.union(other)
   167 
   168     # g = g1 & g2
   169     def __and__(self, other):
   170         "Returns the intersection of this Geometry and the other."
   171         return self.intersection(other)
   172 
   173     # g = g1 - g2
   174     def __sub__(self, other):
   175         "Return the difference this Geometry and the other."
   176         return self.difference(other)
   177 
   178     # g = g1 ^ g2
   179     def __xor__(self, other):
   180         "Return the symmetric difference of this Geometry and the other."
   181         return self.sym_difference(other)
   182 
   183     #### Coordinate Sequence Routines ####
   184     @property
   185     def has_cs(self):
   186         "Returns True if this Geometry has a coordinate sequence, False if not."
   187         # Only these geometries are allowed to have coordinate sequences.
   188         if isinstance(self, (Point, LineString, LinearRing)):
   189             return True
   190         else:
   191             return False
   192 
   193     def _set_cs(self):
   194         "Sets the coordinate sequence for this Geometry."
   195         if self.has_cs:
   196             self._cs = GEOSCoordSeq(capi.get_cs(self.ptr), self.hasz)
   197         else:
   198             self._cs = None
   199 
   200     @property
   201     def coord_seq(self):
   202         "Returns a clone of the coordinate sequence for this Geometry."
   203         if self.has_cs:
   204             return self._cs.clone()
   205 
   206     #### Geometry Info ####
   207     @property
   208     def geom_type(self):
   209         "Returns a string representing the Geometry type, e.g. 'Polygon'"
   210         return capi.geos_type(self.ptr)
   211 
   212     @property
   213     def geom_typeid(self):
   214         "Returns an integer representing the Geometry type."
   215         return capi.geos_typeid(self.ptr)
   216 
   217     @property
   218     def num_geom(self):
   219         "Returns the number of geometries in the Geometry."
   220         return capi.get_num_geoms(self.ptr)
   221 
   222     @property
   223     def num_coords(self):
   224         "Returns the number of coordinates in the Geometry."
   225         return capi.get_num_coords(self.ptr)
   226 
   227     @property
   228     def num_points(self):
   229         "Returns the number points, or coordinates, in the Geometry."
   230         return self.num_coords
   231 
   232     @property
   233     def dims(self):
   234         "Returns the dimension of this Geometry (0=point, 1=line, 2=surface)."
   235         return capi.get_dims(self.ptr)
   236 
   237     def normalize(self):
   238         "Converts this Geometry to normal form (or canonical form)."
   239         return capi.geos_normalize(self.ptr)
   240 
   241     #### Unary predicates ####
   242     @property
   243     def empty(self):
   244         """
   245         Returns a boolean indicating whether the set of points in this Geometry 
   246         are empty.
   247         """
   248         return capi.geos_isempty(self.ptr)
   249 
   250     @property
   251     def hasz(self):
   252         "Returns whether the geometry has a 3D dimension."
   253         return capi.geos_hasz(self.ptr)
   254 
   255     @property
   256     def ring(self):
   257         "Returns whether or not the geometry is a ring."
   258         return capi.geos_isring(self.ptr)
   259 
   260     @property
   261     def simple(self):
   262         "Returns false if the Geometry not simple."
   263         return capi.geos_issimple(self.ptr)
   264 
   265     @property
   266     def valid(self):
   267         "This property tests the validity of this Geometry."
   268         return capi.geos_isvalid(self.ptr)
   269 
   270     #### Binary predicates. ####
   271     def contains(self, other):
   272         "Returns true if other.within(this) returns true."
   273         return capi.geos_contains(self.ptr, other.ptr)
   274 
   275     def crosses(self, other):
   276         """
   277         Returns true if the DE-9IM intersection matrix for the two Geometries
   278         is T*T****** (for a point and a curve,a point and an area or a line and
   279         an area) 0******** (for two curves).
   280         """
   281         return capi.geos_crosses(self.ptr, other.ptr)
   282 
   283     def disjoint(self, other):
   284         """
   285         Returns true if the DE-9IM intersection matrix for the two Geometries
   286         is FF*FF****.
   287         """
   288         return capi.geos_disjoint(self.ptr, other.ptr)
   289 
   290     def equals(self, other):
   291         """
   292         Returns true if the DE-9IM intersection matrix for the two Geometries 
   293         is T*F**FFF*.
   294         """
   295         return capi.geos_equals(self.ptr, other.ptr)
   296 
   297     def equals_exact(self, other, tolerance=0):
   298         """
   299         Returns true if the two Geometries are exactly equal, up to a
   300         specified tolerance.
   301         """
   302         return capi.geos_equalsexact(self.ptr, other.ptr, float(tolerance))
   303 
   304     def intersects(self, other):
   305         "Returns true if disjoint returns false."
   306         return capi.geos_intersects(self.ptr, other.ptr)
   307 
   308     def overlaps(self, other):
   309         """
   310         Returns true if the DE-9IM intersection matrix for the two Geometries
   311         is T*T***T** (for two points or two surfaces) 1*T***T** (for two curves).
   312         """
   313         return capi.geos_overlaps(self.ptr, other.ptr)
   314 
   315     def relate_pattern(self, other, pattern):
   316         """
   317         Returns true if the elements in the DE-9IM intersection matrix for the
   318         two Geometries match the elements in pattern.
   319         """
   320         if not isinstance(pattern, basestring) or len(pattern) > 9:
   321             raise GEOSException('invalid intersection matrix pattern')
   322         return capi.geos_relatepattern(self.ptr, other.ptr, pattern)
   323 
   324     def touches(self, other):
   325         """
   326         Returns true if the DE-9IM intersection matrix for the two Geometries
   327         is FT*******, F**T***** or F***T****.
   328         """
   329         return capi.geos_touches(self.ptr, other.ptr)
   330 
   331     def within(self, other):
   332         """
   333         Returns true if the DE-9IM intersection matrix for the two Geometries
   334         is T*F**F***.
   335         """
   336         return capi.geos_within(self.ptr, other.ptr)
   337 
   338     #### SRID Routines ####
   339     def get_srid(self):
   340         "Gets the SRID for the geometry, returns None if no SRID is set."
   341         s = capi.geos_get_srid(self.ptr)
   342         if s == 0: return None
   343         else: return s
   344 
   345     def set_srid(self, srid):
   346         "Sets the SRID for the geometry."
   347         capi.geos_set_srid(self.ptr, srid)
   348     srid = property(get_srid, set_srid)
   349 
   350     #### Output Routines ####
   351     @property
   352     def ewkt(self):
   353         "Returns the EWKT (WKT + SRID) of the Geometry."
   354         if self.get_srid(): return 'SRID=%s;%s' % (self.srid, self.wkt)
   355         else: return self.wkt
   356 
   357     @property
   358     def wkt(self):
   359         "Returns the WKT (Well-Known Text) of the Geometry."
   360         return io.wkt_w.write(self.ptr)
   361 
   362     @property
   363     def hex(self):
   364         """
   365         Returns the HEX of the Geometry -- please note that the SRID is not
   366         included in this representation, because the GEOS C library uses
   367         -1 by default, even if the SRID is set.
   368         """
   369         # A possible faster, all-python, implementation: 
   370         #  str(self.wkb).encode('hex')
   371         return io.wkb_w.write_hex(self.ptr)
   372 
   373     @property
   374     def json(self):
   375         """
   376         Returns GeoJSON representation of this Geometry if GDAL 1.5+ 
   377         is installed.
   378         """
   379         if gdal.GEOJSON: return self.ogr.json
   380     geojson = json
   381 
   382     @property
   383     def wkb(self):
   384         "Returns the WKB of the Geometry as a buffer."
   385         return io.wkb_w.write(self.ptr)
   386 
   387     @property
   388     def kml(self):
   389         "Returns the KML representation of this Geometry."
   390         gtype = self.geom_type
   391         return '<%s>%s</%s>' % (gtype, self.coord_seq.kml, gtype)
   392 
   393     @property
   394     def prepared(self):
   395         """
   396         Returns a PreparedGeometry corresponding to this geometry -- it is
   397         optimized for the contains, intersects, and covers operations.
   398         """
   399         if GEOS_PREPARE: 
   400             return PreparedGeometry(self)
   401         else:
   402             raise GEOSException('GEOS 3.1+ required for prepared geometry support.')
   403 
   404     #### GDAL-specific output routines ####
   405     @property
   406     def ogr(self):
   407         "Returns the OGR Geometry for this Geometry."
   408         if gdal.HAS_GDAL:
   409             if self.srid:
   410                 return gdal.OGRGeometry(self.wkb, self.srid)
   411             else:
   412                 return gdal.OGRGeometry(self.wkb)
   413         else:
   414             return None
   415 
   416     @property
   417     def srs(self):
   418         "Returns the OSR SpatialReference for SRID of this Geometry."
   419         if gdal.HAS_GDAL and self.srid:
   420             return gdal.SpatialReference(self.srid)
   421         else:
   422             return None
   423 
   424     @property
   425     def crs(self):
   426         "Alias for `srs` property."
   427         return self.srs
   428 
   429     def transform(self, ct, clone=False):
   430         """
   431         Requires GDAL. Transforms the geometry according to the given 
   432         transformation object, which may be an integer SRID, and WKT or 
   433         PROJ.4 string. By default, the geometry is transformed in-place and 
   434         nothing is returned. However if the `clone` keyword is set, then this 
   435         geometry will not be modified and a transformed clone will be returned
   436         instead.
   437         """
   438         srid = self.srid
   439         if gdal.HAS_GDAL and srid:
   440             # Creating an OGR Geometry, which is then transformed.
   441             g = gdal.OGRGeometry(self.wkb, srid)
   442             g.transform(ct)
   443             # Getting a new GEOS pointer
   444             ptr = io.wkb_r.read(g.wkb)
   445             if clone: 
   446                 # User wants a cloned transformed geometry returned.
   447                 return GEOSGeometry(ptr, srid=g.srid)
   448             if ptr:
   449                 # Reassigning pointer, and performing post-initialization setup
   450                 # again due to the reassignment.
   451                 capi.destroy_geom(self.ptr)
   452                 self.ptr = ptr
   453                 self._post_init(g.srid)
   454             else:
   455                 raise GEOSException('Transformed WKB was invalid.')
   456 
   457     #### Topology Routines ####
   458     def _topology(self, gptr):
   459         "Helper routine to return Geometry from the given pointer."
   460         return GEOSGeometry(gptr, srid=self.srid)
   461 
   462     @property
   463     def boundary(self):
   464         "Returns the boundary as a newly allocated Geometry object."
   465         return self._topology(capi.geos_boundary(self.ptr))
   466 
   467     def buffer(self, width, quadsegs=8):
   468         """
   469         Returns a geometry that represents all points whose distance from this
   470         Geometry is less than or equal to distance. Calculations are in the
   471         Spatial Reference System of this Geometry. The optional third parameter sets
   472         the number of segment used to approximate a quarter circle (defaults to 8).
   473         (Text from PostGIS documentation at ch. 6.1.3)
   474         """
   475         return self._topology(capi.geos_buffer(self.ptr, width, quadsegs))
   476 
   477     @property
   478     def centroid(self):
   479         """
   480         The centroid is equal to the centroid of the set of component Geometries
   481         of highest dimension (since the lower-dimension geometries contribute zero
   482         "weight" to the centroid).
   483         """
   484         return self._topology(capi.geos_centroid(self.ptr))
   485 
   486     @property
   487     def convex_hull(self):
   488         """
   489         Returns the smallest convex Polygon that contains all the points 
   490         in the Geometry.
   491         """
   492         return self._topology(capi.geos_convexhull(self.ptr))
   493 
   494     def difference(self, other):
   495         """
   496         Returns a Geometry representing the points making up this Geometry
   497         that do not make up other.
   498         """
   499         return self._topology(capi.geos_difference(self.ptr, other.ptr))
   500 
   501     @property
   502     def envelope(self):
   503         "Return the envelope for this geometry (a polygon)."
   504         return self._topology(capi.geos_envelope(self.ptr))
   505 
   506     def intersection(self, other):
   507         "Returns a Geometry representing the points shared by this Geometry and other."
   508         return self._topology(capi.geos_intersection(self.ptr, other.ptr))
   509 
   510     @property
   511     def point_on_surface(self):
   512         "Computes an interior point of this Geometry."
   513         return self._topology(capi.geos_pointonsurface(self.ptr))
   514 
   515     def relate(self, other):
   516         "Returns the DE-9IM intersection matrix for this Geometry and the other."
   517         return capi.geos_relate(self.ptr, other.ptr)
   518 
   519     def simplify(self, tolerance=0.0, preserve_topology=False):
   520         """
   521         Returns the Geometry, simplified using the Douglas-Peucker algorithm
   522         to the specified tolerance (higher tolerance => less points).  If no
   523         tolerance provided, defaults to 0.
   524 
   525         By default, this function does not preserve topology - e.g. polygons can 
   526         be split, collapse to lines or disappear holes can be created or 
   527         disappear, and lines can cross. By specifying preserve_topology=True, 
   528         the result will have the same dimension and number of components as the 
   529         input. This is significantly slower.         
   530         """
   531         if preserve_topology:
   532             return self._topology(capi.geos_preservesimplify(self.ptr, tolerance))
   533         else:
   534             return self._topology(capi.geos_simplify(self.ptr, tolerance))
   535 
   536     def sym_difference(self, other):
   537         """
   538         Returns a set combining the points in this Geometry not in other,
   539         and the points in other not in this Geometry.
   540         """
   541         return self._topology(capi.geos_symdifference(self.ptr, other.ptr))
   542 
   543     def union(self, other):
   544         "Returns a Geometry representing all the points in this Geometry and other."
   545         return self._topology(capi.geos_union(self.ptr, other.ptr))
   546 
   547     #### Other Routines ####
   548     @property
   549     def area(self):
   550         "Returns the area of the Geometry."
   551         return capi.geos_area(self.ptr, byref(c_double()))
   552 
   553     def distance(self, other):
   554         """
   555         Returns the distance between the closest points on this Geometry
   556         and the other. Units will be in those of the coordinate system of
   557         the Geometry.
   558         """
   559         if not isinstance(other, GEOSGeometry): 
   560             raise TypeError('distance() works only on other GEOS Geometries.')
   561         return capi.geos_distance(self.ptr, other.ptr, byref(c_double()))
   562 
   563     @property
   564     def extent(self):
   565         """
   566         Returns the extent of this geometry as a 4-tuple, consisting of
   567         (xmin, ymin, xmax, ymax).
   568         """
   569         env = self.envelope
   570         if isinstance(env, Point):
   571             xmin, ymin = env.tuple
   572             xmax, ymax = xmin, ymin
   573         else:
   574             xmin, ymin = env[0][0]
   575             xmax, ymax = env[0][2]
   576         return (xmin, ymin, xmax, ymax)
   577 
   578     @property
   579     def length(self):
   580         """
   581         Returns the length of this Geometry (e.g., 0 for point, or the
   582         circumfrence of a Polygon).
   583         """
   584         return capi.geos_length(self.ptr, byref(c_double()))
   585     
   586     def clone(self):
   587         "Clones this Geometry."
   588         return GEOSGeometry(capi.geom_clone(self.ptr), srid=self.srid)
   589 
   590 # Class mapping dictionary.  Has to be at the end to avoid import 
   591 # conflicts with GEOSGeometry.
   592 from django.contrib.gis.geos.linestring import LineString, LinearRing
   593 from django.contrib.gis.geos.point import Point
   594 from django.contrib.gis.geos.polygon import Polygon
   595 from django.contrib.gis.geos.collections import GeometryCollection, MultiPoint, MultiLineString, MultiPolygon
   596 GEOS_CLASSES = {0 : Point,
   597                 1 : LineString,
   598                 2 : LinearRing,
   599                 3 : Polygon,
   600                 4 : MultiPoint,
   601                 5 : MultiLineString,
   602                 6 : MultiPolygon,
   603                 7 : GeometryCollection,
   604                 }
   605 
   606 # If supported, import the PreparedGeometry class.
   607 if GEOS_PREPARE:
   608     from django.contrib.gis.geos.prepared import PreparedGeometry