JAL-4045 - don't apply distribution dependent scale factor to the axis lines!
[jalview.git] / src / jalview / gui / RotatableCanvas.java
index dc33b36..ef0b2aa 100755 (executable)
  */
 package jalview.gui;
 
-import jalview.api.RotatableCanvasI;
-import jalview.bin.Cache;
-import jalview.datamodel.SequenceGroup;
-import jalview.datamodel.SequenceI;
-import jalview.datamodel.SequencePoint;
-import jalview.math.RotatableMatrix;
-import jalview.util.MessageManager;
-import jalview.viewmodel.AlignmentViewport;
-
 import java.awt.Color;
 import java.awt.Dimension;
 import java.awt.Font;
@@ -44,328 +35,203 @@ import java.awt.event.MouseListener;
 import java.awt.event.MouseMotionListener;
 import java.awt.event.MouseWheelEvent;
 import java.awt.event.MouseWheelListener;
-import java.util.Vector;
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.List;
 
 import javax.swing.JPanel;
 import javax.swing.ToolTipManager;
 
+import jalview.api.RotatableCanvasI;
+import jalview.datamodel.Point;
+import jalview.datamodel.SequenceGroup;
+import jalview.datamodel.SequenceI;
+import jalview.datamodel.SequencePoint;
+import jalview.math.RotatableMatrix;
+import jalview.math.RotatableMatrix.Axis;
+import jalview.util.ColorUtils;
+import jalview.util.MessageManager;
+import jalview.viewmodel.AlignmentViewport;
+
 /**
- * DOCUMENT ME!
- * 
- * @author $author$
- * @version $Revision$
+ * Models a Panel on which a set of points, and optionally x/y/z axes, can be
+ * drawn, and rotated or zoomed with the mouse
  */
