from gk_shape import *
from gk_shapes_basic import *
from gk_shape_modify import *
from numpy import *

class gds_shape_modifier(gds_dynamic_shape):
    def __init__(self, original_shape):
        gds_dynamic_shape.__init__(self)
        self.original_shape = original_shape

    def set_original_shape(self, shape):
        if isinstance(shape, gds_shape):
            self.__original_shape__ = shape
        else:
            self.__original_shape__ = gds_shape(shape)
    def get_original_shape(self):
        return self.__original_shape__
    original_shape = property(get_original_shape, set_original_shape)

    def move(self, position):
        self.original_shape = shape_translate(self.original_shape)
        return self

class gds_shape_restricted_modifier(gds_shape_modifier):
    def __init__(self, original_shape):
        gds_dynamic_shape.__init__(self)
        self.original_shape = original_shape

    def set_original_shape(self, shape):
        if isinstance(shape, gds_shape):
            original_shape = shape
        else:
            original_shape = gds_shape(shape)
        if self.__verify_original_shape__(original_shape):
            self.__original_shape__ = original_shape

    def get_original_shape(self):
        return self.__original_shape__
    original_shape = property(get_original_shape, set_original_shape)

    def move(self, position):
        self.original_shape = shape_translate(self.original_shape)
        return self

class gds_shape_round(gds_shape_modifier):
    """ returns a shape with rounded corners based on a given shape """
    def __init__(self, original_shape, radius, angle_step = None):
        if angle_step == None:
            angle_step = get_angle_step()
        gds_shape_modifier.__init__(self, original_shape)
        self.radius = radius
        self.angle_step = angle_step

    def __calc__(self):
        s = gds_shape(self.original_shape).remove_straight_angles()
        c = s.__points__
        if len(c)== 0: return

        min_r = self.radius
        D = s.distances()
        a2 = s.angles_rad() # angle to next vertex
        a1 = roll(a2, 1)   # angle from previous vertex
        t = (a2 - a1+pi)%(2*pi) - pi # turns, save an extra angle calculation
        tt = abs(tan(0.5*t))
        r = ones(size(t)) * self.radius  # bend radii
        L = r * tt # length of the straight section consumed by the bend
        if not self.original_shape.closed:
            L[0] = 0
            L[-1] = 0
        # check where the bend consumes more length than possible!
        m_L = ((L + roll(L, -1))-D) # missing length in the next segment
        missing_L = amax(column_stack((m_L, roll(m_L, 1))), 1) # missing length over previous and next segment
        overf = (missing_L > 0)
        r[overf] = 0.5*(amin(column_stack((D[overf], roll(D, 1)[overf])), 1)) / tt[overf]
        if min(r) < self.radius:
            print >> stderr, "Warning: Bend radius is reduced from " + str(self.radius) + " to " + str(min(r)) + "to round shape."
        if not self.original_shape.closed:
            r[0] = 0
            r[-1] = 0
        L = r * tt # recalculate length of the straight section consumed by the bend

        # create the bends
        s = c - column_stack((L*cos(a1), L * sin(a1))) # bend start points

        S = []
        if not self.original_shape.closed:
            S.append(array([c[0]]))
        for i in range(1, len(c)-1):
            sh = shape_bend_rel(s[i], r[i], a1[i]*RAD2DEG, t[i]*RAD2DEG, self.angle_step)
            S.append(sh.points)

        if self.original_shape.closed:
            sh = shape_bend_rel(s[-1], r[-1], a1[-1]*RAD2DEG, t[-1]*RAD2DEG, self.angle_step)
            S.append(sh.points)
            sh = shape_bend_rel(s[0], r[0], a1[0]*RAD2DEG, t[0]*RAD2DEG, self.angle_step)
            S.append(sh.points)
            self.__closed__ = True
        else:
            # open curve
            S.append(array([c[-1]]))
            self.__closed__ = False
        self.__points__ = vstack(S)

def shape_round(original_shape, radius, angle_step = None):
    """ rounds the corners of a shape """
    return gds_shape_round(original_shape, radius, angle_step )

