
# CIF.py : CIF parsing and rendering
#
# Distributed under the terms described in the LICENCE file.
#
# Developed by akuchlin@mems-exchange.org
#

__revision__ = "$Id: CIF.py,v 1.14 2002/02/19 22:31:34 akuchlin Exp $"

import string, sys, re, math

from matrix import Matrix3x3

ParseError = "CIF parsing error"

# Regex patterns for parsing various bits of CIF
blank = '[^-\dA-Z();]'
sep =   '[^-\d();]'
integer =   '\d+'
point = '(-?\d+) %s+ (-?\d+)' % sep
point_pat = re.compile(point, re.VERBOSE)
path = '%s(%s+ %s)*' % (point, sep, point)
path_pat = re.compile(path, re.VERBOSE)
transformation = """ %s*
(?P<transformation> T %s* %s |
 M %s* X | M %s* Y | 
 R %s* %s)
""" % (sep,
       blank, point,
       blank, blank, 
       blank, point)
transformation_pat = re.compile(transformation, re.VERBOSE | re.IGNORECASE)

def parse_point (point, scale):
    m2 = point_pat.match(point)
    return (scale * int(m2.group(1)), scale * int(m2.group(2)))

class CIFElement:
    "Class representing CIF elements such as polygons or wires"
    def __init__ (self):
        raise RuntimeError, ('Not implemented in class ' +
                             self.__class__.__name__)

    def display (self, context):
#        return
        raise RuntimeError, ('Not implemented in class ' +
                             self.__class__.__name__)

    def parse (self, cmd, scale):
        raise RuntimeError, ('Not implemented in class ' +
                             self.__class__.__name__)

    def __repr__ (self):
        return '<%s instance>' % self.__class__.__name__
    __str__ = __repr__
    
class Polygon (CIFElement):
    def __init__ (self):
        self.path = []

    def parse (self, cmd, scale):
        p = point_pat.findall(cmd)
        path = []
        for x,y in p:
            pt = (int(x) * scale, int(y) * scale)
            path.append(pt)
        self.path = path

    def display (self, context):
        context.draw_polygon(self.path)
    
box_args_pat = re.compile("%s* (?P<length>%s) %s+ (?P<width>%s) %s "
                          "(?P<center>%s) (?:%s+ (?P<direction>%s))?"
                          % (sep, integer, sep, integer, sep,
                             point, sep, point),
                          re.VERBOSE | re.IGNORECASE)
class Box (CIFElement):
    def __init__ (self):
        self.width = self.length = 0
        self.center = self.direction = (0,0)
        
    def parse (self, cmd, scale):
        m = box_args_pat.match(cmd)
        if m is None:
            raise ParseError, "Incorrect B arguments: " +repr(cmd)
        self.width  = int(m.group('width')) * scale
        self.length = int(m.group('length')) * scale 

        self.center = parse_point(m.group('center'), scale)

        direction = m.group('direction')
        if direction is None:
            self.direction = (1,0)
        else:
            self.direction = parse_point(direction)

    def display (self, context):
        xc, yc = self.center
        ul_corner = (xc - self.length/2, yc - self.width/2)
        ll_corner = (xc - self.length/2, yc + self.width/2)
        lr_corner = (xc + self.length/2, yc + self.width/2)
        ur_corner = (xc + self.length/2, yc - self.width/2)

        # XXX rotate using direction vector
        context.draw_polygon([ul_corner, ll_corner, lr_corner,
                               ur_corner])
        
roundflash_args_pat = re.compile("%s* (?P<diameter>%s) %s+ (?P<center>%s)"
                                % (sep, integer, sep, point),
                                re.VERBOSE | re.IGNORECASE)

class RoundFlash (CIFElement):
    def __init__ (self):
        self.diameter = 0
        self.center = (0,0)

    def parse (self, cmd, scale):
        m = roundflash_args_pat.match(cmd)
        if m is None:
            raise ParseError, "Incorrect R arguments: " +repr(cmd)
        self.diameter  = scale * int(m.group('diameter'))
        self.center = parse_point(m.group('center'), scale)

    def display (self, context):
        xc, yc = self.center
        radius = self.diameter / 2
        context.draw_circle(self.center, radius)
        
wire_args_pat = re.compile("%s* (?P<width>%s) %s+ (?P<path>%s)"
                           % (sep, integer, sep, path),
                           re.VERBOSE | re.IGNORECASE)