-public class RotatableCanvas extends JPanel implements MouseListener,
-        MouseMotionListener, KeyListener, RotatableCanvasI
+public class RotatableCanvas extends JPanel
+        implements MouseListener, MouseMotionListener, KeyListener,
+        RotatableCanvasI, MouseWheelListener
 {
-  RotatableMatrix idmat = new RotatableMatrix(3, 3);
+  private static final float ZOOM_OUT = 0.9f;
 
-  RotatableMatrix objmat = new RotatableMatrix(3, 3);
+  private static final float ZOOM_IN = 1.1f;
 
-  RotatableMatrix rotmat = new RotatableMatrix(3, 3);
+  /*
+   * pixels distance within which tooltip shows sequence name
+   */
+  private static final int NEARBY = 3;
 
-  // RubberbandRectangle rubberband;
-  boolean drawAxes = true;
+  private static final List<String> AXES = Arrays.asList("x", "y", "z");
 
-  int omx = 0;
+  private static final Color AXIS_COLOUR = Color.yellow;
 
-  int mx = 0;
+  private static final int DIMS = 3;
 
-  int omy = 0;
+  boolean drawAxes = true;
 
-  int my = 0;
+  int mouseX;
+
+  int mouseY;
 
   Image img;
 
   Graphics ig;
 
-  Dimension prefsize;
-
-  float[] centre = new float[3];
-
-  float[] width = new float[3];
+  Dimension prefSize;
 
-  float[] max = new float[3];
-
-  float[] min = new float[3];
+  /*
+   * the min-max [x, y, z] values of sequence points when the points
+   * were set on the object, or when the view is reset; 
+   * x and y ranges are not recomputed as points are rotated, as this
+   * would make scaling (zoom) unstable, but z ranges are (for correct
+   * graduated colour brightness based on z-coordinate)
+   */
+  float[] seqMin;
 
-  float maxwidth;
+  float[] seqMax;
 
-  float scale;
+  /*
+   * a scale factor used in drawing; when equal to 1, the points span
+   * half the available width or height (whichever is less); increase this
+   * factor to zoom in, decrease it to zoom out
+   */
+  private float scaleFactor;
 
   int npoint;
 
-  Vector<SequencePoint> points;
-
-  float[][] orig;
-
-  float[][] axes;
-
-  int startx;
-
-  int starty;
-
-  int lastx;
-
-  int lasty;
-
-  int rectx1;
-
-  int recty1;
-
-  int rectx2;
+  /*
+   * sequences and their (x, y, z) PCA dimension values
+   */
+  List<SequencePoint> sequencePoints;
 
-  int recty2;
+  /*
+   * x, y, z axis end points (PCA dimension values)
+   */
+  private Point[] axisEndPoints;
 
-  float scalefactor = 1;
+  // fields for 'select rectangle' (JAL-1124)
+  // int rectx1;
+  // int recty1;
+  // int rectx2;
+  // int recty2;
 
   AlignmentViewport av;
 
   AlignmentPanel ap;
 
-  boolean showLabels = false;
+  private boolean showLabels;
 
-  Color bgColour = Color.black;
+  private Color bgColour;
 
-  boolean applyToAllViews = false;
+  private boolean applyToAllViews;
 
-  boolean first = true;
-
-  public RotatableCanvas(AlignmentPanel ap)
+  /**
+   * Constructor
+   * 
+   * @param panel
+   */
+  public RotatableCanvas(AlignmentPanel panel)
   {
-    this.av = ap.av;
-    this.ap = ap;
-
-    addMouseWheelListener(new MouseWheelListener()
-    {
-      @Override
-      public void mouseWheelMoved(MouseWheelEvent e)
-      {
-        double wheelRotation = e.getPreciseWheelRotation();
-        if (wheelRotation > 0)
-        {
-          /*
-           * zoom in
-           */
-          scale = (float) (scale * 1.1);
-          repaint();
-        }
-        else if (wheelRotation < 0)
-        {
-          /*
-           * zoom out
-           */
-          scale = (float) (scale * 0.9);
-          repaint();
-        }
-      }
-    });
-
+    this.av = panel.av;
+    this.ap = panel;
+    setAxisEndPoints(new Point[DIMS]);
+    setShowLabels(false);
+    setApplyToAllViews(false);
+    setBgColour(Color.BLACK);
+    resetAxes();
+
+    ToolTipManager.sharedInstance().registerComponent(this);
+
+    addMouseListener(this);
+    addMouseMotionListener(this);
+    addMouseWheelListener(this);
   }
 
-  public void showLabels(boolean b)
+  /**
+   * Refreshes the display with labels shown (or not)
+   * 
+   * @param show
+   */
+  public void showLabels(boolean show)
   {
-    showLabels = b;
+    setShowLabels(show);
     repaint();
   }
 
   @Override
-  public void setPoints(Vector<SequencePoint> points, int npoint)
+  public void setPoints(List<SequencePoint> points, int np)
   {
-    this.points = points;
-    this.npoint = npoint;
-    if (first)
-    {
-      ToolTipManager.sharedInstance().registerComponent(this);
-      ToolTipManager.sharedInstance().setInitialDelay(0);
-      ToolTipManager.sharedInstance().setDismissDelay(10000);
-    }
-    prefsize = getPreferredSize();
-    orig = new float[npoint][3];
+    this.sequencePoints = points;
+    this.npoint = np;
+    prefSize = getPreferredSize();
 
-    for (int i = 0; i < npoint; i++)
-    {
-      SequencePoint sp = points.elementAt(i);
+    findWidths();
 
-      for (int j = 0; j < 3; j++)
-      {
-        orig[i][j] = sp.coord[j];
-      }
-    }
-
-    // Initialize the matrices to identity
-    for (int i = 0; i < 3; i++)
-    {
-      for (int j = 0; j < 3; j++)
-      {
-        if (i != j)
-        {
-          idmat.addElement(i, j, 0);
-          objmat.addElement(i, j, 0);
-          rotmat.addElement(i, j, 0);
-        }
-        else
-        {
-          idmat.addElement(i, j, 0);
-          objmat.addElement(i, j, 0);
-          rotmat.addElement(i, j, 0);
-        }
-      }
-    }
-
-    axes = new float[3][3];
-    initAxes();
-
-    findCentre();
-    findWidth();
-
-    scale = findScale();
-    if (first)
-    {
-
-      addMouseListener(this);
-
-      addMouseMotionListener(this);
-    }
-    first = false;
-  }
-
-  public void initAxes()
-  {
-    for (int i = 0; i < 3; i++)
-    {
-      for (int j = 0; j < 3; j++)
-      {
-        if (i != j)
-        {
-          axes[i][j] = 0;
-        }
-        else
-        {
-          axes[i][j] = 1;
-        }
-      }
-    }
+    setScaleFactor(1f);
   }
 
   /**
-   * DOCUMENT ME!
+   * Resets axes to the initial state: x-axis to the right, y-axis up, z-axis to
+   * back (so obscured in a 2-D display)
    */
-  public void findWidth()
+  protected void resetAxes()
   {
-    max = new float[3];
-    min = new float[3];
-
-    max[0] = (float) -1e30;
-    max[1] = (float) -1e30;
-    max[2] = (float) -1e30;
-
-    min[0] = (float) 1e30;
-    min[1] = (float) 1e30;
-    min[2] = (float) 1e30;
-
-    for (int i = 0; i < 3; i++)
-    {
-      for (int j = 0; j < npoint; j++)
-      {
-        SequencePoint sp = points.elementAt(j);
-
-        if (sp.coord[i] >= max[i])
-        {
-          max[i] = sp.coord[i];
-        }
-
-        if (sp.coord[i] <= min[i])
-        {
-          min[i] = sp.coord[i];
-        }
-      }
-    }
-
-    // System.out.println("xmax " + max[0] + " min " + min[0]);
-    // System.out.println("ymax " + max[1] + " min " + min[1]);
-    // System.out.println("zmax " + max[2] + " min " + min[2]);
-    width[0] = Math.abs(max[0] - min[0]);
-    width[1] = Math.abs(max[1] - min[1]);
-    width[2] = Math.abs(max[2] - min[2]);
-
-    maxwidth = width[0];
-
-    if (width[1] > width[0])
-    {
-      maxwidth = width[1];
-    }
-
-    if (width[2] > width[1])
-    {
-      maxwidth = width[2];
-    }
-
-    // System.out.println("Maxwidth = " + maxwidth);
+    getAxisEndPoints()[0] = new Point(1f, 0f, 0f);
+    getAxisEndPoints()[1] = new Point(0f, 1f, 0f);
+    getAxisEndPoints()[2] = new Point(0f, 0f, 1f);
   }
 
   /**
-   * DOCUMENT ME!
-   * 
-   * @return DOCUMENT ME!
+   * Computes and saves the min-max ranges of x/y/z positions of the sequence
+   * points
    */
-  public float findScale()
+  protected void findWidths()
   {
-    int dim;
-    int w;
-    int height;
+    float[] max = new float[DIMS];
+    float[] min = new float[DIMS];
 
-    if (getWidth() != 0)
-    {
-      w = getWidth();
-      height = getHeight();
-    }
-    else
-    {
-      w = prefsize.width;
-      height = prefsize.height;
-    }
+    max[0] = -Float.MAX_VALUE;
+    max[1] = -Float.MAX_VALUE;
+    max[2] = -Float.MAX_VALUE;
 
-    if (w < height)
-    {
-      dim = w;
-    }
-    else
+    min[0] = Float.MAX_VALUE;
+    min[1] = Float.MAX_VALUE;
+    min[2] = Float.MAX_VALUE;
+
+    for (SequencePoint sp : sequencePoints)
     {
-      dim = height;
+      max[0] = Math.max(max[0], sp.coord.x);
+      max[1] = Math.max(max[1], sp.coord.y);
+      max[2] = Math.max(max[2], sp.coord.z);
+      min[0] = Math.min(min[0], sp.coord.x);
+      min[1] = Math.min(min[1], sp.coord.y);
+      min[2] = Math.min(min[2], sp.coord.z);
     }
 
-    return (dim * scalefactor) / (2 * maxwidth);
+    seqMin = min;
+    seqMax = max;
   }
 
   /**
-   * DOCUMENT ME!
-   */
-  public void findCentre()
-  {
-    // Find centre coordinate
-    findWidth();
-
-    centre[0] = (max[0] + min[0]) / 2;
-    centre[1] = (max[1] + min[1]) / 2;
-    centre[2] = (max[2] + min[2]) / 2;
-
-    // System.out.println("Centre x " + centre[0]);
-    // System.out.println("Centre y " + centre[1]);
-    // System.out.println("Centre z " + centre[2]);
-  }
-
-  /**
-   * DOCUMENT ME!
+   * Answers the preferred size if it has been set, else 400 x 400
    * 
-   * @return DOCUMENT ME!
+   * @return
    */
   @Override
   public Dimension getPreferredSize()
   {
-    if (prefsize != null)
+    if (prefSize != null)
     {
-      return prefsize;
+      return prefSize;
     }
     else
     {
@@ -374,9 +240,10 @@ public class RotatableCanvas extends JPanel implements MouseListener,
   }
 
   /**
-   * DOCUMENT ME!
+   * Answers the preferred size
    * 
-   * @return DOCUMENT ME!
+   * @return
+   * @see RotatableCanvas#getPreferredSize()
    */
   @Override
   public Dimension getMinimumSize()
@@ -385,10 +252,9 @@ public class RotatableCanvas extends JPanel implements MouseListener,
   }
 
   /**
-   * DOCUMENT ME!
+   * Repaints the panel
    * 
    * @param g
-   *          DOCUMENT ME!
    */
   @Override
   public void paintComponent(Graphics g1)
@@ -398,7 +264,7 @@ public class RotatableCanvas extends JPanel implements MouseListener,
 
     g.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
             RenderingHints.VALUE_ANTIALIAS_ON);