class gds_shape_stub(gds_shape_modifier):
    """ stubs the corners of a given shape """
    def __init__(self, original_shape, stub_width, Only_Sharp_Angles = False):
        gds_shape_modifier.__init__(self, original_shape)
        self.stub_width = stub_width
        self.only_sharp_angles = Only_Sharp_Angles

    def __calc__(self):
        c = gds_shape(self.original_shape)
        if len(c)== 0: return

        if self.original_shape.is_closed():
            if not c[0] == c[-1]:
                c.append(c[0])
            #closed curve
            c.append(c[1])
        else:
            # open curve
            self.append(c[0])

        min_sw = self.stub_width
        for i in range(1, len(c)-1):
            angle1 = angle_rad(c[i], c[i-1])
            angle2 = angle_rad(c[i+1], c[i])
            turn = (angle2 - angle1+pi)%(2*pi) - pi
            if turn==0 or (abs(turn)<=(pi/2.0) and self.only_sharp_angles):
                self.append(c[i])
            elif abs(turn == pi):
                print >> stderr, "Error: Cannot stub shape with a 180 degree turn"
            else:
                d1 = distance(c[i], c[i-1])
                d2 = distance(c[i+1], c[i])
                L = self.stub_width * sin(turn/2.0)/sin(turn)
                max_L = max([d1/2.0, d2/2.0])
                if L > max_L:
                    L = max_L
                    sw = L * sin(turn)/sin(turn/2.0)
                else:
                    sw = self.stub_width
                if sw < min_sw:
                    min_sw = sw

                s1 = (c[i][0]- L* cos(angle1), c[i][1] - L * sin(angle1))
                s2 = (c[i][0]+ L* cos(angle2), c[i][1] + L * sin(angle2))
                self.append(s1)
                self.append(s2)
        if self.original_shape.closed:
            #closed curve
            self.close()
        else:
            # open curve
            self.append(c[-1])

        if min_sw < self.stub_width:
            print >> stderr, "Warning: Stub width is reduced from " + str(self.stub_width) + " to " + str(min_sw) + "to stub shape."

class gds_shape_serif(gds_shape_modifier):
    """ puts a bump on the corners of a given shape """
    def __init__(self, original_shape, stub_width, stub_height, tip_width = 0.0, Only_Sharp_Angles = False):
        gds_shape_modifier.__init__(self, original_shape)
        self.stub_width = stub_width
        self.stub_height = stub_height
        if tip_width <= 0.0:
                tip_width = stub_width
        self.tip_width = tip_width
        self.only_sharp_angles = Only_Sharp_Angles
                 
    def __calc__(self):
        if self.tip_width <= 0.0:
            tip_width = self.stub_width
        else:
            tip_width = self.tip_width

        c = gds_shape(self.original_shape)
        if len(c)== 0: return

        if self.original_shape.is_closed():
            if not c[0] == c[-1]:
                c.append(c[0])
            #closed curve
            c.append(c[1])
        else:
            # open curve
            self.append(c[0])

        min_sw = self.stub_width
        for i in range(1, len(c)-1):
            angle1 = angle_rad(c[i], c[i-1])
            angle2 = angle_rad(c[i+1], c[i])
            turn = (angle2 - angle1+pi)%(2*pi) - pi
            if turn==0 or (abs(turn)<=(pi/2.0) and self.only_sharp_angles):
                self.append(c[i])
            elif abs(turn == pi):
                print >> stderr, "Error: Cannot stub shape with a 180 degree turn"
            else:
                d1 = distance(c[i], c[i-1])
                d2 = distance(c[i+1], c[i])
                L = self.stub_width * sin(turn/2.0)/sin(turn)
                max_L = max([d1/2.0, d2/2.0])
                if L > max_L:
                    L = max_L
                    sw = L * sin(turn)/sin(turn/2.0)
                else:
                    sw = self.stub_width
                if sw < min_sw:
                    min_sw = sw

                theta_div2 = (pi - angle1 + angle2)/2.0
                s1 = coord2(c[i][0]- L* cos(angle1), c[i][1] - L * sin(angle1))
                s2 = (c[i][0]+ L* cos(angle2), c[i][1] + L * sin(angle2))

                B = -self.stub_height * sign(turn)
                delta = 0.5 * (self.stub_width - tip_width)
                s2 = (s1[0] + B * cos(angle1 + theta_div2) + delta*cos(angle1 + theta_div2-pi/2.0),
                      s1[1] + B * sin(angle1 + theta_div2) + delta*sin(angle1 + theta_div2-pi/2.0))
                s4 = coord2(c[i][0]+ L* cos(angle2), c[i][1] + L * sin(angle2))
                s3 = (s4[0] + B * cos(angle2 + pi - theta_div2) + delta*cos(angle2 + pi - theta_div2 + pi/2.0),
                      s4[1] + B * sin(angle2 + pi - theta_div2) + delta*sin(angle2 + pi - theta_div2 + pi/2.0))
                self.append(s1)
                self.append(s2)
                self.append(s3)
                self.append(s4)

                
        if self.original_shape.closed:
            #closed curve
            self.close()
        else:
            # open curve
            self.append(c[-1])

        if min_sw < self.stub_width:
            print >> stderr, "Warning: Stub width is reduced from " + str(self.stub_width) + " to " + str(min_sw) + "to stub shape."



