"""mems.instrument.client.tk.image_display

Contains the ImageDisplay class, which displays a microscope image
and allows clicking on the image to move the microscope.
"""

# created 2002/01/23, AMK

__revision__ = "$Id: image_display.py,v 1.7 2002/02/25 22:05:35 akuchlin Exp $"

import math, time
import Tkinter, Pmw
from PIL import ImageTk

from mems.instrument import protocols

# Flag to enable dummy layout display code
LAYOUT = 0

class ImageDisplay(Tkinter.Frame):
    # Height of label used for displaying measurements
    LABEL_HEIGHT = 20

    # Height of label for displaying scale
    SCALE_HEIGHT = 25

    # Interval (in seconds) between messages to the server informing it
    # of a new frame rate.
    
    FRAME_UPDATE_SECS = 10
    
    def __init__ (self, client, *args):
        apply(Tkinter.Frame.__init__,(self,) + args)

        self.image = None
        self.mouse_down = 0
        self.client = client

        self._conversion_time = 1.0
        
        self.show_crosshair = 0
        self.show_scale = 0

        # We'll send the first update in 10 seconds.
        self.after(1000*10, self.update_frame_rate)
        
        self.layout = None ; self.layout_context = None
        self.layers_shown = {}
        self.set_layout(None)

        self.codec = None
        self.canvas = Tkinter.Canvas(self, width=320, height=240)
        self.canvas.pack(expand=1, fill=Tkinter.BOTH)
        self.canvas.bind('<ButtonPress-1>',
                          self.mouse_pressed)
        self.canvas.bind('<ButtonRelease-1>',
                          self.mouse_released)
        self.canvas.bind('<ButtonPress-3>',
                          self.popup)
        self.canvas.bind('<Motion>',
                          self.update_mouse_display)

        p = self.popupMenu = Tkinter.Menu(self, tearoff=0)
        p.add_command(label = 'Move', command=self.select_move_mode)
        p.add_command(label = 'Measure', command=self.select_measurement_mode)
        p.add_command(label = 'Rectangle', command=self.select_rectangle_mode)