-    if (points == null)
+    if (sequencePoints == null)
     {
       g.setFont(new Font("Verdana", Font.PLAIN, 18));
       g.drawString(
@@ -407,21 +273,21 @@ public class RotatableCanvas extends JPanel implements MouseListener,
     }
     else
     {
-      // Only create the image at the beginning -
-      if ((img == null) || (prefsize.width != getWidth())
-              || (prefsize.height != getHeight()))
+      /*
+       * create the image at the beginning or after a resize
+       */
+      boolean resized = prefSize.width != getWidth()
+              || prefSize.height != getHeight();
+      if (img == null || resized)
       {
-        prefsize.width = getWidth();
-        prefsize.height = getHeight();
+        prefSize.width = getWidth();
+        prefSize.height = getHeight();
 
-        scale = findScale();
-
-        // System.out.println("New scale = " + scale);
         img = createImage(getWidth(), getHeight());
         ig = img.getGraphics();
       }
 
-      drawBackground(ig, bgColour);
+      drawBackground(ig);
       drawScene(ig);
 
       if (drawAxes)
@@ -434,95 +300,110 @@ public class RotatableCanvas extends JPanel implements MouseListener,
   }
 
   /**
-   * DOCUMENT ME!
+   * Resets the rotation and choice of axes to the initial state (without change
+   * of scale factor)
+   */
+  public void resetView()
+  {
+    img = null;
+    findWidths();
+    resetAxes();
+    repaint();
+  }
+
+  /**
+   * Draws lines for the x, y, z axes
    * 
    * @param g
-   *          DOCUMENT ME!
    */
   public void drawAxes(Graphics g)
   {
+    g.setColor(AXIS_COLOUR);
 
-    g.setColor(Color.yellow);
+    int midX = getWidth() / 2;
+    int midY = getHeight() / 2;
+    // float maxWidth = Math.max(Math.abs(seqMax[0] - seqMin[0]),
+    // Math.abs(seqMax[1] - seqMin[1]));
+    int pix = Math.min(getWidth(), getHeight());
+    float scaleBy = pix * getScaleFactor() / (2f);
 
-    for (int i = 0; i < 3; i++)
+    for (int i = 0; i < DIMS; i++)
     {
-      g.drawLine(getWidth() / 2, getHeight() / 2,
-              (int) ((axes[i][0] * scale * max[0]) + (getWidth() / 2)),
-              (int) ((axes[i][1] * scale * max[1]) + (getHeight() / 2)));
+      g.drawLine(midX, midY,
+              midX + (int) (getAxisEndPoints()[i].x * scaleBy * 0.25),
+              midY + (int) (getAxisEndPoints()[i].y * scaleBy * 0.25));
     }
   }
 
   /**
-   * DOCUMENT ME!
+   * Fills the background with the currently configured background colour
    * 
    * @param g
-   *          DOCUMENT ME!
-   * @param col
-   *          DOCUMENT ME!
    */
-  public void drawBackground(Graphics g, Color col)
+  public void drawBackground(Graphics g)
   {
-    g.setColor(col);
-    g.fillRect(0, 0, prefsize.width, prefsize.height);
+    g.setColor(getBgColour());
+    g.fillRect(0, 0, prefSize.width, prefSize.height);
   }
 
   /**
-   * DOCUMENT ME!
+   * Draws points (6x6 squares) for the sequences of the PCA, and labels
+   * (sequence names) if configured to do so. The sequence points colours are
+   * taken from the sequence ids in the alignment (converting black to white).
+   * Sequences 'at the back' (z-coordinate is negative) are shaded slightly
+   * darker to help give a 3-D sensation.
    * 
    * @param g
-   *          DOCUMENT ME!
    */
   public void drawScene(Graphics g1)
   {
-
     Graphics2D g = (Graphics2D) g1;
 
     g.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
             RenderingHints.VALUE_ANTIALIAS_ON);
+    int pix = Math.min(getWidth(), getHeight());
+    float xWidth = Math.abs(seqMax[0] - seqMin[0]);
+    float yWidth = Math.abs(seqMax[1] - seqMin[1]);
+    float maxWidth = Math.max(xWidth, yWidth);
+    float scaleBy = pix * getScaleFactor() / (2f * maxWidth);
 
-    int halfwidth = getWidth() / 2;
-    int halfheight = getHeight() / 2;
+    float[] centre = getCentre();
 
     for (int i = 0; i < npoint; i++)
     {
-      SequencePoint sp = points.elementAt(i);
-      int x = (int) ((sp.coord[0] - centre[0]) * scale) + halfwidth;
-      int y = (int) ((sp.coord[1] - centre[1]) * scale)
-              + halfheight;
-      float z = sp.coord[1] - centre[2];
-
-      SequenceI sequence = sp.getSequence();
-      if (av.getSequenceColour(sequence) == Color.black)
-      {
-        g.setColor(Color.white);
-      }
-      else
-      {
-        g.setColor(av.getSequenceColour(sequence));
-      }
-
-      if (av.getSelectionGroup() != null)
-      {
-        if (av.getSelectionGroup().getSequences(null)
-                .contains(sequence))
-        {
-          g.setColor(Color.gray);
-        }
-      }
+      /*
+       * sequence point colour as sequence id, but
+       * gray if sequence is currently selected
+       */
+      SequencePoint sp = sequencePoints.get(i);
+      Color sequenceColour = getSequencePointColour(sp);
+      g.setColor(sequenceColour);
+
+      int halfwidth = getWidth() / 2;
+      int halfheight = getHeight() / 2;
+      int x = (int) ((sp.coord.x - centre[0]) * scaleBy) + halfwidth;
+      int y = (int) ((sp.coord.y - centre[1]) * scaleBy) + halfheight;
+      g.fillRect(x - 3, y - 3, 6, 6);
 
-      if (z < 0)
+      if (isShowLabels())
       {
-        g.setColor(g.getColor().darker());
+        g.setColor(Color.red);
+        g.drawString(sp.getSequence().getName(), x - 3, y - 4);
       }
-
-      g.fillRect(x - 3, y - 3, 6, 6);
-      if (showLabels)
+    }
+    if (isShowLabels())
+    {
+      g.setColor(AXIS_COLOUR);
+      int midX = getWidth() / 2;
+      int midY = getHeight() / 2;
+      Iterator<String> axes = AXES.iterator();
+      for (Point p : getAxisEndPoints())
       {
-        g.setColor(Color.red);
-        g.drawString(sequence.getName(), x - 3, y - 4);
+        int x = midX + (int) (p.x * scaleBy * seqMax[0]);
+        int y = midY + (int) (p.y * scaleBy * seqMax[1]);
+        g.drawString(axes.next(), x - 3, y - 4);
       }
     }
-
     // //Now the rectangle
     // if (rectx2 != -1 && recty2 != -1) {
     // g.setColor(Color.white);
@@ -532,81 +413,123 @@ public class RotatableCanvas extends JPanel implements MouseListener,
   }
 
   /**
-   * DOCUMENT ME!
+   * Determines the colour to use when drawing a sequence point. The colour is
+   * taken from the sequence id, with black converted to white, and then
+   * graduated from darker (at the back) to brighter (at the front) based on the
+   * z-axis coordinate of the point.
    * 
-   * @return DOCUMENT ME!
+   * @param sp
+   * @return
    */
