class sdo_geometry(object):
    '''
    Process an Oracle SDO_GEOMETRY object as returned by cx_Oracle.
    '''
    # definitions
    geom_types = {'00': 'UNKNOWN_GEOMETRY', # UNKNOWN_GEOMETRY
                  '01': 'POINT', # POINT
                  '02': 'LINESTRING', # LINE or CURVE
                  '03': 'POLYGON', # POLYGON
                  '04': 'GEOMETRYCOLLECTION', # COLLECTION
                  '05': 'MULTIPOINT', # MULTIPOINT
                  '06': 'MULTILINESTRING', # MULTILINE or MULTICURVE
                  '07': 'MULTIPOLYGON'} # MULTIPOLYGON
    
    # SDO_ETYPES
    # first element of triplet in SDO_ELEM_INFO
    sdo_etype = {0: 'UNSUPPORTED_GEOMETRY_ETYPE',
                 1: 'POINT_ETYPE',
                 2: 'LINE_ETYPE', 
                 4: 'COMPOUND_LINESTRING_ETYPE', 
                 1003: 'EXTERIOR_CLOSED_SHAPE_ETYPE', 
                 2003: 'INTERIOR_CLOSED_SHAPE_ETYPE',
                 1005: 'COMPOUND_EXTERIOR_CLOSED_SHAPE_ETYPE', 
                 2005: 'COMPOUND_INTERIOR_CLOSED_SHAPE_ETYPE'}
    
    # SDO_INTERPRETATIONS
    # second element of triplet in SDO_ELEM_INFO
    # applies to points - sdo_etype 1
    sdo_interpretation_point = {0: 'ORIENTED_POINT', 
                                1: 'SIMPLE_POINT'
                                # n > 1: point cluster with n points
                                }
    
    # applies to lines - sdo_etype 2
    sdo_interpretation_line = {1: 'STRAIGHT_SEGMENTS', 
                               2: 'CURVED_SEGMENTS'}
    
    # applies to polygons - sdo_etypes 1003 and 2003            
    sdo_interpretation_multi = {1: 'SIMPLE_POLY', 
                                2: 'ARCS_POLY', 
                                3: 'RECTANGLE', 
                                4: 'CIRCLE'} 
    
    # complex geometries - sdo_etypes 4, 1005, 2005 always have n > 1
    # n is the number of contiguous subelements
    # subsequent subelements each define one element
    
    
        
    # init function
    def __init__(self, sdo_geometry_obj=None, debug=False, strict=False):
        '''Read the geometry from the sdo_geometry object.
        
        Keyword arguments:
        debug - produce some debug output.
        strict - if False (default), convert geometry to a supported type 
                where possible,
        e.g. Oriented Point to Point
        '''
        if debug:
            print 'Debugging on.'
        # read the geometry from the sdo_geometry object
        self.geometry = sdo_geometry_obj
        try:
            self.g_type = str(int(self.geometry.__getattribute__('SDO_GTYPE')))
            self.g_srid = int(self.geometry.__getattribute__('SDO_SRID'))
            self.g_point = [self.geometry.__getattribute__('SDO_POINT').X, 
                            self.geometry.__getattribute__('SDO_POINT').Y, 
                            self.geometry.__getattribute__('SDO_POINT').Z]
            self.g_eleminfo_arr = self.geometry.__getattribute__(
                                       'SDO_ELEM_INFO')
            self.g_ords_arr = self.geometry.__getattribute__('SDO_ORDINATES')
        except AttributeError:
            if debug:
                print 'Not a geometry', 
            return None
        self.dims = self.get_dims()
        self.topology = self.has_topology()
        self.gtype = self.get_gtype()
        #self.wkb = self.get_wkb()
        self.valid = False
        self.wkt = self.get_wkt(debug)
        self.coord_dim = self.st_coorddim()
        ''' for the moment, this is a reference back to the valid property, 
        updated from self.get_wkt()'''
        self.is_valid = self.st_isvalid()
    
    # functions
    def get_dims(self):
        '''Return dimensions of the geometry.
        
        This is extracted from the first character of the SDO_ETYPE value
        '''
        return int(self.g_type[0])
    
    def st_coorddim(self):
        '''Return dimensions of the geometry.
        
        This is extracted from the first character of the SDO_ETYPE value
        This function is a synonym of get_dims
        '''
        return self.get_dims()
    
    def has_topology(self):
        '''Return true if the geometry has topology, false if not, or None.
        
        This is extracted from the second character of the SDO_ETYPE value.
        '''
        if 0 <= int(self.g_type[1]) <= 1:
            return int(self.g_type[1])
        else:
            return None
            
    def get_geometry_text(self):
        '''
        Return the type of geometry.
        This is extracted from the third and fourth characters 
        of the SDO_ETYPE value
        '''
        return self.geom_types[self.g_type[2:4]]
        
    def get_gtype(self):
        '''
        Return the type of geometry.
        This is extracted from the third and fourth characters 
        of the SDO_ETYPE value
        '''
        return int(self.g_type[2:4])
               
    def get_srid(self):
        '''Return the srid of the data.
        
        This is as defined in the database and may be an Oracle specific format
        (not EPSG).
        '''
        return self.g_srid    
    
    def get_num_elements(self):
        '''Return the number of elements in the SDO_ORDINATES array or None.
        
        These may be used more than once (end and start adjacent elements).
        '''
        if self.g_eleminfo_arr:
            return len(self.g_eleminfo_arr)
        else:
            return None
            
    def get_etype(self):
        '''Return the SDO_ETYPE value, or None if it is not defined.'''
        if not self.g_eleminfo_arr:
            return None
        else:
            return int(self.g_eleminfo_arr[1])

    def get_interpretation(self):
        '''Return the SDO_INTERPRETATION value, or None if not defined.'''
        if not self.g_eleminfo_arr:
            return None
        else:
            return int(self.g_eleminfo_arr[2])
            
    def get_point_text(self, point):
        '''Convert a point (2d or 3d list) into WKT text.
        
        input [x, y], [x, y, z, ...]
        return 'x y', 'x y z ...'
        '''
        if self.dims == 2:
            return '%.12f %.12f' % (point[0], point[1])
        else:
            return '%.12f %.12f %.12f' % (point[0], point[1], point[2])
            
    def to_points(self, l,n):
        '''Convert a list l into a list of smaller lists of dimension n.'''
        return [l[i:i+n] for i in xrange(0, len(l), n)]
        
    def points_to_WKT(self, points_list):
        '''Convert a list of points into WKT text format.
        
        This can then be used in simple or multi WKT.
        e.g. [x1, y1,...xn,yn] to 'x1 y1, ..., xn yn'
        '''
        wkt_text = ''
        for point in points_list:
            wkt_text += self.get_point_text(point)+','
        wkt_text = wkt_text[:-1]
        return wkt_text
            
    def make_arrays(self, g_eleminfo_arr, g_ords_arr, debug=False):
        '''Convert the ordinates to an array of points using SDO_ELEM_INFO.'''
        num_triplets = len(g_eleminfo_arr)/3
        triplets = self.to_points(g_eleminfo_arr, 3)
        if debug:
            print 'sets:',num_triplets, '\ntriplets:', triplets
        start_positions = []
        end_positions = []
        elem_type = []
        elements = []
        elem_text = []
        for i, triplet in enumerate(triplets):
            print i, triplet
            if i == 0:
                # first element
                start_positions.append(0)
            else:
                # intermediate element
                start_positions.append(int(triplets[i][0]) - 1)
            if i != num_triplets - 1: 
                # intermediate element
                end_positions.append(int(triplets[i+1][0]) - 1)
            else:
                # last element
                end_positions.append(len(g_ords_arr)) 
            elem_type.append(int(triplets[i][1]))
            if debug:
                print 'start:', start_positions, \
                      'end:', end_positions, \
                      'length:', len(g_ords_arr)
            elements.append(g_ords_arr[start_positions[i]:end_positions[i]])
            points = self.to_points(elements[i], self.dims)
            elem_text.append(self.points_to_WKT(points))
        if debug:
            print 'elements:', len(elements), '\nelem_text:', len(elem_text)
        return elem_text, elem_type
            
    def st_isvalid(self):
        '''Return True for valid geometry, False for invalid or None'''
        # Place holder for now.
        return self.valid
    
    def get_ewkt(self):
        '''Return EWKT - combine SRID and WKT.'''
        return 'SRID=%d:%s' % (self.get_srid(), self.get_wkt())

    def get_wkt(self, debug=False):
        '''Calculate the WKT for the geometry or None for invalid geometry.
        
        Point geometry may require only SDO_POINT, all other geometries 
        require the use of SDO_ELEM_INFO and SDO_ORDINATES.
        Geometry may be simple or complex.  Simple geometries are defined in 
        one SDO_ELEM_INFO triplet, Complex geometries require multiple 
        SDO_ELEM_INFO triplets.
        
        '''
        geom_type = self.get_geometry_text()
        if geom_type == 'UNKNOWN_GEOMETRY':
            return None
        
        elif geom_type == 'POINT':
            '''Return WKT - POINT(x y).'''
            if self.g_point:
                # case 1 - simple point
                point_text = self.get_point_text(self.g_point)
                if debug:
                    print point_text

            else:
                # case 2 - simple point - extract point from sdo_ordinates
                if  len(self.g_eleminfo_arr) == 3:
                    points = self.to_points(self.g_ords_arr, self.dims)

                # case 3 - oriented point 
                #        - doesn't seem to be supported in OGC WKT
                # truncate to first point
                else:
                    points = self.to_points(self.g_ords_arr, self.dims)
                    points = points[0]
                    
            if not point_text:
                point_text = self.points_to_WKT(points)
            self.valid = True
            return '%s(%s)' % (geom_type, point_text)  
                
        
        elif geom_type == 'LINESTRING':
            '''Return WKT - LINESTRING(x1 y1,x2 y2,...,xn yn)
            
            simple element, with a single SDO_ELEM_INFO triplet.
            each point is listed sequentially in the SDO_ORDINATES
            direct conversion to WKT
            
            '''
            # validity check - may need to expand
            if self.get_etype() != 2 or len(self.g_eleminfo_arr) != 3:
                self.valid = False
                return None
            # straight segments
            if self.get_interpretation() == 1 \
                or ( self.get_interpretation() == 2 \
                and strict == False):
                
                points = self.to_points(self.g_ords_arr, self.dims)
                ls_text = self.points_to_WKT(points)
                self.valid = True
                return '%s(%s)' % (geom_type, ls_text)
            # curved segments
            elif self.get_interpretation() == 2 and strict == True:
                # to do
                return None
                
            # compound linestrings - mix of straight and curved elements, 
            # each with a SDO_ELEM_INFO triplet, 
            # and each overlapping the last by one point
            
        elif geom_type == 'POLYGON':
            '''Return WKT - POLYGON((x1 y1,x2 y2,...,xn yn,x1 y1)
                                    (i1 j1, 12 j2,...,in jn,i1 j1))
            
            May include more than one element if there are internal holes
            There can be only one external ring, this must be listed 
            counterclockwise (not checked at present).
            There may be zero or more internal rings, these must be listed 
            clockwise (not checked at present), and each has one additional 
            SDO_ELEM_INFO triplet.
            Simple case
               Simple element, with a single SDO_ELEM_INFO triplet.
            Complex case
               More than 1 SDO_ELEM_INFO triplet, first for external ring
               , other(s) for internal rings.
            The triple can have the following values
            [0] - 1 - starting position (base 1)
            [1] - 1003 or 2003 (SDO_ETYPE)
            [2] - 1, 2, 3, or 4 (SDO_INTERPRETATION)
            Each point is listed sequentially in the SDO_ORDINATES
            The last point in a ring is the same as first point.
            
            '''
            # validity check - may need to expand ToDo - review
            if (self.get_etype() != 1003 and self.get_etype() != 2003):
                self.valid = False
                return None
            
            if self.get_interpretation() == 1 \
                or (self.get_interpretation() == 2 \
                and strict == False): 
            # straight segments
                elem_text, elem_type = self.make_arrays(self.g_eleminfo_arr
                                                      , self.g_ords_arr, debug)
                poly_text = ''
                for elem in elem_text:
                    poly_text = '%s(%s)' % (poly_text, elem)
                self.valid = True
                return '%s(%s)' % (geom_type, poly_text)
            elif self.get_interpretation() == 2:
                # curved segments
                return None
            elif self.get_interpretation() == 3:
                # rectangle - 2 points x1 y1, x2 y2 -> x1 y1, x1 y2, x2 y2, x2 y1, x1 y1
                coords = [self.g_ords_arr[0],self.g_ords_arr[1],
                          self.g_ords_arr[0],self.g_ords_arr[3],
                          self.g_ords_arr[3],self.g_ords_arr[3],
                          self.g_ords_arr[3],self.g_ords_arr[1],
                          self.g_ords_arr[0],self.g_ords_arr[1]]
                points = self.to_points(coords, self.dims)
                poly_text = '(%s)' % points
                self.valid = True
                return '%s(%s)' % (geom_type, poly_text) 
            elif self.get_interpretation() == 4:
                # circle - 3 points
                # this is, obviously, a triangle
                points = self.to_points(self.g_ords_arr, self.dims)
                poly_text = '(%s)' % points
                return '%s(%s)' % (geom_type, poly_text)
            else:
                # invalid
                return None

                
            
        elif geom_type == 'GEOMETRYCOLLECTION':
            '''Return WKT - GEOMETRYCOLLECTION(geom1,geom2,..,geomn) 
               - Container for other simple geometries 

            SDO_ELEM_ARRAY triples will define different geometries.  
            Need to watch for termination of polygons.
            
            It is not clear how oracle would handle multi geometries within a 
            geometry collection, so this is not implemented.
            
            '''
            elem_text, elem_type = self.make_arrays(self.g_eleminfo_arr
                                                      , self.g_ords_arr, debug)
            
            '''Create a holder for each element.'''
            elems = []
            elem_arr = []
            num_elems = len(elem_type)
            for i,e in enumerate(elem_type):
                if e == 1 or e == 2 or e == 4:
                    '''Point, linestring or compound linestring.'''
                    elems.append(e)
                    elem_arr.append(elem_text[i])
                else:
                    '''Polygon or multi geometry.
                    
                    For 1003 start a new sub element, for 2003 add to it.
                    '''
                    if e == 1003:
                        sub_elem_type = []
                        sub_elem = []
                        
                    if e == 1003 or e == 2003:
                        sub_elem_type.append(e)
                        sub_elem.append(elem_text[i])
                    
                    if i == num_elems:
                        elems.append(sub_elem_type)
                        elem_arr.append(sub_elem)
                    elif elem_type[i+1] <> 2003:
                        elems.append(sub_elem_type)
                        elem_arr.append(sub_elem)
                    else:
                        pass
            
            print elems, elem_arr
            
            '''Put the geometry string together.'''
            coll_text = 'GEOMETRY('
            for i, e in enumerate(elems):
                if e == 1:
                    point_text = 'POINT(%s)' % (elem_arr[i])
                    coll_text = coll_text + point_text + ','
                elif e == 2:
                    line_text = 'LINESTRING(%s)' % (elem_arr[i])
                    coll_text = coll_text + line_text + ','
                else:
                    poly_text = ''
                    for elem in elem_arr[i]:
                        poly_text = '%s(%s)' % (poly_text, elem)
                    poly_text = 'POLYGON(%s)' % (poly_text)
                    coll_text = coll_text + poly_text + ','
                
            coll_text = coll_text[:-1]+')'
            self.valid = True
            return coll_text
            
        elif geom_type == 'MULTIPOINT': # not tested yet !!!
            '''Return WKT - MULTIPOINT(x1 y1, ... , xn yn)
            
            OGC compliant alternative of 
               MULTIPOINT((x1 y1), ... , (xn yn)) is not currently supported.
            MultiPoint - series of points
            A single SDO_ELEM_INFO is used
            [0] SDO_STARTING_OFFSET === 1
            [1] SDO_ETYPE = 1
            [2] SDO_INTERPRETATION = number of points in array
            
            '''
            
            # validity check - may need to expand
            if self.get_etype() != 1:
                self.valid = False
                return None
            
            else:
                # num_Points = self.get_interpretation()
                points = self.to_points(self.g_ords_arr, self.dims)
                mp_text = self.points_to_WKT(points)
                self.valid = True
                return '%s(%s)' % (geom_type, mp_text)
            
        elif geom_type == 'MULTILINESTRING':
            '''Retrun WKT - MULTILINESTRING((x1 y1,...,xn yn)(i1 j1,...,in jn))
            
            MultiLineString - a series of line strings
            Each line string is defined by one SDO_ELEM_INFO triplet
            Mixed segments, with straight & cureved elements, 
            have different behaviour 
            - the end point of one segment is the start point of the next.
            
            '''
            
            # validity check - may need to expand
            if self.get_etype() != 2:
                valid = False
                return None
            
            else:
                # this is identical to polygons
                if self.get_interpretation() == 1 \
                       or (self.get_interpretation() == 2 \
                       and strict == False):
                    elem_text, elem_type = self.make_arrays(self.g_eleminfo_arr
                                                      , self.g_ords_arr, debug)
                    ml_text = ''
                    for elem in elem_text:
                        ml_text = '%s(%s)' % (ml_text, elem)
                    self.valid = True
                    return '%s(%s)' % (geom_type, ml_text)
                else:
                    # curved segments
                    return None
            
             
            
        elif geom_type == 'MULTIPOLYGON':
            '''Return WKT - MULTIPOLYGON(((x1 y1, ... , x1 y1) 
                                      ((x2 y2, ..., x2 y2)(x3 y3, ..., x3 y3)))
            
            MultiPolygon - a number of polygons, each with one external ring  
            and zero or more internal rings.
            External rings have SDO_ETYPE of 1003 and are grouped with 
            subsequent internal rings, which have SDO_ETYPE of 2003 so 
            SDO_ELEM_INFO like [1,1003,1, 1,1003,1, 1,2003,1] maps to 
            (note parenthesis (( ()  (()()) )
            MULTIPOLYGON(((x1 y1, ... , x1 y1)((x2 y2, ..., x2 y2)
                                              (x3 y3, ..., x3 y3)))
            SDO_ELEM_INFO[1] == 1003 => start new outer ring
            SDO_ELEM_INFO[2] => start position
            
            '''
            # validity check
            if self.get_interpretation() == 1 \
                or (self.get_interpretation() == 2 \
                and strict == False):
                pass
            
            elem_text, elem_type = self.make_arrays(self.g_eleminfo_arr
                                                      , self.g_ords_arr, debug)
            mp_text = ''
            for i, elem in enumerate(elem_text):
                if elem_type[i] == 1003: # outer ring
                    # start with '(('
                    mp_text += '('
                elif elem_type[i] == 2003: # inner ring
                    # start with '('
                    pass
                mp_text = '%s(%s)' % (mp_text, elem)
                if i == len(elem_type) - 1: # last element
                    mp_text += ')'
                elif elem_type[i+1] == 1003: # terminate outer ring
                    mp_text += ')'
            self.valid = True
            return '%s(%s)' % (geom_type, mp_text)
        else:
            return None
    
