""" This module houses the Point, LineString, LinearRing, and Polygon OGC geometry classes. All geometry classes in this module inherit from GEOSGeometry. """ from ctypes import c_uint, byref from django.contrib.gis.geos.base import GEOSGeometry from django.contrib.gis.geos.coordseq import GEOSCoordSeq from django.contrib.gis.geos.error import GEOSException, GEOSIndexError from django.contrib.gis.geos.libgeos import get_pointer_arr, GEOM_PTR, HAS_NUMPY from django.contrib.gis.geos.prototypes import * if HAS_NUMPY: from numpy import ndarray, array class Point(GEOSGeometry): def __init__(self, x, y=None, z=None, srid=None): """ The Point object may be initialized with either a tuple, or individual parameters. For Example: >>> p = Point((5, 23)) # 2D point, passed in as a tuple >>> p = Point(5, 23, 8) # 3D point, passed in with individual parameters """ if isinstance(x, (tuple, list)): # Here a tuple or list was passed in under the `x` parameter. ndim = len(x) if ndim < 2 or ndim > 3: raise TypeError('Invalid sequence parameter: %s' % str(x)) coords = x elif isinstance(x, (int, float, long)) and isinstance(y, (int, float, long)): # Here X, Y, and (optionally) Z were passed in individually, as parameters. if isinstance(z, (int, float, long)): ndim = 3 coords = [x, y, z] else: ndim = 2 coords = [x, y] else: raise TypeError('Invalid parameters given for Point initialization.') # Creating the coordinate sequence, and setting X, Y, [Z] cs = create_cs(c_uint(1), c_uint(ndim)) cs_setx(cs, 0, coords[0]) cs_sety(cs, 0, coords[1]) if ndim == 3: cs_setz(cs, 0, coords[2]) # Initializing using the address returned from the GEOS # createPoint factory. super(Point, self).__init__(create_point(cs), srid=srid) def __len__(self): "Returns the number of dimensions for this Point (either 0, 2 or 3)." if self.empty: return 0 if self.hasz: return 3 else: return 2 def get_x(self): "Returns the X component of the Point." return self._cs.getOrdinate(0, 0) def set_x(self, value): "Sets the X component of the Point." self._cs.setOrdinate(0, 0, value) def get_y(self): "Returns the Y component of the Point." return self._cs.getOrdinate(1, 0) def set_y(self, value): "Sets the Y component of the Point." self._cs.setOrdinate(1, 0, value) def get_z(self): "Returns the Z component of the Point." if self.hasz: return self._cs.getOrdinate(2, 0) else: return None def set_z(self, value): "Sets the Z component of the Point." if self.hasz: self._cs.setOrdinate(2, 0, value) else: raise GEOSException('Cannot set Z on 2D Point.') # X, Y, Z properties x = property(get_x, set_x) y = property(get_y, set_y) z = property(get_z, set_z) ### Tuple setting and retrieval routines. ### def get_coords(self): "Returns a tuple of the point." return self._cs.tuple def set_coords(self, tup): "Sets the coordinates of the point with the given tuple." self._cs[0] = tup # The tuple and coords properties tuple = property(get_coords, set_coords) coords = tuple class LineString(GEOSGeometry): #### Python 'magic' routines #### def __init__(self, *args, **kwargs): """ Initializes on the given sequence -- may take lists, tuples, NumPy arrays of X,Y pairs, or Point objects. If Point objects are used, ownership is _not_ transferred to the LineString object. Examples: ls = LineString((1, 1), (2, 2)) ls = LineString([(1, 1), (2, 2)]) ls = LineString(array([(1, 1), (2, 2)])) ls = LineString(Point(1, 1), Point(2, 2)) """ # If only one argument provided, set the coords array appropriately if len(args) == 1: coords = args[0] else: coords = args if isinstance(coords, (tuple, list)): # Getting the number of coords and the number of dimensions -- which # must stay the same, e.g., no LineString((1, 2), (1, 2, 3)). ncoords = len(coords) if coords: ndim = len(coords[0]) else: raise TypeError('Cannot initialize on empty sequence.') self._checkdim(ndim) # Incrementing through each of the coordinates and verifying for i in xrange(1, ncoords): if not isinstance(coords[i], (tuple, list, Point)): raise TypeError('each coordinate should be a sequence (list or tuple)') if len(coords[i]) != ndim: raise TypeError('Dimension mismatch.') numpy_coords = False elif HAS_NUMPY and isinstance(coords, ndarray): shape = coords.shape # Using numpy's shape. if len(shape) != 2: raise TypeError('Too many dimensions.') self._checkdim(shape[1]) ncoords = shape[0] ndim = shape[1] numpy_coords = True else: raise TypeError('Invalid initialization input for LineStrings.') # Creating a coordinate sequence object because it is easier to # set the points using GEOSCoordSeq.__setitem__(). cs = GEOSCoordSeq(create_cs(ncoords, ndim), z=bool(ndim==3)) for i in xrange(ncoords): if numpy_coords: cs[i] = coords[i,:] elif isinstance(coords[i], Point): cs[i] = coords[i].tuple else: cs[i] = coords[i] # Getting the correct initialization function if kwargs.get('ring', False): func = create_linearring else: func = create_linestring # If SRID was passed in with the keyword arguments srid = kwargs.get('srid', None) # Calling the base geometry initialization with the returned pointer # from the function. super(LineString, self).__init__(func(cs.ptr), srid=srid) def __getitem__(self, index): "Gets the point at the specified index." return self._cs[index] def __setitem__(self, index, value): "Sets the point at the specified index, e.g., line_str[0] = (1, 2)." self._cs[index] = value def __iter__(self): "Allows iteration over this LineString." for i in xrange(len(self)): yield self[i] def __len__(self): "Returns the number of points in this LineString." return len(self._cs) def _checkdim(self, dim): if dim not in (2, 3): raise TypeError('Dimension mismatch.') #### Sequence Properties #### @property def tuple(self): "Returns a tuple version of the geometry from the coordinate sequence." return self._cs.tuple coords = tuple def _listarr(self, func): """ Internal routine that returns a sequence (list) corresponding with the given function. Will return a numpy array if possible. """ lst = [func(i) for i in xrange(len(self))] if HAS_NUMPY: return array(lst) # ARRRR! else: return lst @property def array(self): "Returns a numpy array for the LineString." return self._listarr(self._cs.__getitem__) @property def x(self): "Returns a list or numpy array of the X variable." return self._listarr(self._cs.getX) @property def y(self): "Returns a list or numpy array of the Y variable." return self._listarr(self._cs.getY) @property def z(self): "Returns a list or numpy array of the Z variable." if not self.hasz: return None else: return self._listarr(self._cs.getZ) # LinearRings are LineStrings used within Polygons. class LinearRing(LineString): def __init__(self, *args, **kwargs): "Overriding the initialization function to set the ring keyword." kwargs['ring'] = True # Setting the ring keyword argument to True super(LinearRing, self).__init__(*args, **kwargs) class Polygon(GEOSGeometry): def __init__(self, *args, **kwargs): """ Initializes on an exterior ring and a sequence of holes (both instances may be either LinearRing instances, or a tuple/list that may be constructed into a LinearRing). Examples of initialization, where shell, hole1, and hole2 are valid LinearRing geometries: >>> poly = Polygon(shell, hole1, hole2) >>> poly = Polygon(shell, (hole1, hole2)) Example where a tuple parameters are used: >>> poly = Polygon(((0, 0), (0, 10), (10, 10), (0, 10), (0, 0)), ((4, 4), (4, 6), (6, 6), (6, 4), (4, 4))) """ if not args: raise TypeError('Must provide at list one LinearRing instance to initialize Polygon.') # Getting the ext_ring and init_holes parameters from the argument list ext_ring = args[0] init_holes = args[1:] n_holes = len(init_holes) # If initialized as Polygon(shell, (LinearRing, LinearRing)) [for backward-compatibility] if n_holes == 1 and isinstance(init_holes[0], (tuple, list)) and \ (len(init_holes[0]) == 0 or isinstance(init_holes[0][0], LinearRing)): init_holes = init_holes[0] n_holes = len(init_holes) # Ensuring the exterior ring and holes parameters are LinearRing objects # or may be instantiated into LinearRings. ext_ring = self._construct_ring(ext_ring, 'Exterior parameter must be a LinearRing or an object that can initialize a LinearRing.') holes_list = [] # Create new list, cause init_holes is a tuple. for i in xrange(n_holes): holes_list.append(self._construct_ring(init_holes[i], 'Holes parameter must be a sequence of LinearRings or objects that can initialize to LinearRings')) # Why another loop? Because if a TypeError is raised, cloned pointers will # be around that can't be cleaned up. holes = get_pointer_arr(n_holes) for i in xrange(n_holes): holes[i] = geom_clone(holes_list[i].ptr) # Getting the shell pointer address. shell = geom_clone(ext_ring.ptr) # Calling with the GEOS createPolygon factory. super(Polygon, self).__init__(create_polygon(shell, byref(holes), c_uint(n_holes)), **kwargs) def __getitem__(self, index): """ Returns the ring at the specified index. The first index, 0, will always return the exterior ring. Indices > 0 will return the interior ring at the given index (e.g., poly[1] and poly[2] would return the first and second interior ring, respectively). """ if index == 0: return self.exterior_ring else: # Getting the interior ring, have to subtract 1 from the index. return self.get_interior_ring(index-1) def __setitem__(self, index, ring): "Sets the ring at the specified index with the given ring." # Checking the index and ring parameters. self._checkindex(index) if not isinstance(ring, LinearRing): raise TypeError('must set Polygon index with a LinearRing object') # Getting the shell if index == 0: shell = geom_clone(ring.ptr) else: shell = geom_clone(get_extring(self.ptr)) # Getting the interior rings (holes) nholes = len(self)-1 if nholes > 0: holes = get_pointer_arr(nholes) for i in xrange(nholes): if i == (index-1): holes[i] = geom_clone(ring.ptr) else: holes[i] = geom_clone(get_intring(self.ptr, i)) holes_param = byref(holes) else: holes_param = None # Getting the current pointer, replacing with the newly constructed # geometry, and destroying the old geometry. prev_ptr = self.ptr srid = self.srid self._ptr = create_polygon(shell, holes_param, c_uint(nholes)) if srid: self.srid = srid destroy_geom(prev_ptr) def __iter__(self): "Iterates over each ring in the polygon." for i in xrange(len(self)): yield self[i] def __len__(self): "Returns the number of rings in this Polygon." return self.num_interior_rings + 1 def _checkindex(self, index): "Internal routine for checking the given ring index." if index < 0 or index >= len(self): raise GEOSIndexError('invalid Polygon ring index: %s' % index) def _construct_ring(self, param, msg=''): "Helper routine for trying to construct a ring from the given parameter." if isinstance(param, LinearRing): return param try: ring = LinearRing(param) return ring except TypeError: raise TypeError(msg) def get_interior_ring(self, ring_i): """ Gets the interior ring at the specified index, 0 is for the first interior ring, not the exterior ring. """ self._checkindex(ring_i+1) return GEOSGeometry(geom_clone(get_intring(self.ptr, ring_i)), srid=self.srid) #### Polygon Properties #### @property def num_interior_rings(self): "Returns the number of interior rings." # Getting the number of rings return get_nrings(self.ptr) def get_ext_ring(self): "Gets the exterior ring of the Polygon." return GEOSGeometry(geom_clone(get_extring(self.ptr)), srid=self.srid) def set_ext_ring(self, ring): "Sets the exterior ring of the Polygon." self[0] = ring # properties for the exterior ring/shell exterior_ring = property(get_ext_ring, set_ext_ring) shell = exterior_ring @property def tuple(self): "Gets the tuple for each ring in this Polygon." return tuple([self[i].tuple for i in xrange(len(self))]) coords = tuple @property def kml(self): "Returns the KML representation of this Polygon." inner_kml = ''.join(["%s" % self[i+1].kml for i in xrange(self.num_interior_rings)]) return "%s%s" % (self[0].kml, inner_kml)