-  public Dimension minimumsize()
+  protected Color getSequencePointColour(SequencePoint sp)
   {
-    return prefsize;
-  }
+    SequenceI sequence = sp.getSequence();
+    Color sequenceColour = av.getSequenceColour(sequence);
+    if (sequenceColour == Color.black)
+    {
+      sequenceColour = Color.white;
+    }
+    if (av.getSelectionGroup() != null)
+    {
+      if (av.getSelectionGroup().getSequences(null).contains(sequence))
+      {
+        sequenceColour = Color.gray;
+      }
+    }
 
-  /**
-   * DOCUMENT ME!
-   * 
-   * @return DOCUMENT ME!
-   */
-  public Dimension preferredsize()
-  {
-    return prefsize;
+    /*
+     * graduate brighter for point in front of centre, darker if behind centre
+     */
+    float zCentre = (seqMin[2] + seqMax[2]) / 2f;
+    if (sp.coord.z > zCentre)
+    {
+      sequenceColour = ColorUtils.getGraduatedColour(sp.coord.z, 0,
+              sequenceColour, seqMax[2], sequenceColour.brighter());
+    }
+    else if (sp.coord.z < zCentre)
+    {
+      sequenceColour = ColorUtils.getGraduatedColour(sp.coord.z, seqMin[2],
+              sequenceColour.darker(), 0, sequenceColour);
+    }
+
+    return sequenceColour;
   }
 
