# ImageDisplay.py : Panel that displays the microscope image
#
# Distributed under the terms described in the LICENCE file.
#
# Developed by akuchlin@mems-exchange.org
#
# Contains the last image sent out by the microscope server, and the
# microscope's current state (coordinates, image viewing mode, etc.).

__revision__ = "$Id: ImageDisplay.py,v 1.50 2002/01/15 20:19:38 akuchlin Exp $"

from java import awt
from javax import swing
from java.awt.Toolkit import getDefaultToolkit
from java import lang
import math, time

# Minimum time delay between displaying successive images (in milliseconds)
MIN_IMAGE_DECOMPRESS_DELAY = 350

# Maximum time delay between displaying successive images (in milliseconds)
MAX_IMAGE_DECOMPRESS_DELAY = 750

class ImageDecompressorThread(lang.Thread):
    def __init__ (self, image_disp):
        self.image_disp = image_disp

    def createImage (self, data):
        # JDK 1.1 code
        ##im = toolkit.createImage(self.image_disp.image_data)
        ##return im
        
        # For unknown reasons, the Toolkit.createImage() works in Java 1.1,
        # but not in 1.3, so this alternate approach has to be used instead,
        # 6 lines of code instead of 1.  This is called progress.
        from java.io import ByteArrayInputStream
        from com.sun.image.codec.jpeg import JPEGCodec
        from java.awt.image import ColorModel
        
        ba = ByteArrayInputStream(data) 
        codec = JPEGCodec.createJPEGDecoder(ba)
        bf = codec.decodeAsBufferedImage()
        return bf
        
    def run (self):
	toolkit = getDefaultToolkit()
        last_data = None
        t = time.time()
        while 1:
            t2=time.time()
            
            if self.image_disp.image_data is None:
                lang.Thread.sleep(50)
                continue
            elif self.image_disp.image_data is last_data:
                lang.Thread.sleep(50)
                continue
                
            last_data = self.image_disp.image_data

            start = time.time()
            im = self.createImage(self.image_disp.image_data)
            self.image_disp.scope_image = im
            w,h = im.getWidth(), im.getHeight()
            
            if (w!=self.image_disp.image_width or
                h!=self.image_disp.image_height):
                self.image_disp.resize_image()
            self.image_disp.repaint()
            decomp_delay = (time.time() - start) * 1000 * 1.2

            # Clip delay time to between MIN_ and MAX_ values 
            decomp_delay = min(decomp_delay, MAX_IMAGE_DECOMPRESS_DELAY)
            decomp_delay = max(decomp_delay, MIN_IMAGE_DECOMPRESS_DELAY)
            lang.Thread.sleep(int(decomp_delay))

class ImageDisplay(swing.JPanel,
                   awt.event.ActionListener):
    """Class that displays a microscope image
    Attributes:
    show_scale          Boolean: draw scale bar?
    """

    def __init__ (self, client):
        swing.JPanel.__init__(self)
        self.xpos = self.ypos = self.zpos = 0
	self.x_scale = self.y_scale = 1.0
	self.theta_angle = 0 ; self.mag = 5
        self.mode = "Brightfield"
        self.aperture = 1 ; self.light = 1
        self.z_autofocus = -1
        
        self.show_scale = self.show_crosshair = 0
        self.mouse_mode = 'move'
        self.offimage = None
        self.ox, self.oy = (1,1)

	self.scope_image = None
        self.image_data = None
	self.client = client
        self.mousePressed = self.pressed
        self.mouseReleased = self.released
        self.mouseDragged = self.dragged
        self.isPressed = 0
        self.coords = {}
        
        # Set some sane starting values for the image size; they'll get
        # reset when the first image arrives.
	self.image_width, self.image_height = 320,200

        popup = self.popup = awt.PopupMenu()
        self.add(popup)

        mi = awt.MenuItem("Move the microscope")
        mi.addActionListener(self)
        mi.setActionCommand("move")
        popup.add(mi)

        mi = awt.MenuItem("Measure distances")
        mi.addActionListener(self)
        mi.setActionCommand("measure")
        popup.add(mi)

        mi = awt.MenuItem("Draw rectangles")
        mi.addActionListener(self)
        mi.setActionCommand("rectangle")
        popup.add(mi)

        # Fire up the ImageDecompressor subthread
        st = self.subthread = ImageDecompressorThread(self)
        st.setPriority(lang.Thread.MIN_PRIORITY)
        st.start()


    def actionPerformed (self, e):
        cmd = e.getActionCommand()
        if cmd in ["move", "measure", "rectangle"]:
            self.mouse_mode = cmd
            self.coords = {}
        
    def draw_mouse_annotation (self, gc = None):
        if gc is None: gc = self.getGraphics()
        mode = self.mouse_mode
        if mode == 'rectangle' and self.coords.has_key(1) and self.coords.has_key(2):
            if not self.in_paint: self.repaint()
            # Rectangle mode
            gc.setColor(awt.Color.red)
            rx = min(self.coords[1][0], self.coords[2][0])
            ry = min(self.coords[1][1], self.coords[2][1])
            rw = abs (self.coords[2][0]-self.coords[1][0])
            rh = abs (self.coords[2][1]-self.coords[1][1])
            gc.drawRect(rx, ry, rw, rh)

        elif mode == 'measure' and self.coords.has_key(1) and self.coords.has_key(2):
            # Measurement mode
            if not self.in_paint: self.repaint()
            gc.setColor(awt.Color.red)
            gc.drawLine(self.coords[1][0], self.coords[1][1],
                        self.coords[2][0], self.coords[2][1])
            dx = ((self.coords[1][0] - self.coords[2][0]) *
                  (self.x_scale / self.image_width) )
            dy = ((self.coords[1][1] - self.coords[2][1]) *
                  (self.y_scale / self.image_height) )
            distance = math.sqrt(dx*dx + dy*dy) 
            distance = round(distance,1)

            ypos = self.image_height - 20;
            if self.show_scale: ypos = ypos - 20

            s = str(distance) + ' um'
            fontmet = gc.getFontMetrics()
            width = fontmet.bytesWidth(s, 0, len(s))

            # Draw a white rectangle, with 5 pixels of space around the text,
            # and 2 pixels above and below it.
            gc.setColor(awt.Color.white)
            gc.fillRect(2, 2, width + 10, fontmet.getHeight() + 4)

            gc.setColor(awt.Color.black)
            gc.drawString(s, 5, 4 + fontmet.getAscent())

    def dragged (self, e):
        # Update display to match the current mouse position
        if self.mouse_mode in ['measure', 'rectangle']:
            self.coords[2] = e.getX(), e.getY()
            self.repaint()