#        p.add_command(label = 'Rotate', command=self.select_rotate_mode)
#        p.add_command(label = 'Save Image', command=self.save_image_mode)

        # Set timer for updating the image in one second)
        self.after(100, self.update_image)


    def update_frame_rate (self):
        """update_frame_rate()

        Called every FRAME_UPDATE_SECS to tell the server how many
        frames per second this client can handle.  This is determined
        by counting the number of frames processed by the codec
        in the most recent interval, and adding up the processing
        time required to handle those frames.  
        """

        if self.codec is None:
            fps = 1
        else:
            # XXX this calculation doesn't do the Right Thing.
            # Sadly it's not clear what the Right Thing is.
            nf = self.codec.num_frames
            fps = nf / ((self.FRAME_UPDATE_SECS - self._conversion_time*nf -
                         self.codec.processing_time
                         ))
            fps = int(fps)+1

            # Reset processing time and frame count.
            self.codec.processing_time = 0.0
            self.codec.num_frames = 0
        
        # Clip value to between 1 and 30 frames/sec
        if   fps > 30: fps=30
        elif fps < 1 : fps=1

        if self.client is not None:
            self.client.send_message('frames num_frames=%i' % fps)
        self.after(1000*self.FRAME_UPDATE_SECS, self.update_frame_rate)


    def update_image (self):
        # If no codec has been set yet, simply try again in a tenth of
        # a second. 
        if self.codec is None:
            self.after(100, self.update_image)
            return

        start = time.time()
        image = self.codec.get_image()
        
        if (self.image is None or
            image.size != (self.image.width(), self.image.height())):
            # No PhotoImage has been created yet, or the size has changed,
            # so create a new PhotoImage
            self.image = ImageTk.PhotoImage(image)
            w,h = image.size
            self.canvas.delete('image')
            self.canvas.create_image(w/2, h/2,
                                     image=self.image, tags=('image',))
            self.canvas.lower('image')
            self.canvas.configure(width=w, height=h)
        else:
            self.image.paste(image)

        # Measure time it took to update the image, and schedule an update
        # in that amount of time.
        self._conversion_time = (time.time() - start) 
        self.after(int(self._conversion_time*1000), self.update_image)

        
    def _clear_mouse_data (self):
        "Remove data related to mouse position, used when switching modes"
        self.canvas.delete('mouse')
        self.canvas.delete('label')
        self.client.state.x1 = self.client.state.x2 = None

    def set_layout (self, layout):
        if not LAYOUT:
            return
        
        if self.layout_context is not None:
            self.layout_context.clear()

        self.layout = layout
        if self.layout is None:
            self.layout_context = None
        else:
            self.layers_shown.clear()
            for layer in self.layout.list_layers():
                v = self.layers_shown[ layer ] = Tkinter.BooleanVar()
                v.set(1)

            self.layout_context = CIF.TkRenderingContext(self.canvas)
            self.change_layers()
            
        self.client.make_layer_menu(self.layout, self.layout_context,
                                     self.layers_shown)


    def change_layers (self):
        "Change which layers are displayed"
        # XXX this requires checking all the layers
        if self.layout_context:
            for layer in self.layout.list_layers():
                var = self.layers_shown[ layer ]
                if var.get():
                    self.layout_context.show_layer(layer)
                else:
                    self.layout_context.hide_layer(layer)

            self.layout_context.clear()
            self.redisplay_layout()
            
    def redisplay_layout (self):
        self.canvas.delete('layout')
        if self.layout is not None:
            self.layout.display(self.layout_context)
        
    def select_move_mode (self):
        self.client.state.draw_annotation = protocols.ANNOTATE_NONE
        self._clear_mouse_data()
    def select_measurement_mode (self):
        self.client.state.draw_annotation = protocols.ANNOTATE_MEASUREMENT
        self._clear_mouse_data()
    def select_rectangle_mode (self):
        self.client.state.draw_annotation = protocols.ANNOTATE_RECTANGLE
        self._clear_mouse_data()
    def select_rotate_mode (self):
        self.client.state.draw_annotation = protocols.ANNOTATE_ROTATION
        self._clear_mouse_data()

    def popup (self, event):
        self.popupMenu.post(event.x_root, event.y_root)


    def mouse_released (self, event):
        self.mouse_down = 0
        
    def mouse_pressed (self, event):
        x, y = event.x, event.y
        state = self.client.state

        if state.draw_annotation == protocols.ANNOTATE_NONE:
            # Determine center of image
            w, h = int(self.canvas['width']), int(self.canvas['height'])
            xc, yc = w/2, h/2
            
            # Compute size of movement in pixels
            dx, dy = x - xc, y - yc

            ##print 'mouse movement in pixels:', dx, dy
            # Convert from pixels to microns
            dx = float(dx) / w * state.x_scale 
            dy = float(dy) / h * state.y_scale 

            ##print 'mouse movement in microns:', dx, dy
            # Perform the movement!
            self.client.move(x = (state.x - dx),
                              y = (state.y - dy))