class Wire (CIFElement):
    def __init__ (self):
        self.width = 0
        self.path = []

    def parse (self, cmd, scale):
        m = wire_args_pat.match(cmd)
        if m is None:
            raise ParseError, "Incorrect W arguments: " +repr(cmd)
        
        self.width = scale * int(m.group('width'))

        path_str = m.group('path')
        p = point_pat.findall(path_str)
        path = []
        for x,y in p:
            pt = (scale * int(x), scale * int(y))
            path.append(pt)
        self.path = path

    def display (self, context):
        context.draw_wire(self.path, self.width)

layer_args_pat = re.compile('%s* (?P<layer>\w{1,4}) %s* $' % (blank, sep),
                            re.VERBOSE)
                            
class Layer (CIFElement):
    def __init__ (self):
        self.name = ""

    def parse (self, cmd, scale):
        m = layer_args_pat.match(cmd)
        if m is None:
            raise ParseError, "Incorrect L arguments: " +repr(cmd)
        self.name = m.group('layer')

    def display (self, context):
        context.set_layer(self.name)
       
call_args_pat = re.compile("%s* (?P<number>%s) (?P<trans_list> %s+)?"
                           % (blank, integer, transformation),
                           re.VERBOSE | re.IGNORECASE)
class Call (CIFElement):
    def __init__ (self):
        self.number = None

        # Transformation matrix specified for this call
        self.transform = None
        
    def parse (self, cmd, scale):
        m = call_args_pat.match(cmd)
        if m is None:
            raise ParseError, "Incorrect C arguments: " +repr(cmd)

        self.number = int(m.group('number'))

        # Initialize the transformation matrix to the identity
        T = Matrix3x3()

        transf = m.group('trans_list')
        if transf is not None:
            # Get list of all the transformation patterns; since
            # the pattern includes subgroups, the list looks something
            # like [('T -900000 -2564000', '-900000', '-2564000', '', '')].
            transformations = transformation_pat.findall(transf)

            # Throw away the extra groups
            transformations = map(lambda x: x[0], transformations)

            # The list is now something like ['T -900000 -256', 'MX', ...]
            for step in transformations:
                mult = None

                # T a,b: Translate by a,b
                m = re.match('(?x)T%s*(?P<point>%s)' % (sep,point), step)
                if m is not None:
                    a,b = parse_point(m.group('point'), scale)
                    mult = Matrix3x3([ [1,0,0], [0,1,0], [a,b,1 ] ])

                # R a,b: Rotation to direction vector a,b
                m = re.match('(?x)R%s*(?P<point>%s)' % (sep,point), step)
                if m is not None:
                    a,b = parse_point(m.group('point'), scale)
                    c = math.sqrt(a*a + b*b)
                    mult = Matrix3x3([ [a/c,b/c,0], [-b/c,a/c,0], [0,0,1 ] ])

                # MX: mirroring in the X direction
                if re.match('M%s*X' % sep, step):
                    mult = Matrix3x3([ [-1,0,0], [0,1,0], [0,0,1] ])

                # MY: mirroring in the Y direction
                elif re.match('M%s*Y' % sep, step):
                    mult = Matrix3x3([ [1,0,0], [0,-1,0], [0,0,1] ])

                if mult is None:
                    raise ParseError, 'Unknown transformation: ' + repr(step)
                
                T = T * mult

        self.transform = T
        
    def display (self, context):
        pass
            
class UserExtension (CIFElement):
    def __init__ (self):
        self.args = []

    def parse (self, cmd, scale):
        self.args = string.split(cmd)

    def display (self, context):
        pass

class Comment (CIFElement):
    def __init__ (self):
        self.text = ""

    def parse (self, cmd, scale):
        self.text = cmd

    def display (self, context):
        pass
    
delete_args_pat = re.compile("%s* (?P<number>%s)" %(blank, integer),
                             re.VERBOSE | re.IGNORECASE)
class Delete (CIFElement):
    def __init__ (self):
        self.number = None
    
    def parse (self, cmd, scale):
        m = delete_args_pat.match(cmd)
        self.number = int(m.group('number'))
    

COMMANDS = {'P':Polygon, 'B':Box, 'R':RoundFlash, 'W': Wire,
            'L': Layer, 'C': Call, '(':Comment }