#            self.draw_mouse_annotation()

    def pressed (self, e):
        # If the right button has been pressed, display the pop-up menu.
        if e.isPopupTrigger():
            self.popup.show(self, e.getX(), e.getY())
                            
        # First, check that button 1 was pressed.  For some
        # inexplicable reason, the AWT doesn't actually set
        # BUTTON1_MASK.  (Why did they even bother to include the
        # constant, in that case?)  So instead, we simply exit if
        # either of buttons 2 or 3 was pressed.
        
        if ((e.getModifiers() & awt.event.InputEvent.BUTTON2_MASK) or
             (e.getModifiers() & awt.event.InputEvent.BUTTON3_MASK)):
            return
        assert self.isPressed == 0
        x,y = e.getX(), e.getY()
	if self.mouse_mode == 'move' and self.client.hasControl:
	    # Move-to-click mode: if the click is within the image,
            # send a motion command.

	    x,y = x-self.ox, y-self.oy
	    if (0<= x <self.image_width) and (0<= y <self.image_height):
                dx, dy = x-(self.image_width/2), y-(self.image_height/2)
                dx = (self.xpos - dx * self.x_scale / self.image_width )
                dy = (self.ypos - dy * self.y_scale / self.image_height) 
                self.client.move(x = dx, y = dy)

	# Otherwise, save the coordinate for drawing rectangles or
        # lines
        elif self.mouse_mode in ['measure', 'rectangle']:
            self.coords[1] = (x,y) 
            self.isPressed = 1
        e.consume()

    def released (self, e):
        # If the right button has been pressed, display the pop-up menu.
        if e.isPopupTrigger():
            self.popup.show(self, e.getX(), e.getY())

        # First, check that button 1 was released; the same notes
        # apply about BUTTON_MASK1 as before.
        if ((e.getModifiers() & awt.event.InputEvent.BUTTON2_MASK) or
             (e.getModifiers() & awt.event.InputEvent.BUTTON3_MASK)):
            return

	mode = self.mouse_mode
        self.coords[2] = e.getX(), e.getY()
        self.isPressed = 0
        self.repaint()