-  /**
-   * DOCUMENT ME!
-   * 
-   * @param evt
-   *          DOCUMENT ME!
-   */
   @Override
   public void keyTyped(KeyEvent evt)
   {
   }
 
-  /**
-   * DOCUMENT ME!
-   * 
-   * @param evt
-   *          DOCUMENT ME!
-   */
   @Override
   public void keyReleased(KeyEvent evt)
   {
   }
 
   /**
-   * DOCUMENT ME!
+   * Responds to up or down arrow key by zooming in or out, respectively
    * 
    * @param evt
-   *          DOCUMENT ME!
    */
   @Override
   public void keyPressed(KeyEvent evt)
   {
-    if (evt.getKeyCode() == KeyEvent.VK_UP)
+    int keyCode = evt.getKeyCode();
+    boolean shiftDown = evt.isShiftDown();
+
+    if (keyCode == KeyEvent.VK_UP)
+    {
+      if (shiftDown)
+      {
+        rotate(0f, -1f);
+      }
+      else
+      {
+        zoom(ZOOM_IN);
+      }
+    }
+    else if (keyCode == KeyEvent.VK_DOWN)
+    {
+      if (shiftDown)
+      {
+        rotate(0f, 1f);
+      }
+      else
+      {
+        zoom(ZOOM_OUT);
+      }
+    }
+    else if (shiftDown && keyCode == KeyEvent.VK_LEFT)
     {
-      scalefactor = (float) (scalefactor * 1.1);
-      scale = findScale();
+      rotate(1f, 0f);
     }
-    else if (evt.getKeyCode() == KeyEvent.VK_DOWN)
+    else if (shiftDown && keyCode == KeyEvent.VK_RIGHT)
     {
-      scalefactor = (float) (scalefactor * 0.9);
-      scale = findScale();
+      rotate(-1f, 0f);
     }
     else if (evt.getKeyChar() == 's')
     {
-      Cache.log.warn("DEBUG: Rectangle selection");
+      // Cache.warn("DEBUG: Rectangle selection");
       // todo not yet enabled as rectx2, recty2 are always -1
-      // need to set them in mouseDragged
-      if ((rectx2 != -1) && (recty2 != -1))
-      {
-        rectSelect(rectx1, recty1, rectx2, recty2);
-      }
+      // need to set them in mouseDragged; JAL-1124
+      // if ((rectx2 != -1) && (recty2 != -1))
+      // {
+      // rectSelect(rectx1, recty1, rectx2, recty2);
+      // }
     }
 
     repaint();
   }
 
   @Override