class TkRenderingContext:

    """The TkRenderingContext class stores all the display parameters
    used to display a CIF file on a Tk Canvas widget.  Some parameters
    are set by the Client class in the GUI module, while others are intended
    to be user-tuneable.  This is why there are often redundant parameters
    that are used in the same way; both the _translate and _offset values
    are added to the x,y coordinates, but _translate is set by the Client
    class, and users can tweak the _offset values.

    Set by the Client class:
    {x,y}_scale : Scaling factor for converting from microns to pixels;
                  this is provided by the microscope server.
    {x,y}_translate : Amount to slide the layout over; depends on the
                      canvas size.  Measured in microns.
    {x_y}_shift : Another translation, used to compensate for the size
                  of the displayed image.  (When you're at x,y, that's
                  the point in the center of the image, not the top
                  left corner.)  Measured in pixels.

    Set by the user:
    {x,y}_multiplier : Scaling factor used to shrink or grow the layout
    {x,y}_offset : Amount to slide the layout over, in microns.
    """
    
    def __init__ (self, canvas):
        self.canvas = canvas

        # set default values for everything
        self.set_direction((1,0))
        self.set_scale(1.0, 1.0)
        self.set_multiplier(1.0, 1.0)
        self.set_shift(0, 0)

        self.x_translate, self.y_translate = 0,0
        self.x_offset, self.y_offset = 0,0

        self.layer = None
        self.hidden_layers = []
        self.colors = {}
        self.patterns_available = [('red', 'gray12'), ('green', 'gray12'),
                         ('blue', 'gray12'), ('black', 'gray12'),
                         ('white', 'gray12'), ('yellow', 'gray12'),
                         ('cyan', 'gray12'), ('purple', 'gray12'),
                         ('red', 'gray25'), ('green', 'gray25'),
                         ('blue', 'gray25'), ('black', 'gray25'),
                         ('white', 'gray25'), ('yellow', 'gray25'),
                         ('cyan', 'gray25'), ('purple', 'gray25') ]

    def set_scale (self, x_scale, y_scale):
        self.x_scale = x_scale
        self.y_scale = y_scale

    def set_multiplier (self, x_multiplier, y_multiplier):
        self.x_multiplier = x_multiplier
        self.y_multiplier = y_multiplier

    def set_translate (self, x_translate, y_translate):
        # Shift layout elements
        dx = (x_translate - self.x_translate) / self.x_scale
        dy = (y_translate - self.y_translate) / self.y_scale
        self.canvas.move('layout', dx, dy)
        
        self.x_translate = x_translate
        self.y_translate = y_translate

    def set_offset (self, x_offset, y_offset):
        # Shift layout elements
        dx = (x_offset - self.x_offset) / self.x_scale
        dy = (y_offset - self.y_offset) / self.y_scale
        self.canvas.move('layout', dx, dy)

        self.x_offset = x_offset
        self.y_offset = y_offset

    def set_shift (self, x_shift, y_shift):
        self.x_shift = x_shift
        self.y_shift = y_shift

    def set_direction (self, direction):
        dx, dy = direction
        angle = math.atan2(dy, dx)
        self.angle_cos = math.cos(angle)
        self.angle_sin = math.sin(angle)

    def set_angle (self, angle):
        angle = float(angle) / 180 * math.pi
        self.angle_cos = math.cos(angle)
        self.angle_sin = math.sin(angle)

    def get_layer (self):
        "Return the current layer"
        return self.layer
    
    def set_layer (self, layer):
        "Set the layer being drawn"
        self.layer = layer

    def list_layers (self):
        return self.colors.items()

    def show_layer (self, layer):
        if layer in self.hidden_layers:
            self.hidden_layers.remove(layer)

    def hide_layer (self, layer):
        if layer not in self.hidden_layers:
            self.hidden_layers.append(layer)
            
    def get_color (self):
        "Get colour and fill pattern for the current layer"
        try:
            return self.colors [ self.layer ]
        except KeyError:
            self.colors[ self.layer ] = self.patterns_available.pop()
            return self.colors [ self.layer ]

    def transform_point (self, point):
        x,y = point

        # Multiply the point by the current transformation matrix.  The
        # point is given in homogenous coordinates (x,y,1), so the output
        # needs to be divided in order to obtain the actual point.
        x,y, denom = [x,y,1] * self.transform 
        x = x / denom ; y = y / denom
        
        xp = x * self.angle_cos - y * self.angle_sin
        yp = x * self.angle_sin + y * self.angle_cos

        x, y = xp * self.x_multiplier, yp * self.y_multiplier

        x = x + (self.x_translate + self.x_offset) * 1000
        y = y + (self.y_translate + self.y_offset) * 1000

        # Until this point, x and y have been expressed in CIF units, which are
        # .001um.  Now we divide by 1000 to convert to microns, and then
        # multiply by the scaling factor to convert to pixels.
        x = x / 1000 / self.x_scale ; y = y / 1000 / self.y_scale
        x = x + self.x_shift ; y = y + self.y_shift
        
        return (x, y)

    def clear (self):
        "Delete all the objects rendered previously"
        self.canvas.delete('layout')
        
    def draw_polygon (self, path):
        if self.get_layer() in self.hidden_layers:
            return
        
        color, pattern = self.get_color()
        path = map(lambda pt, self=self:
                      self.transform_point(pt),
                   path)
        args = []
        for p in path:
            args.append(p[0]) ; args.append(p[1])
        args = tuple(args)
        apply(self.canvas.create_polygon, args,
        {'tags': ('layout', 'layout-' + self.layer),
         'outline': color, 'width': 1,
         'fill':color, 'stipple': pattern
        })
        
    def draw_circle (self, center, radius):
        if self.get_layer() in self.hidden_layers:
            return

        xradius = radius * self.x_multiplier / self.x_scale / 1000
        yradius = radius * self.y_multiplier / self.y_scale / 1000
        
        xc, yc = self.transform_point(center)
        color, pattern = self.get_color()
        self.canvas.create_oval(xc-xradius, yc-yradius,
                                 xc+xradius, yc+yradius,
                                 outline = color, width = 1,
#                                 stipple = pattern,
                                 tags= ('layout', 'layout-' + self.layer)
                                )

    def draw_wire (self, path, width):
        if self.get_layer() in self.hidden_layers:
            return

        color, pattern = self.get_color()
        path = map(lambda pt, self=self:
                      self.transform_point(pt),
                   path)
        args = []
        for p in path:
            args.append(p[0]) ; args.append(p[1])
        args = tuple(args)

        width = width / self.x_scale / 1000
        if width < 1: width = 1

        apply(self.canvas.create_line, args,
        {'tags': ('layout', 'layout-' + self.layer),
        'width': width, 'fill':color, 'stipple': pattern})
        