#        self.draw_mouse_annotation()
	if self.client.hasControl and mode in ['measure', 'rectangle']:
	    cmd = '%s x1=%i y1=%i x2=%i y2=%i\n' % (mode,
					self.coords[1][0], self.coords[1][1],
					self.coords[2][0], self.coords[2][1])
	    self.client.send_message(cmd)

        e.consume()
        
    def getInsets (self):
        return awt.Insets(5,5,5,5)	

    def paint (self, gc):
        start_time = time.time()
        self.in_paint = 1
        # It seems that the paint() method may sometimes be called when
        # the .scope_image attribute hasn't even been created yet.
        # Also, curiously sometimes gc seems to be None.  (Bug somewhere else?)
        if (not hasattr(self, 'scope_image') or
            self.image_width <= 0 or self.image_height <=0):
            return
        if gc is None: return

        # We want to figure out where the image should be clipped, in case
        # the user sizes the window to be really small.
	w,h = self.image_width, self.image_height
        if self.offimage is None:
            self.offimage = self.createImage(w,h)
        gc2 = self.offimage.getGraphics()
        
	if self.scope_image:
	    clipwidth = min(self.scope_image.getWidth(),
                            w - 2)
	    clipheight = min(self.scope_image.getHeight(),
                             h - 2)
	else:
	    clipwidth = self.image_width ; clipheight = self.image_height

	ox, oy = 1,1
	gc2.setColor(awt.Color(255,255,0))
        gc2.drawRect(ox-1,oy-1, clipwidth+1, clipheight+1)
 	gc2.setClip(ox,oy, clipwidth, clipheight)
        
	if self.scope_image:
            gc2.drawImage(self.scope_image, ox,oy, 1+clipwidth, 1+clipheight,
                          0,0, clipwidth, clipheight, self)

        if self.show_scale: self.paint_scale(gc2, clipwidth, clipheight)
        if self.show_crosshair: self.paint_crosshair(gc2, clipwidth, clipheight)
        self.draw_mouse_annotation(gc2)

        # Copy the offscreen image onto the screen
        if self.offimage is not None:
            gc.drawImage(self.offimage, 0, 0, None)
        self.in_paint = 0

    update = paint

    def paint_scale (self, gc, im_width, im_height):
        "Draw a scale bar on the bottom of the image"
        # XXX this has gotten rather convoluted, and needs to be
        # cleaned up and simplified.
        width = min(200, im_width/3)
        distance = width * self.x_scale / 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(distance) /
                                  math.log(10))

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

        gc.setColor(awt.Color(255,255,255))
        gc.fillRect(2, im_height-21, width, 20)

        # Draw the bar, centered within the white area
        bar_size = int(bar_length / (self.x_scale/im_width))
        offset = int(width - bar_size) / 2
        gc.setColor(awt.Color(0,0,0))
        gc.fillRect(2+offset, im_height - 2 - 7, bar_size, 7)

        gc.drawString(str(bar_length) +' um', 2+offset, im_height-2-9)
            
    def paint_crosshair (self, gc, im_width, im_height):
        "Draw a crosshair on the center of the image"
        gc.setColor(awt.Color(0,0,0))
        cx, cy = im_width/2, im_height/2
        gc.drawLine(cx - 15, cy,
                    cx + 15, cy)
        gc.drawLine(cx, cy - 15,
                    cx, cy + 15)
        
    def getPreferredSize (self):
        return awt.Dimension(self.image_width + 2, self.image_height + 2)
    
    getMinimumSize = getMaximumSize = getPreferredSize

    # Commands from the server
    
    def cmd_measure (self, sock, x1, y1, x2, y2):
        # If this client is in control, it doesn't need to take notice of
        # the measurement.
        if self.client.hasControl:
            return

        # If the user is measuring something herself, don't stomp over 
        # her coordinates with what just arrived from the server.
        if (not self.coords.has_key(1) or not self.coords.has_key(2)
            or (x1, y1) != self.coords[1] or (x2,y2) != self.coords[2]):
	    self.mouse_mode= 'measure' 
	    self.coords[1] = x1,y1
	    self.coords[2] = x2,y2
            self.repaint()
#	    self.draw_mouse_annotation()
	    
    def cmd_rectangle (self, sock, x1, y1, x2, y2):
        # If this client is in control, it doesn't need to take notice of
        # the measurement.
        if self.client.hasControl:
            return

        if (not self.coords.has_key(1) or not self.coords.has_key(2)
            or (x1, y1) != self.coords[1] or (x2,y2) != self.coords[2]):
            self.mouse_mode = 'rectangle'
            self.coords[1] = x1,y1
            self.coords[2] = x2,y2
            self.repaint()

    def cmd_scope (self, sock, x=None, y=None, z=None,
                   angle=None, magnification=None,
                   mode=None, aperture=None, light=None,
                   x_scale=None, y_scale=None):
        if x_scale is not None: self.x_scale = float(x_scale)
        if y_scale is not None: self.y_scale = float(y_scale)
        if x is not None: self.xpos = x
        if y is not None: self.ypos = y
        if z is not None: self.zpos = z
        if angle is not None: self.theta_angle = angle
        if magnification is not None: self.mag = magnification
        if mode is not None: self.mode = mode
        if aperture is not None: self.aperture = aperture
        if light is not None: self.light = light

        # Tell the parent Client object to update the settings in the
        # StatusDisplay panel.
	self.client.update_coords()

    def cmd_image (self, sock, args):
        length = int(args[0])
	image = ""
	while len(image) < length:
		image = image + sock.recv(length-len(image))
        self.image_data = image

    def resize_image (self):
        # Resize the image display when the image size changes

        w = self.image_width = self.scope_image.getWidth()
        h = self.image_height = self.scope_image.getHeight()
        
        # To make the ImageDisplay and its accompanying buttons adjust
        # properly to their new size, we'll change their sizes to
        # something much too small.
        d = awt.Dimension(25,25)
        self.client.button_up.setSize(awt.Dimension(w,25))
        self.client.button_down.setSize(awt.Dimension(w,25))
        self.client.button_left.setSize(awt.Dimension(25,h))
        self.client.button_right.setSize(awt.Dimension(25,h))

        p = self.client.im_panel
        p.invalidate()
        self.client.setSize(self.client.getPreferredSize())

        # Delete the offscreen graphic buffer before repainting;
        # update()/paint() will create a new one of the right size
        self.offimage = None
        self.client.repaint()