+  public void zoom(float factor)
+  {
+    if (factor > 0f)
+    {
+      setScaleFactor(getScaleFactor() * factor);
+    }
+  }
+
+  @Override
   public void mouseClicked(MouseEvent evt)
   {
   }
@@ -638,20 +561,13 @@ public class RotatableCanvas extends JPanel implements MouseListener,
     int x = evt.getX();
     int y = evt.getY();
 
-    mx = x;
-    my = y;
-
-    omx = mx;
-    omy = my;
-
-    startx = x;
-    starty = y;
+    mouseX = x;
+    mouseY = y;
 
-    rectx1 = x;
-    recty1 = y;
-
-    rectx2 = -1;
-    recty2 = -1;
+    // rectx1 = x;
+    // recty1 = y;
+    // rectx2 = -1;
+    // recty2 = -1;
 
     SequenceI found = findSequenceAtPoint(x, y);
 
@@ -694,56 +610,125 @@ public class RotatableCanvas extends JPanel implements MouseListener,
   }
 
   /**
-   * DOCUMENT ME!
+   * Action handler for a mouse drag. Rotates the display around the X axis (for
+   * up/down mouse movement) and/or the Y axis (for left/right mouse movement).
    * 
    * @param evt
-   *          DOCUMENT ME!
    */
   @Override
   public void mouseDragged(MouseEvent evt)
   {
-    mx = evt.getX();
-    my = evt.getY();
+    int xPos = evt.getX();
+    int yPos = evt.getY();
+
+    if (xPos == mouseX && yPos == mouseY)
+    {
+      return;
+    }
+
+    int xDelta = xPos - mouseX;
+    int yDelta = yPos - mouseY;
 
     // Check if this is a rectangle drawing drag
-    if ((evt.getModifiers() & InputEvent.BUTTON2_MASK) != 0)
+    if ((evt.getModifiersEx() & InputEvent.BUTTON2_DOWN_MASK) != 0)
     {
       // rectx2 = evt.getX();
       // recty2 = evt.getY();
     }
     else
     {
-      rotmat.setIdentity();
+      rotate(xDelta, yDelta);
 
-      rotmat.rotate(my - omy, 'x');
-      rotmat.rotate(mx - omx, 'y');
+      mouseX = xPos;
+      mouseY = yPos;
 
-      for (int i = 0; i < npoint; i++)
-      {
-        SequencePoint sp = points.elementAt(i);
-        sp.coord[0] -= centre[0];
-        sp.coord[1] -= centre[1];
-        sp.coord[2] -= centre[2];
-
-        // Now apply the rotation matrix
-        sp.coord = rotmat.vectorMultiply(sp.coord);
-
-        // Now translate back again
-        sp.coord[0] += centre[0];
-        sp.coord[1] += centre[1];
-        sp.coord[2] += centre[2];
-      }
+      // findWidths();
 
-      for (int i = 0; i < 3; i++)
-      {
-        axes[i] = rotmat.vectorMultiply(axes[i]);
-      }
+      repaint();
+    }
+  }
 