def shape_stub(original_shape, stub_width, Only_Sharp_Angles = False):
    """ stubs the corners of a given shape """
    return gds_shape_stub(original_shape, stub_width, Only_Sharp_Angles)

class gds_shape_path_basic(gds_shape_modifier):
    """ base class for path shapes"""
    def __init__(self, original_shape, path_width):
        gds_shape_modifier.__init__(self, original_shape)
        self.path_width = path_width
        self.closed = True


class gds_shape_path_simple(gds_shape_path_basic):
    """ simple path basec on a centerline shape """
    def __init__(self, original_shape, path_width):
        gds_shape_path_basic.__init__(self, original_shape, path_width)
        #if hasattr(self.original_shape, "start_angle"):
            #self.start_angle = original_shape.start_angle
        #else:
            #self.start_angle = None

        #if hasattr(self.original_shape, "end_angle"):
            #self.end_angle = original_shape.end_angle
        #else:
            #self.end_angle = None

    def __calc__(self):
        original = gds_shape(self.original_shape).remove_straight_angles()
        if len(original) <= 1: return

     


        a2 = original.angles_rad() * 0.5
        a1 = roll(a2, 1)
        
        if not original.closed:
            if hasattr(self.original_shape, "end_angle"):
                if self.original_shape.end_angle != None:
                    a2[-1] = self.original_shape.end_angle * DEG2RAD * 0.5
                else:
                    a2[-1] = a1[-1]
            else:
                a2[-1] = a1[-1]


            if hasattr(self.original_shape, "start_angle"):
                if self.original_shape.start_angle != None:
                    a1[0] = self.original_shape.start_angle * DEG2RAD * 0.5
                else:
                    a1[0] = a2[0]
            else:
                a1[0] = a2[0]

        a_plus = a2 + a1
        cos_a_min = cos(a2 - a1)
        offsets = column_stack((-sin(a_plus) / cos_a_min, cos(a_plus) / cos_a_min))*(0.5*self.path_width)

        # calculate offsets from each point
        self.__points__ = vstack((original.__points__ + offsets, flipud(original.__points__ - offsets)))


class gds_shape_path_spike(gds_shape_path_basic):
    """ simple path based on a centerline shape,but with a sharp endpoint with a given angle """
    def __init__(self, original_shape, path_width, spike_angle = 90.0):
        gds_shape_path_basic.__init__(self, original_shape, path_width)
        self.spike_angle = spike_angle
        
    def __calc__(self):
        original = gds_shape(self.original_shape).remove_straight_angles()
        if len(original) <= 1: return
        a = original.angles_rad()
        a2 = a * 0.5
        a1 = roll(a2, 1)
        if not original.closed:
            a2[-1] = a1[-1]
            a1[0] = a2[0]
        a_plus = a2 + a1
        cos_a_min = cos(a2 - a1)
        offsets = column_stack((-sin(a_plus) / cos_a_min, cos(a_plus) / cos_a_min))*(0.5*self.path_width)
       
        # spikes
        if not original.closed and self.spike_angle > 0 and self.spike_angle < 180.0:
            L = 0.5*self.path_width/ tan(self.spike_angle * DEG2RAD * 0.5)
            start_spike = array([[original[0][0] - cos(a[0]) * L,  original[0][1] - sin(a[0])*L]])
            end_spike = array([[original[-1][0] + cos(a[-2]) * L,  original[-1][1] + sin(a[-2])*L]])
        else:
            start_spike = ndarray((0, 2))
            end_spike = ndarray((0, 2))
        
        self.__points__ = vstack((start_spike, original.__points__ + offsets, end_spike, flipud(original.__points__ - offsets)))