##            print 'final x,y coordinate:', self.client.state.x - dx,
##            print self.client.state.y - dy
        else:
            # Some other movement mode, so we need to save the
            # location of the mouse press.
            self.mouse_down = 1
            if state.x1 is None:
                state.x1, state.y1 = (x,y)
            elif state.x2 is None: 
                state.x2, state.y2 = (x,y)
            else:
                # Both points filled, so we start afresh
                state.x1, state.y1 = (x,y)
                state.x2, state.y2 = (None, None)
                
    def update (self):
        self.paint_scale()
        self.paint_annotation()

    def render_string (self, S, labelwidth = None):
        c = self.canvas
        width, height = int(c['width']), int(c['height'])

        if labelwidth is None:
            # XXX compute depending on size of string
            labelwidth = width/3

        self.canvas.delete('label')
        self.canvas.create_rectangle(2, 2,
                                     2 + labelwidth, 2 + self.LABEL_HEIGHT,
                                     fill="white", tags=('label'))
        self.canvas.create_text(2+ labelwidth/2, 2 + self.LABEL_HEIGHT/2,
                                 text = S,
				 tags = ('label',))

    def paint_annotation (self):
        state = self.client.state
        w, h = int(self.canvas['width']), int(self.canvas['height'])
        
        if (state.draw_annotation == protocols.ANNOTATE_MEASUREMENT and
            state.x1 is not None):
            x1, y1 = state.x1, state.y1
            if state.x2 is not None: x2, y2 = state.x2, state.y2
            elif self.current_pos is not None: x2, y2 = self.current_pos
            else: return
            self.canvas.delete('mouse')
            self.canvas.create_line(x1, y1, x2, y2,
                                     fill = "yellow", width=1,
                                     tags = ("mouse",)
                                    )
            dx = (x2 - x1) * self.client.state.x_scale / w
            dy = (y2 - y1) * self.client.state.y_scale / h
            distance = math.sqrt(dx*dx + dy*dy)
            distance = str(round(distance)) + ' um'
            self.render_string(distance)

        elif (state.draw_annotation == protocols.ANNOTATE_RECTANGLE and
              state.x1 is not None):
            x1, y1 = state.x1, state.y1
            if state.x2 is not None: x2, y2 = state.x2, state.y2
            elif self.current_pos is not None: x2, y2 = self.current_pos
            else: return

            self.canvas.delete('mouse')
            self.canvas.create_rectangle(x1, y1, x2, y2,
                                     outline = "yellow", width=2,
                                     tags = ("mouse",)
                                    )
            dx = (x2 - x1) * self.client.state.x_scale / w
            dy = (y2 - y1) * self.client.state.y_scale / h
            distance = "%.0f x %.0f um" % (abs(dx), abs(dy))
            self.render_string(distance)

        
    def update_mouse_display (self, event):
        "Paint any displays tied to the mouse"
        if not self.mouse_down:
            return
        
        state = self.client.state
        x2, y2 = event.x, event.y
        state.x2, state.y2 = x2, y2
        
        if (state.draw_annotation == protocols.ANNOTATE_MEASUREMENT and
            state.x1 is not None):
            x1, y1 = state.x1, state.y1
            if state.in_control:
                self.client.send_message('measure x1=%i y1=%i x2=%i y2=%i'
                                         % (x1,y1, x2,y2))

        elif (state.draw_annotation == protocols.ANNOTATE_RECTANGLE and
              state.x1 is not None):
            x1, y1 = state.x1, state.y1
            if state.in_control:
                self.client.send_message('rectangle x1=%i y1=%i x2=%i y2=%i'
                                         % (x1,y1, x2,y2))

        self.paint_annotation()

    def paint_crosshair (self):
        self.canvas.delete('crosshair')
        if not self.show_crosshair:
            return

        # Update crosshair
        CROSSHAIR_SIZE = 15
        width, height = int(self.canvas['width']), int(self.canvas['height'])
        cx, cy = width/2, height/2

        self.canvas.create_line(cx - CROSSHAIR_SIZE, cy,
                                 cx + CROSSHAIR_SIZE, cy,
                                 fill = "red", width=2,
                                 tags = ("crosshair",)
                                )
        self.canvas.create_line(cx, cy - CROSSHAIR_SIZE, 
                                 cx, cy + CROSSHAIR_SIZE,
                                 fill = "red", width=2,
                                 tags = ("crosshair",)
                                )

    def paint_scale (self):
        # XXX we should only draw the scale when the image size changes,
        # or when the x_scale, y_scale values have changed

        self.canvas.delete('scale')
        if not self.show_scale:
            return

        im_width, im_height = int(self.canvas['width']), int(self.canvas['height'])

        # Update scale rectangle.
        # First we draw a white rectangle whose size does not depend on
        # the microscope settings at all, just on the size of the image
        # being displayed.

        width = min(200, im_width/3)
        self.canvas.create_rectangle(2, im_height - self.SCALE_HEIGHT - 2,
                                     2 + width, im_height - 2,
                                     fill="white",
                                     tags=('scale'))

        # Compute length of the white rectangle in microns
        x_scale = self.client.state.x_scale
        max_length = x_scale * (float(width)/im_width)
        
        # Figure out the size of the scale bar to draw.  Start by
        # determining the largest power of 10 that fits into the
        # available space.
        bar_length = 10 ** math.floor(math.log(max_length) /
                                       math.log(10))

        # Now check if some interesting multiples (2.5 and 5) will 
        # fit into the available space.
        if bar_length * 5.0 < max_length:
            bar_length = bar_length * 5.0
        elif bar_length * 2.5 < max_length:
            bar_length = bar_length * 2.5

        # OK; bar_length is now the length in microns of the scale to draw.
        # Draw the bar, centered within the white area
        bar_length_pixels = int(im_width * (bar_length/x_scale))
        offset = int(width - bar_length_pixels) / 2
        sx, sy = 2 + offset, im_height - 2 - 7
        self.canvas.create_rectangle(sx, sy,
                                     sx + bar_length_pixels, im_height - 4,
                                     fill="black",
                                     tags=('scale'))
        self.canvas.create_text(2+ width/2, sy - 8,
                                 text = str(bar_length) +' um',
				 tags = ('scale',))
            
    def toggle_scale (self):
        self.show_scale = not self.show_scale
        self.paint_scale()
        
    def toggle_crosshair (self):
        self.show_crosshair = not self.show_crosshair
        self.paint_crosshair()