def compile_commands (cmdlist, a,b):
    """Take a list of commands represented as tuples of strings,
    and convert them to object instances, returning the resulting
    list
    """

    L = []
    scale = float(a)/float(b)
    for entry in cmdlist:
        c = entry[0]
        if c == 'D':
            c2 = entry[1]
            if c2 == 'D':
                args = entry[2:] ; klass = Delete
            else:
                raise ParseError, 'Unknown command: ' + repr(entry)
        elif c in string.digits:
            args = entry
            klass = UserExtension
        else:
            args = entry[1:]
            klass = COMMANDS[ c ]

        obj = klass() ; obj.parse(args, scale)
        L.append(obj)

    return L


    
class CIFLayout:
    """Class representing a CIF layout file

    Attributes:
    cmdlist: list of top-level commands in the layout file
    symbols: dictionary containing the symbol definitions from the file.
    It maps integers to (a,b,cmdlist) tuples, where a,b are the scaling factors
    (1,1 if not provided) and cmdlist is the list of commands making
    up the symbol.
    """
    
    def __init__ (self):
        self.symbols = {}
        self.cmdlist = []
        self.layers = []
        self.layer_names = {}
        
    def parse (self, S):
        "Parse the CIF file contained in the string S"
        S = string.upper(S)
        cmdlist = []
        pos = 0
        while pos < len(S):
            # Skip over blanks
            blanks = string.digits + string.letters + '-();'
            while S[pos] not in blanks and pos < len(S):
                pos = pos + 1

            end = string.find(S, ';', pos)
            if end == -1: end = len(S)
            cmd = S[pos: end]
            pos = end + 1

            # End command
            if cmd[0] == 'E':
                break

            cmdlist.append(cmd)


        # Group command definitions into sublists
        last = i = 0
        L2 = []
        while i < len(cmdlist):
            cmd = cmdlist[i]
            if cmd[0:2] == 'DS':
                L2 = L2 + cmdlist[last:i]
                start = i

            elif cmd[0:2] == 'DF':
                end = i
                L2.append(cmdlist[start:end+1])
                i = i + 1
                last = i

            i = i + 1

        L2 = L2 + cmdlist[last:]

        cmdlist = L2

        # Loop over the command list and fill in the attributes
        symbols = filter(lambda x: type(x) == type([]), cmdlist)
        for symboldef in symbols:
            dscmd = symboldef[0]
            dfcmd = symboldef[-1]
            assert re.match ('^DS', dscmd) is not None
            assert re.match('^DF', dfcmd)

            # Get number, and optional scaling factors
            s = dscmd[1:]
            m = ds_args_pat.match(dscmd)
            if not m:
                raise ParseError, "Incorrect DS arguments: " +repr(s)
            number = int(m.group('number'))
            a = b = 1.0
            if m.group('a') is not None: a = int(m.group('a'))
            if m.group('b') is not None: b = int(m.group('b'))
            compiled_list = compile_commands(symboldef[1:-1], a,b)
            self.add_symbol(number, compiled_list, a,b)

        commands = filter(lambda x: type(x) != type([]), cmdlist)
        self.set_cmdlist(compile_commands(commands, 1,1))

        self.parse_ledit_headers()

    def parse_ledit_headers (self):
        for layer in self.layers:
            self.layer_names[ layer ] = layer

        for cmd in self.cmdlist:
            if isinstance(cmd, Comment) and re.match('(?i)L-Edit ', cmd.text):
                # It's a comment containing an L-Edit annotation...

                # Check for layer name
                m = re.match('(?i)L-Edit Layer (.*?) = CIF Layer (.*?)\)',
                             cmd.text)
                if m is not None:
                    self.layer_names[ m.group(2) ] = m.group(1)
                
    def list_layers (self):
        return self.layers

    def get_cmdlist (self):
        return self.cmdlist

    def set_cmdlist (self, cmdlist):
        self.cmdlist = cmdlist
        for cmd in cmdlist:
            if isinstance(cmd, Layer):
                if cmd.name not in self.layers:
                    self.layers.append(cmd.name)
        self.layers.sort()

    def get_symbol (self, symbol_number):
        return self.symbols[ symbol_number ]

    def add_symbol (self, symbol_number, cmdlist, a=1.0, b=1.0):
        self.symbols[ symbol_number ] = (a,b,cmdlist[:])
        for cmd in cmdlist:
            if isinstance(cmd, Layer):
                if cmd.name not in self.layers:
                    self.layers.append(cmd.name)

        self.layers.sort()

    def display (self, context):
        "Add objects representing this layout to a Tkinter context"

        # Allocate colors for all the layers
        for layer in self.layers:
            context.set_layer(layer)
            context.get_color()

        # Set initial transform to the 3x3 identity matrix
        context.transform = Matrix3x3([[1,0,0], [0,1,0], [0,0,1]])
        self._perform_cmdlist(context, self.cmdlist)
        
    def _perform_cmdlist (self, context, cmdlist):
        for cmd in cmdlist:
            if isinstance(cmd, Call):
                # Save current transformation matrix and layer
                old_transform = context.transform
                old_layer = context.layer
                
                # Compute new matrix, and make recursive call to
                # perform subcommands
                context.transform = context.transform * cmd.transform
                a,b, symbol_cmds = self.get_symbol(cmd.number)
                self._perform_cmdlist(context, symbol_cmds)

                # Restore previous transformation matrix and layer
                context.transform = old_transform
                context.layer = old_layer
            else:
                cmd.display(context)
        
            
ds_args_pat = re.compile("""
# Pattern for parsing the arguments to a DS command
[A-Z\s]* (?P<number>\d+)
(?: [A-Z\s]* (?P<a>\d+)
    [A-Z\s]+ (?P<b>\d+)
)?
[A-Z\s]*$
""", re.VERBOSE | re.IGNORECASE)



if __name__ == '__main__':
    S = sys.stdin.read()
    layout = CIFLayout(); layout.parse(S)

    import Tkinter
    root = Tkinter.Tk()
    f = Tkinter.Frame(root)
    f.pack()
    c = Tkinter.Canvas(f)
    c.pack(side = Tkinter.TOP, expand=Tkinter.YES, fill=Tkinter.BOTH)
    context = TkRenderingContext(c)
#    context.set_multiplier(3,3)
#    context.set_direction((-20, 10))
    layout.display(context)
#    c.create_text(50,50, text = 'Hello', anchor=Tkinter.CENTER)
    c.mainloop()
    