class gds_shape_path_rounded(gds_shape_path_basic):
    """ path with rounded corners based on a centerline """
    def __init__(self, original_shape, path_width):
        gds_shape_path_basic.__init__(self, original_shape, path_width)
        gds_shape_modifier.__init__(self, original_shape)

    def __calc__(self):

        left_coords = gds_shape()
        right_coords = gds_shape()

        coords = shape_remove_identicals(self.original_shape)
        if len(coords)== 0: return

        # begin
        x = coords[0][0]
        y = coords[0][1]
        angle = atan2(coords[1][1] - y, coords[1][0] - x)
        sa = sin(angle)
        ca = cos(angle)
        left_coords.extend(shape_arc((x,y), self.path_width/2.0, (angle*RAD2DEG) + 270.0, (angle*RAD2DEG) + 90.0, clockwise=True))


        #middle
        for i in range(1,len(coords)-1):
            x = coords[i][0]
            y = coords[i][1]
            angle1 = atan2(y - coords[i-1][1], x - coords[i-1][0])
            angle2 = atan2(coords[i+1][1] - y, coords[i+1][0] - x)
            angle = angle1 + 0.5* ( angle2 - angle1 + pi )%(pi) - 0.5*pi
            ca = cos(angle)
            sa = sin(angle)
            turn = (angle2 - angle1)%(2*pi)
            if turn == pi:
                print >> stderr, "Error: Path to Boundary conversion is not possible with paths that turn 180 degree at a node"
                raise SystemExit
            w = 0.5*self.path_width / abs(cos(0.5*turn))

            c_left = ( x -w*sa, y +  w* ca)
            c_right = (x + w*sa, y -  w* ca)
            if turn < pi:
                # turn left
                left_coords.append(c_left)
                right_coords.extend(shape_arc(c_left, self.path_width, (angle1*RAD2DEG)-90.0, (angle2*RAD2DEG)-90.0, clockwise= False))
            else:
                # turn right
                left_coords.extend(shape_arc(c_right, self.path_width, (angle1*RAD2DEG)+90.0, (angle2*RAD2DEG)+90.0, clockwise= True))
                right_coords.append(c_right)

        #end
        x = coords[-1][0]
        y = coords[-1][1]
        angle = atan2(y - coords[-2][1], x - coords[-2][0] )
        sa = sin(angle)
        ca = cos(angle)
        right_coords.extend (shape_arc((x,y), self.path_width/2.0, (angle*RAD2DEG) -90.0, (angle*RAD2DEG) + 90.0))
        right_coords.reverse()
        self.extend(left_coords)
        self.extend(right_coords)
        self.append(left_coords[0])

