Merged in patch from Aryeh Leib Taurog for #9877, adapting as necessary.
2 This module contains the 'base' GEOSGeometry object -- all GEOS Geometries
3 inherit from this object.
5 # Python, ctypes and types dependencies.
7 from ctypes import addressof, byref, c_double, c_size_t
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
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
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)
27 class GEOSGeometry(GEOSBase, ListMixin):
28 "A class that, generally, encapsulates a GEOS geometry."
32 #### Python 'magic' routines ####
33 def __init__(self, geo_input, srid=None):
35 The base constructor for GEOS geometry objects, and may take the
40 - HEXEWKB (a PostGIS-specific canonical form)
41 - GeoJSON (requires GDAL)
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.
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')
53 wkt_m = wkt_regex.match(geo_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)
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).
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)
75 # Invalid geometry type.
76 raise TypeError('Improper geometry input type: %s' % str(type(geo_input)))
79 # Setting the pointer object with a valid pointer.
82 raise GEOSException('Could not initialize GEOS Geometry with given input.')
84 # Post-initialization setup.
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
92 # Setting the class type (e.g., Point, Polygon, etc.)
93 self.__class__ = GEOS_CLASSES[self.geom_typeid]
95 # Setting the coordinate sequence for the geometry (will be None on
96 # geometries that do not have coordinate sequences)
101 Destroys this Geometry; in other words, frees the memory used by the
104 if self._ptr: capi.destroy_geom(self._ptr)
108 Returns a clone because the copy of a GEOSGeometry may contain an
109 invalid pointer location if the original is garbage collected.
113 def __deepcopy__(self, memodict):
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.
122 "WKT is used for the string representation."
126 "Short-hand representation because WKT may be very large."
127 return '<%s object at %s>' % (self.geom_type, hex(addressof(self.ptr)))
130 def __getstate__(self):
131 # The pickled state is simply a tuple of the WKB (in string form)
133 return str(self.wkb), self.srid
135 def __setstate__(self, state):
136 # Instantiating from the tuple state that was pickled.
138 ptr = capi.from_wkb(wkb, len(wkb))
139 if not ptr: raise GEOSException('Invalid Geometry loaded from pickled state.')
141 self._post_init(srid)
143 # Comparison operators
144 def __eq__(self, other):
146 Equivalence testing, a Geometry may be compared with another Geometry
147 or a WKT representation.
149 if isinstance(other, basestring):
150 return self.wkt == other
151 elif isinstance(other, GEOSGeometry):
152 return self.equals_exact(other)
156 def __ne__(self, other):
157 "The not equals operator."
158 return not (self == other)
160 ### Geometry set-like operations ###
161 # Thanks to Sean Gillies for inspiration:
162 # http://lists.gispython.org/pipermail/community/2007-July/001034.html
164 def __or__(self, other):
165 "Returns the union of this Geometry and the other."
166 return self.union(other)
169 def __and__(self, other):
170 "Returns the intersection of this Geometry and the other."
171 return self.intersection(other)
174 def __sub__(self, other):
175 "Return the difference this Geometry and the other."
176 return self.difference(other)
179 def __xor__(self, other):
180 "Return the symmetric difference of this Geometry and the other."
181 return self.sym_difference(other)
183 #### Coordinate Sequence Routines ####
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)):
194 "Sets the coordinate sequence for this Geometry."
196 self._cs = GEOSCoordSeq(capi.get_cs(self.ptr), self.hasz)
202 "Returns a clone of the coordinate sequence for this Geometry."
204 return self._cs.clone()
206 #### Geometry Info ####
209 "Returns a string representing the Geometry type, e.g. 'Polygon'"
210 return capi.geos_type(self.ptr)
213 def geom_typeid(self):
214 "Returns an integer representing the Geometry type."
215 return capi.geos_typeid(self.ptr)
219 "Returns the number of geometries in the Geometry."
220 return capi.get_num_geoms(self.ptr)
223 def num_coords(self):
224 "Returns the number of coordinates in the Geometry."
225 return capi.get_num_coords(self.ptr)
228 def num_points(self):
229 "Returns the number points, or coordinates, in the Geometry."
230 return self.num_coords
234 "Returns the dimension of this Geometry (0=point, 1=line, 2=surface)."
235 return capi.get_dims(self.ptr)
238 "Converts this Geometry to normal form (or canonical form)."
239 return capi.geos_normalize(self.ptr)
241 #### Unary predicates ####
245 Returns a boolean indicating whether the set of points in this Geometry
248 return capi.geos_isempty(self.ptr)
252 "Returns whether the geometry has a 3D dimension."
253 return capi.geos_hasz(self.ptr)
257 "Returns whether or not the geometry is a ring."
258 return capi.geos_isring(self.ptr)
262 "Returns false if the Geometry not simple."
263 return capi.geos_issimple(self.ptr)
267 "This property tests the validity of this Geometry."
268 return capi.geos_isvalid(self.ptr)
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)
275 def crosses(self, other):
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).
281 return capi.geos_crosses(self.ptr, other.ptr)
283 def disjoint(self, other):
285 Returns true if the DE-9IM intersection matrix for the two Geometries
288 return capi.geos_disjoint(self.ptr, other.ptr)
290 def equals(self, other):
292 Returns true if the DE-9IM intersection matrix for the two Geometries
295 return capi.geos_equals(self.ptr, other.ptr)
297 def equals_exact(self, other, tolerance=0):
299 Returns true if the two Geometries are exactly equal, up to a
302 return capi.geos_equalsexact(self.ptr, other.ptr, float(tolerance))
304 def intersects(self, other):
305 "Returns true if disjoint returns false."
306 return capi.geos_intersects(self.ptr, other.ptr)
308 def overlaps(self, other):
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).
313 return capi.geos_overlaps(self.ptr, other.ptr)
315 def relate_pattern(self, other, pattern):
317 Returns true if the elements in the DE-9IM intersection matrix for the
318 two Geometries match the elements in pattern.
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)
324 def touches(self, other):
326 Returns true if the DE-9IM intersection matrix for the two Geometries
327 is FT*******, F**T***** or F***T****.
329 return capi.geos_touches(self.ptr, other.ptr)
331 def within(self, other):
333 Returns true if the DE-9IM intersection matrix for the two Geometries
336 return capi.geos_within(self.ptr, other.ptr)
338 #### SRID Routines ####
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
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)
350 #### Output Routines ####
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
359 "Returns the WKT (Well-Known Text) of the Geometry."
360 return io.wkt_w.write(self.ptr)
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.
369 # A possible faster, all-python, implementation:
370 # str(self.wkb).encode('hex')
371 return io.wkb_w.write_hex(self.ptr)
376 Returns GeoJSON representation of this Geometry if GDAL 1.5+
379 if gdal.GEOJSON: return self.ogr.json
384 "Returns the WKB of the Geometry as a buffer."
385 return io.wkb_w.write(self.ptr)
389 "Returns the KML representation of this Geometry."
390 gtype = self.geom_type
391 return '<%s>%s</%s>' % (gtype, self.coord_seq.kml, gtype)
396 Returns a PreparedGeometry corresponding to this geometry -- it is
397 optimized for the contains, intersects, and covers operations.
400 return PreparedGeometry(self)
402 raise GEOSException('GEOS 3.1+ required for prepared geometry support.')
404 #### GDAL-specific output routines ####
407 "Returns the OGR Geometry for this Geometry."
410 return gdal.OGRGeometry(self.wkb, self.srid)
412 return gdal.OGRGeometry(self.wkb)
418 "Returns the OSR SpatialReference for SRID of this Geometry."
419 if gdal.HAS_GDAL and self.srid:
420 return gdal.SpatialReference(self.srid)
426 "Alias for `srs` property."
429 def transform(self, ct, clone=False):
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
439 if gdal.HAS_GDAL and srid:
440 # Creating an OGR Geometry, which is then transformed.
441 g = gdal.OGRGeometry(self.wkb, srid)
443 # Getting a new GEOS pointer
444 ptr = io.wkb_r.read(g.wkb)
446 # User wants a cloned transformed geometry returned.
447 return GEOSGeometry(ptr, srid=g.srid)
449 # Reassigning pointer, and performing post-initialization setup
450 # again due to the reassignment.
451 capi.destroy_geom(self.ptr)
453 self._post_init(g.srid)
455 raise GEOSException('Transformed WKB was invalid.')
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)
464 "Returns the boundary as a newly allocated Geometry object."
465 return self._topology(capi.geos_boundary(self.ptr))
467 def buffer(self, width, quadsegs=8):
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)
475 return self._topology(capi.geos_buffer(self.ptr, width, quadsegs))
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).
484 return self._topology(capi.geos_centroid(self.ptr))
487 def convex_hull(self):
489 Returns the smallest convex Polygon that contains all the points
492 return self._topology(capi.geos_convexhull(self.ptr))
494 def difference(self, other):
496 Returns a Geometry representing the points making up this Geometry
497 that do not make up other.
499 return self._topology(capi.geos_difference(self.ptr, other.ptr))
503 "Return the envelope for this geometry (a polygon)."
504 return self._topology(capi.geos_envelope(self.ptr))
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))
511 def point_on_surface(self):
512 "Computes an interior point of this Geometry."
513 return self._topology(capi.geos_pointonsurface(self.ptr))
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)
519 def simplify(self, tolerance=0.0, preserve_topology=False):
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.
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.
531 if preserve_topology:
532 return self._topology(capi.geos_preservesimplify(self.ptr, tolerance))
534 return self._topology(capi.geos_simplify(self.ptr, tolerance))
536 def sym_difference(self, other):
538 Returns a set combining the points in this Geometry not in other,
539 and the points in other not in this Geometry.
541 return self._topology(capi.geos_symdifference(self.ptr, other.ptr))
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))
547 #### Other Routines ####
550 "Returns the area of the Geometry."
551 return capi.geos_area(self.ptr, byref(c_double()))
553 def distance(self, other):
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
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()))
566 Returns the extent of this geometry as a 4-tuple, consisting of
567 (xmin, ymin, xmax, ymax).
570 if isinstance(env, Point):
571 xmin, ymin = env.tuple
572 xmax, ymax = xmin, ymin
574 xmin, ymin = env[0][0]
575 xmax, ymax = env[0][2]
576 return (xmin, ymin, xmax, ymax)
581 Returns the length of this Geometry (e.g., 0 for point, or the
582 circumfrence of a Polygon).
584 return capi.geos_length(self.ptr, byref(c_double()))
587 "Clones this Geometry."
588 return GEOSGeometry(capi.geom_clone(self.ptr), srid=self.srid)
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,
603 7 : GeometryCollection,
606 # If supported, import the PreparedGeometry class.
608 from django.contrib.gis.geos.prepared import PreparedGeometry