-      omx = mx;
-      omy = my;
+  @Override
+  public void rotate(float x, float y)
+  {
+    if (x == 0f && y == 0f)
+    {
+      return;
+    }
 
-      paint(this.getGraphics());
+    /*
+     * get the identity transformation...
+     */
+    RotatableMatrix rotmat = new RotatableMatrix();
+
+    /*
+     * rotate around the X axis for change in Y
+     * (mouse movement up/down); note we are equating a
+     * number of pixels with degrees of rotation here!
+     */
+    if (y != 0)
+    {
+      rotmat.rotate(y, Axis.X);
     }
+
+    /*
+     * rotate around the Y axis for change in X
+     * (mouse movement left/right)
+     */
+    if (x != 0)
+    {
+      rotmat.rotate(x, Axis.Y);
+    }
+
+    /*
+     * apply the composite transformation to sequence points;
+     * update z min-max range (affects colour graduation), but not
+     * x or y min-max (as this would affect axis scaling)
+     */
+    float[] centre = getCentre();
+    float zMin = Float.MAX_VALUE;
+    float zMax = -Float.MAX_VALUE;
+
+    for (int i = 0; i < npoint; i++)
+    {
+      SequencePoint sp = sequencePoints.get(i);
+      sp.translate(-centre[0], -centre[1], -centre[2]);
+
+      // Now apply the rotation matrix
+      sp.coord = rotmat.vectorMultiply(sp.coord);
+
+      // Now translate back again
+      sp.translate(centre[0], centre[1], centre[2]);
+
+      zMin = Math.min(zMin, sp.coord.z);
+      zMax = Math.max(zMax, sp.coord.z);
+    }
+
+    seqMin[2] = zMin;
+    seqMax[2] = zMax;
+
+    /*
+     * rotate the x/y/z axis positions
+     */
+    for (int i = 0; i < DIMS; i++)
+    {
+      getAxisEndPoints()[i] = rotmat.vectorMultiply(getAxisEndPoints()[i]);
+    }
+  }
+
+  /**
+   * Answers the x/y/z coordinates that are midway between the maximum and
+   * minimum sequence point values
+   * 
+   * @return
+   */
+  private float[] getCentre()
+  {
+    float xCentre = (seqMin[0] + seqMax[0]) / 2f;
+    float yCentre = (seqMin[1] + seqMax[1]) / 2f;
+    float zCentre = (seqMin[2] + seqMax[2]) / 2f;
+
+    return new float[] { xCentre, yCentre, zCentre };
   }
 
   /**
@@ -756,14 +741,16 @@ public class RotatableCanvas extends JPanel implements MouseListener,
    * @param x2
    * @param y2
    */