class gds_shape_path_extended(gds_shape_path_basic):
    """ path with extended ends based on a centerline shape """
    def __init__(self, original_shape, path_width):
        gds_shape_path_basic.__init__(self, original_shape, path_width)
        gds_shape_modifier.__init__(self, original_shape)

    def __calc__(self):

        left_coords = gds_shape()
        right_coords = gds_shape()

        coords = shape_remove_identicals(self.original_shape)
        if len(coords)== 0: return

        # begin
        x = coords[0][0]
        y = coords[0][1]
        angle = atan2(coords[1][1] - y, coords[1][0] - x)
        sa = sin(angle)
        ca = cos(angle)
        left_coords.append((x - 0.5*self.path_width*(sa+ca), y + 0.5 * self.path_width * (ca - sa)))
        right_coords.append((x + 0.5*self.path_width*(sa - ca), y - 0.5 * self.path_width * (ca + sa)))


        #middle
        for i in range(1,len(coords)-1):
            x = coords[i][0]
            y = coords[i][1]
            angle1 = atan2(y - coords[i-1][1], x - coords[i-1][0])
            angle2 = atan2(coords[i+1][1] - y, coords[i+1][0] - x)
            angle = angle1 + 0.5* ( angle2 - angle1 + pi )%(pi) - 0.5*pi
            ca = cos(angle)
            sa = sin(angle)
            turn = (angle2 - angle1)%(2*pi)
            if turn == pi:
                print >> stderr, "Error: Path to Boundary conversion is not possible with paths that turn 180 degree at a node"
                raise SystemExit
            w = 0.5*self.path_width / abs(cos(0.5*turn))

            c_left = ( x -w*sa, y +  w* ca)
            c_right = (x + w*sa, y -  w* ca)
            left_coords.append(c_left)
            right_coords.append(c_right)

        #end
        x = coords[-1][0]
        y = coords[-1][1]
        angle = atan2(y - coords[-2][1], x - coords[-2][0] )
        sa = sin(angle)
        ca = cos(angle)
        left_coords.append((x - 0.5*self.path_width*(sa-ca), y + 0.5 * self.path_width * (ca+sa)))
        right_coords.append((x + 0.5*self.path_width*(sa+ca), y - 0.5 * self.path_width * (ca-sa)))
        right_coords.reverse()
        self.extend(left_coords)
        self.extend(right_coords)
        self.append(left_coords[0])


class gds_shape_path_stub(gds_shape_path_basic):
    """ path with stubbed corners based on a centerline shape """
    def __init__(self, original_shape, path_width, stub_width = 0.0):
        gds_shape_path_basic.__init__(self, original_shape, path_width)
        gds_shape_modifier.__init__(self, original_shape)
        self.stub_width = stub_width
        if hasattr(self.original_shape, "start_angle"):
            self.start_angle = original_shape.start_angle
        else:
            self.start_angle = None

        if hasattr(self.original_shape, "end_angle"):
            self.end_angle = original_shape.end_angle
        else:
            self.end_angle = None

    def __calc__(self):

        left_coords = gds_shape()
        right_coords = gds_shape()

        coords = shape_remove_identicals(self.original_shape)
        if len(coords)== 0: return

        # begin
        x = coords[0][0]
        y = coords[0][1]
        angle = atan2(coords[1][1] - y, coords[1][0] - x)
        sa = sin(angle)
        ca = cos(angle)
        if self.start_angle == None:
            new_angle = angle
            turn = 0.0
        else:
            new_angle = self.start_angle * DEG2RAD
            turn = 2 * (angle - new_angle)%(pi)
        w = 0.5 * self.path_width / abs(cos(turn/2.0))
        left_coords.append(( x -w*sin(new_angle), y +  w* cos(new_angle)))
        right_coords.append((x + w*sin(new_angle), y - w* cos(new_angle)))


        #middle
        for i in range(1,len(coords)-1):
            x = coords[i][0]
            y = coords[i][1]
            angle1 = atan2(y - coords[i-1][1], x - coords[i-1][0])
            angle2 = atan2(coords[i+1][1] - y, coords[i+1][0] - x)
            angle = angle1 + 0.5* ( angle2 - angle1 + pi )%(pi) - 0.5*pi
            ca = cos(angle)
            sa = sin(angle)
            turn = (angle2 - angle1)%(2*pi)
            if turn == pi:
                print >> stderr, "Error: Path to Boundary conversion is not possible with paths that turn 180 degree at a node"
                raise SystemExit
            w = 0.5*self.path_width / abs(cos(0.5*turn))

            c_left = ( x -w*sa, y +  w* ca)
            c_right = (x + w*sa, y -  w* ca)
            left_coords.append(c_left)
            right_coords.append(c_right)

        #end
        x = coords[-1][0]
        y = coords[-1][1]
        angle = atan2(y - coords[-2][1], x - coords[-2][0] )
        sa = sin(angle)
        ca = cos(angle)
        if self.end_angle == None:
           new_angle = angle
           turn = 0.0
        else:
           new_angle = self.end_angle * DEG2RAD
           turn = 2*(new_angle - angle)%(pi)
        w = 0.5*self.path_width / abs(cos(0.5*turn))
        left_coords.append(( x -w *sin(angle), y + w * cos(angle)))
        right_coords.append((x + w *sin(angle), y - w * cos(angle)))

        if (self.stub_width > 0):
            right_coords = shape_stub(right_coords, self.stub_width)
            left_coords = shape_stub(left_coords, self.stub_width)
        right_coords.reverse()
        self.extend(left_coords)
        self.extend(right_coords)
        self.append(left_coords[0])