-  public void rectSelect(int x1, int y1, int x2, int y2)
+  protected void rectSelect(int x1, int y1, int x2, int y2)
   {
+    float[] centre = getCentre();
+
     for (int i = 0; i < npoint; i++)
     {
-      SequencePoint sp = points.elementAt(i);
-      int tmp1 = (int) (((sp.coord[0] - centre[0]) * scale)
+      SequencePoint sp = sequencePoints.get(i);
+      int tmp1 = (int) (((sp.coord.x - centre[0]) * getScaleFactor())
               + (getWidth() / 2.0));
-      int tmp2 = (int) (((sp.coord[1] - centre[1]) * scale)
+      int tmp2 = (int) (((sp.coord.y - centre[1]) * getScaleFactor())
               + (getHeight() / 2.0));
 
       if ((tmp1 > x1) && (tmp1 < x2) && (tmp2 > y1) && (tmp2 < y2))
@@ -771,8 +758,7 @@ public class RotatableCanvas extends JPanel implements MouseListener,
         if (av != null)
         {
           SequenceI sequence = sp.getSequence();
-          if (!av.getSelectionGroup().getSequences(null)
-                  .contains(sequence))
+          if (!av.getSelectionGroup().getSequences(null).contains(sequence))
           {
             av.getSelectionGroup().addSequence(sequence, true);
           }
@@ -790,30 +776,36 @@ public class RotatableCanvas extends JPanel implements MouseListener,
    * 
    * @return
    */
-  public SequenceI findSequenceAtPoint(int x, int y)
+  protected SequenceI findSequenceAtPoint(int x, int y)
   {
     int halfwidth = getWidth() / 2;
     int halfheight = getHeight() / 2;
 
     int found = -1;
+    int pix = Math.min(getWidth(), getHeight());
+    float xWidth = Math.abs(seqMax[0] - seqMin[0]);
+    float yWidth = Math.abs(seqMax[1] - seqMin[1]);
+    float maxWidth = Math.max(xWidth, yWidth);
+    float scaleBy = pix * getScaleFactor() / (2f * maxWidth);
+
+    float[] centre = getCentre();
 
     for (int i = 0; i < npoint; i++)
     {
-      SequencePoint sp = points.elementAt(i);
-      int px = (int) ((sp.coord[0] - centre[0]) * scale)
-              + halfwidth;
-      int py = (int) ((sp.coord[1] - centre[1]) * scale)
-              + halfheight;
+      SequencePoint sp = sequencePoints.get(i);
+      int px = (int) ((sp.coord.x - centre[0]) * scaleBy) + halfwidth;
+      int py = (int) ((sp.coord.y - centre[1]) * scaleBy) + halfheight;
 
-      if ((Math.abs(px - x) < 3) && (Math.abs(py - y) < 3))
+      if ((Math.abs(px - x) < NEARBY) && (Math.abs(py - y) < NEARBY))
       {
         found = i;
+        break;
       }
     }
 
     if (found != -1)
     {
-      return points.elementAt(found).getSequence();
+      return sequencePoints.get(found).getSequence();
     }
     else
     {
@@ -821,9 +813,15 @@ public class RotatableCanvas extends JPanel implements MouseListener,
     }
   }
 
+  /**
+   * Answers the panel the PCA is associated with (all panels for this alignment
+   * if 'associate with all panels' is selected).
+   * 
+   * @return
+   */
   AlignmentPanel[] getAssociatedPanels()
   {
-    if (applyToAllViews)
+    if (isApplyToAllViews())
     {
       return PaintRefresher.getAssociatedPanels(av.getSequenceSetId());
     }
@@ -833,19 +831,114 @@ public class RotatableCanvas extends JPanel implements MouseListener,
     }
   }
 
+  public Color getBackgroundColour()
+  {
+    return getBgColour();
+  }
+
   /**
+   * Zooms in or out in response to mouse wheel movement
+   */
+  @Override
+  public void mouseWheelMoved(MouseWheelEvent e)
+  {
+    double wheelRotation = e.getPreciseWheelRotation();
+    if (wheelRotation > 0)
+    {
+      zoom(ZOOM_IN);
+      repaint();
+    }
+    else if (wheelRotation < 0)
+    {
+      zoom(ZOOM_OUT);
+      repaint();
+    }
+  }
+
+  /**
+   * Answers the sequence point minimum [x, y, z] values. Note these are derived
+   * when sequence points are set, but x and y values are not updated on
+   * rotation (because this would result in changes to scaling).
    * 
-   * @return x,y,z positions of point s (index into points) under current
-   *         transform.
+   * @return
    */
-  public double[] getPointPosition(int s)
+  public float[] getSeqMin()
   {
-    double[] pts = new double[3];
-    float[] p = points.elementAt(s).coord;
-    pts[0] = p[0];
-    pts[1] = p[1];
-    pts[2] = p[2];
-    return pts;
+    return seqMin;
   }
 
+  /**
+   * Answers the sequence point maximum [x, y, z] values. Note these are derived
+   * when sequence points are set, but x and y values are not updated on
+   * rotation (because this would result in changes to scaling).
+   * 
+   * @return
+   */
+  public float[] getSeqMax()
+  {
+    return seqMax;
+  }
+
+  /**
+   * Sets the minimum and maximum [x, y, z] positions for sequence points. For
+   * use when restoring a saved PCA from state data.
+   * 
+   * @param min
+   * @param max
+   */
+  public void setSeqMinMax(float[] min, float[] max)
+  {
+    seqMin = min;
+    seqMax = max;
+  }
+
+  public float getScaleFactor()
+  {
+    return scaleFactor;
+  }
+
+  public void setScaleFactor(float scaleFactor)
+  {
+    this.scaleFactor = scaleFactor;
+  }
+
+  public boolean isShowLabels()
+  {
+    return showLabels;
+  }
+
+  public void setShowLabels(boolean showLabels)
+  {
+    this.showLabels = showLabels;
+  }
+
+  public boolean isApplyToAllViews()
+  {
+    return applyToAllViews;
+  }
+
+  public void setApplyToAllViews(boolean applyToAllViews)
+  {
+    this.applyToAllViews = applyToAllViews;
+  }
+
+  public Point[] getAxisEndPoints()
+  {
+    return axisEndPoints;
+  }
+
+  public void setAxisEndPoints(Point[] axisEndPoints)
+  {
+    this.axisEndPoints = axisEndPoints;
+  }
+
+  public Color getBgColour()
+  {
+    return bgColour;
+  }
+
+  public void setBgColour(Color bgColour)
+  {
+    this.bgColour = bgColour;
+  }
 }