def shape_path(original_shape, path_width, path_type= PATH_TYPE_NORMAL, stub_width = 0.0):
    """ path shape based on centerline """
    if stub_width != 0.0:
        return gds_shape_path_stub(original_shape, path_width, stub_width = 0.0)
    elif path_type == PATH_TYPE_NORMAL:
        return gds_shape_path_simple(original_shape, path_width )
    elif path_type == PATH_TYPE_EXTENDED:
        return gds_shape_path_extended(original_shape, path_width, extended )
    elif path_type == PATH_TYPE_ROUNDED:
        return gds_shape_path_rounded(original_shape, path_width )

def gds_shape_path(original_shape, path_width, path_type= PATH_TYPE_NORMAL, stub_width = 0.0):
    return shape_path(original_shape, path_width, path_type, stub_width)

class gds_shape_offset(gds_shape_modifier):
    """ generates a shape with a given offset from its original shape"""
    def __init__(self, original_shape, offset):
        gds_shape_modifier.__init__(self, original_shape)
        self.offset = offset

    def __calc__(self):
        original = gds_shape(self.original_shape).remove_straight_angles()
        if len(original) <= 1: return
        
        a2 = original.angles_rad() * 0.5
        a1 = roll(a2, 1)
        if not original.closed:
            a2[-1] = a1[-1]
            a1[0] = a2[0]
        a_plus = a2 + a1
        cos_a_min = cos(a2 - a1)
        offsets = column_stack((-sin(a_plus) / cos_a_min, cos(a_plus) / cos_a_min))*(self.offset)

        # calculate offsets from each point
        self.__points__ = (original.__points__ + offsets)

class gds_shape_grow(gds_shape_offset):
    """ generates a shape with uniformly grows """
    def __init__(self, original_shape, amount):
        gds_shape_offset.__init__(self, original_shape, amount)
        self.amount= amount

    def __calc__(self):
        self.offset = self.original_shape.orientation() * self.amount
        gds_shape_offset.__calc__(self)

class gds_shape_variable_offset(gds_shape_modifier):
    """ generates a shape with a variable offset from its original shape"""
    def __init__(self, original_shape, offsets):
        gds_shape_modifier.__init__(self, original_shape)
        self.offset = offset

    def __calc__(self):
        if len(self.offset) != len(self.original_shape):
            raise AssertionError("Length of shape and offsets should be the same in gds_shape_variable_offset")
        original = self.original_shape
        if len(original) <= 1: return
        
        a2 = original.angles_rad() * 0.5
        a1 = roll(a2, 1)
        if not original.closed:
            a2[-1] = a1[-1]
            a1[0] = a2[0]
        a_plus = a2 + a1
        cos_a_min = cos(a2 - a1)
        offsets = column_stack((-sin(a_plus) / cos_a_min *self.offset, cos(a_plus) / cos_a_min * self.offset))

        # calculate offsets from each point
        self.__points__ = (original.__points__ + offsets)

class gds_shape_extend_ends(gds_shape_modifier):
    """ stubs the corners of a given shape """
    def __init__(self, original_shape, start_extension, end_extension):
        gds_shape_modifier.__init__(self, original_shape)
        self.start_extension = start_extension
        self.end_extension = end_extension

    def __calc__(self):
        c = gds_shape(self.original_shape)
        if c.closed:
            self += c
            return
        
        a1 = angle_rad(c[0], c[1])
        self += coord2(c[0].x + cos(a1) * self.start_extension, c[0].y + sin(a1) * self.start_extension)
        self += c[1:-1]
        a1 = angle_rad(c[-1], c[-2])
        self += coord2(c[-1].x + cos(a1) * self.end_extension, c[-1].y + sin(a1) * self.end_extension)
        
