JAL-4045 - don't apply distribution dependent scale factor to the axis lines!
[jalview.git] / src / jalview / gui / RotatableCanvas.java
index 615b403..ef0b2aa 100755 (executable)
  */
 package jalview.gui;
 
-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;
-
 import java.awt.Color;
 import java.awt.Dimension;
 import java.awt.Font;
@@ -46,82 +35,104 @@ 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;
+
 /**
  * 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
 {
-  private static final int DIMS = 3;
-
-  // RubberbandRectangle rubberband;
-  boolean drawAxes = true;
-
-  int mouseX = 0;
-
-  int mouseY = 0;
-
-  Image img;
-
-  Graphics ig;
-
-  Dimension prefsize;
-
-  Point centre;
+  private static final float ZOOM_OUT = 0.9f;
 
-  float[] width = new float[DIMS];
+  private static final float ZOOM_IN = 1.1f;
 
-  float[] max = new float[DIMS];
+  /*
+   * pixels distance within which tooltip shows sequence name
+   */
+  private static final int NEARBY = 3;
 
-  float[] min = new float[DIMS];
+  private static final List<String> AXES = Arrays.asList("x", "y", "z");
 
-  float maxwidth;
+  private static final Color AXIS_COLOUR = Color.yellow;
 
-  float scale;
+  private static final int DIMS = 3;
 
-  int npoint;
+  boolean drawAxes = true;
 
-  Vector<SequencePoint> points;
+  int mouseX;
 
-  Point[] orig;
+  int mouseY;
 
-  Point[] axisEndPoints;
+  Image img;
 
-  int startx;
+  Graphics ig;
 
-  int starty;
+  Dimension prefSize;
 
-  int lastx;
+  /*
+   * 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;
 
-  int lasty;
+  float[] seqMax;
 
-  int rectx1;
+  /*
+   * 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 recty1;
+  int npoint;
 
-  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;
-
-  Color bgColour = Color.black;
+  private boolean showLabels;
 
-  boolean applyToAllViews = false;
+  private Color bgColour;
 
-  boolean first = true;
+  private boolean applyToAllViews;
 
   /**
    * Constructor
@@ -132,33 +143,17 @@ public class RotatableCanvas extends JPanel implements MouseListener,
   {
     this.av = panel.av;
     this.ap = panel;
-    axisEndPoints = new Point[DIMS];
+    setAxisEndPoints(new Point[DIMS]);
+    setShowLabels(false);
+    setApplyToAllViews(false);
+    setBgColour(Color.BLACK);
+    resetAxes();
 
-    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();
-        }
-      }
-    });
+    ToolTipManager.sharedInstance().registerComponent(this);
 
+    addMouseListener(this);
+    addMouseMotionListener(this);
+    addMouseWheelListener(this);
   }
 
   /**
@@ -168,42 +163,20 @@ public class RotatableCanvas extends JPanel implements MouseListener,
    */
   public void showLabels(boolean show)
   {
-    showLabels = show;
+    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 Point[npoint];
-
-    for (int i = 0; i < npoint; i++)
-    {
-      SequencePoint sp = points.elementAt(i);
-      orig[i] = sp.coord;
-    }
-
-    resetAxes();
+    this.sequencePoints = points;
+    this.npoint = np;
+    prefSize = getPreferredSize();
 
-    findCentre();
-    findWidth();
+    findWidths();
 
-    scale = findScale();
-    if (first)
-    {
-      addMouseListener(this);
-      addMouseMotionListener(this);
-    }
-    first = false;
+    setScaleFactor(1f);
   }
 
   /**
@@ -212,30 +185,29 @@ public class RotatableCanvas extends JPanel implements MouseListener,
    */
   protected void resetAxes()
   {
-    axisEndPoints[0] = new Point(1f, 0f, 0f);
-    axisEndPoints[1] = new Point(0f, 1f, 0f);
-    axisEndPoints[2] = new Point(0f, 0f, 1f);
+    getAxisEndPoints()[0] = new Point(1f, 0f, 0f);
+    getAxisEndPoints()[1] = new Point(0f, 1f, 0f);
+    getAxisEndPoints()[2] = new Point(0f, 0f, 1f);
   }
 
   /**
-   * Computes and saves the maximum and minimum (x, y, z) positions of any
-   * sequence point, and also the min-max range (width) for each dimension, and
-   * the maximum width for all dimensions
+   * Computes and saves the min-max ranges of x/y/z positions of the sequence
+   * points
    */
-  protected void findWidth()
+  protected void findWidths()
   {
-    max = new float[DIMS];
-    min = new float[DIMS];
+    float[] max = new float[DIMS];
+    float[] min = new float[DIMS];
 
-    max[0] = Float.MIN_VALUE;
-    max[1] = Float.MIN_VALUE;
-    max[2] = Float.MIN_VALUE;
+    max[0] = -Float.MAX_VALUE;
+    max[1] = -Float.MAX_VALUE;
+    max[2] = -Float.MAX_VALUE;
 
     min[0] = Float.MAX_VALUE;
     min[1] = Float.MAX_VALUE;
     min[2] = Float.MAX_VALUE;
 
-    for (SequencePoint sp : points)
+    for (SequencePoint sp : sequencePoints)
     {
       max[0] = Math.max(max[0], sp.coord.x);
       max[1] = Math.max(max[1], sp.coord.y);
@@ -245,72 +217,21 @@ public class RotatableCanvas extends JPanel implements MouseListener,
       min[2] = Math.min(min[2], sp.coord.z);
     }
 
-    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 = Math.max(width[0], Math.max(width[1], width[2]));
+    seqMin = min;
+    seqMax = max;
   }
 
   /**
-   * DOCUMENT ME!
+   * Answers the preferred size if it has been set, else 400 x 400
    * 
-   * @return DOCUMENT ME!
-   */
-  protected float findScale()
-  {
-    int dim;
-    int w;
-    int height;
-
-    if (getWidth() != 0)
-    {
-      w = getWidth();
-      height = getHeight();
-    }
-    else
-    {
-      w = prefsize.width;
-      height = prefsize.height;
-    }
-
-    if (w < height)
-    {
-      dim = w;
-    }
-    else
-    {
-      dim = height;
-    }
-
-    return (dim * scalefactor) / (2 * maxwidth);
-  }
-
-  /**
-   * Computes and saves the position of the centre of the view
-   */
-  protected void findCentre()
-  {
-    findWidth();
-
-    float x = (max[0] + min[0]) / 2;
-    float y = (max[1] + min[1]) / 2;
-    float z = (max[2] + min[2]) / 2;
-
-    centre = new Point(x, y, z);
-  }
-
-  /**
-   * DOCUMENT ME!
-   * 
-   * @return DOCUMENT ME!
+   * @return
    */
   @Override
   public Dimension getPreferredSize()
   {
-    if (prefsize != null)
+    if (prefSize != null)
     {
-      return prefsize;
+      return prefSize;
     }
     else
     {
@@ -319,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()
@@ -330,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)
@@ -343,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(
@@ -352,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();
-
-        scale = findScale();
+        prefSize.width = getWidth();
+        prefSize.height = getHeight();
 
-        // System.out.println("New scale = " + scale);
         img = createImage(getWidth(), getHeight());
         ig = img.getGraphics();
       }
 
-      drawBackground(ig, bgColour);
+      drawBackground(ig);
       drawScene(ig);
 
       if (drawAxes)
@@ -379,12 +300,15 @@ public class RotatableCanvas extends JPanel implements MouseListener,
   }
 
   /**
-   * Resets the view to initial state (no rotation)
+   * Resets the rotation and choice of axes to the initial state (without change
+   * of scale factor)
    */
   public void resetView()
   {
     img = null;
+    findWidths();
     resetAxes();
+    repaint();
   }
 
   /**
@@ -394,27 +318,32 @@ public class RotatableCanvas extends JPanel implements MouseListener,
    */
   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 < DIMS; i++)
     {
-      g.drawLine(getWidth() / 2, getHeight() / 2,
-              (int) ((axisEndPoints[i].x * scale * max[0]) + (getWidth() / 2)),
-              (int) ((axisEndPoints[i].y * 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));
     }
   }
 
   /**
-   * Fills the background with the specified colour
+   * Fills the background with the currently configured background colour
    * 
    * @param g
-   * @param col
    */
-  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);
   }
 
   /**
@@ -432,6 +361,13 @@ public class RotatableCanvas extends JPanel implements MouseListener,
 
     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);
+
+    float[] centre = getCentre();
 
     for (int i = 0; i < npoint; i++)
     {
@@ -439,23 +375,35 @@ public class RotatableCanvas extends JPanel implements MouseListener,
        * sequence point colour as sequence id, but
        * gray if sequence is currently selected
        */
-      SequencePoint sp = points.elementAt(i);
+      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.x) * scale) + halfwidth;
-      int y = (int) ((sp.coord.y - centre.y) * scale) + halfheight;
+      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 (showLabels)
+      if (isShowLabels())
       {
         g.setColor(Color.red);
         g.drawString(sp.getSequence().getName(), x - 3, y - 4);
       }
     }
-
+    if (isShowLabels())
+    {
+      g.setColor(AXIS_COLOUR);
+      int midX = getWidth() / 2;
+      int midY = getHeight() / 2;
+      Iterator<String> axes = AXES.iterator();
+      for (Point p : getAxisEndPoints())
+      {
+        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);
@@ -490,10 +438,19 @@ public class RotatableCanvas extends JPanel implements MouseListener,
     }
 
     /*
-     * graduate from front (brighter) to back (darker)
+     * graduate brighter for point in front of centre, darker if behind centre
      */
-    sequenceColour = ColorUtils.getGraduatedColour(sp.coord.z, min[2],
-            max[2], sequenceColour);
+    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;
   }
@@ -516,31 +473,63 @@ public class RotatableCanvas extends JPanel implements MouseListener,
   @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)
     {
-      scalefactor = (float) (scalefactor * 1.1);
-      scale = findScale();
+      if (shiftDown)
+      {
+        rotate(0f, 1f);
+      }
+      else
+      {
+        zoom(ZOOM_OUT);
+      }
     }
-    else if (evt.getKeyCode() == KeyEvent.VK_DOWN)
+    else if (shiftDown && keyCode == KeyEvent.VK_LEFT)
     {
-      scalefactor = (float) (scalefactor * 0.9);
-      scale = findScale();
+      rotate(1f, 0f);
+    }
+    else if (shiftDown && keyCode == KeyEvent.VK_RIGHT)
+    {
+      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)
   {
   }
@@ -575,14 +564,10 @@ public class RotatableCanvas extends JPanel implements MouseListener,
     mouseX = x;
     mouseY = y;
 
-    startx = x;
-    starty = y;
-
-    rectx1 = x;
-    recty1 = y;
-
-    rectx2 = -1;
-    recty2 = -1;
+    // rectx1 = x;
+    // recty1 = y;
+    // rectx2 = -1;
+    // recty2 = -1;
 
     SequenceI found = findSequenceAtPoint(x, y);
 
@@ -641,66 +626,109 @@ public class RotatableCanvas extends JPanel implements MouseListener,
       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
     {
-      /*
-       * get the identity transformation...
-       */
-      RotatableMatrix rotmat = new RotatableMatrix();
+      rotate(xDelta, yDelta);
 
-      /*
-       * 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 (yPos != mouseY)
-      {
-        rotmat.rotate(yPos - mouseY, Axis.X);
-      }
+      mouseX = xPos;
+      mouseY = yPos;
 
-      /*
-       * rotate around the Y axis for change in X
-       * (mouse movement left/right)
-       */
-      if (xPos != mouseX)
-      {
-        rotmat.rotate(xPos - mouseX, Axis.Y);
-      }
+      // findWidths();
 
-      /*
-       * apply the composite transformation to sequence points
-       */
-      for (int i = 0; i < npoint; i++)
-      {
-        SequencePoint sp = points.elementAt(i);
-        sp.translateBack(centre);
+      repaint();
+    }
+  }
 
-        // Now apply the rotation matrix
-        sp.coord = rotmat.vectorMultiply(sp.coord);
+  @Override
+  public void rotate(float x, float y)
+  {
+    if (x == 0f && y == 0f)
+    {
+      return;
+    }
 
-        // Now translate back again
-        sp.translate(centre);
-      }
+    /*
+     * get the identity transformation...
+     */
+    RotatableMatrix rotmat = new RotatableMatrix();
 
-      /*
-       * rotate the x/y/z axis positions
-       */
-      for (int i = 0; i < DIMS; i++)
-      {
-        axisEndPoints[i] = rotmat.vectorMultiply(axisEndPoints[i]);
-      }
+    /*
+     * 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);
+    }
 
-      mouseX = xPos;
-      mouseY = yPos;
+    /*
+     * 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]);
 
-      paint(this.getGraphics());
+      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 };
   }
 
   /**
@@ -715,12 +743,14 @@ public class RotatableCanvas extends JPanel implements MouseListener,
    */
   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.x - centre.x) * scale)
+      SequencePoint sp = sequencePoints.get(i);
+      int tmp1 = (int) (((sp.coord.x - centre[0]) * getScaleFactor())
               + (getWidth() / 2.0));
-      int tmp2 = (int) (((sp.coord.y - centre.y) * scale)
+      int tmp2 = (int) (((sp.coord.y - centre[1]) * getScaleFactor())
               + (getHeight() / 2.0));
 
       if ((tmp1 > x1) && (tmp1 < x2) && (tmp2 > y1) && (tmp2 < y2))
@@ -728,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);
           }
@@ -753,16 +782,21 @@ public class RotatableCanvas extends JPanel implements MouseListener,
     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.x - centre.x) * scale)
-              + halfwidth;
-      int py = (int) ((sp.coord.y - centre.y) * 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;
@@ -771,7 +805,7 @@ public class RotatableCanvas extends JPanel implements MouseListener,
 
     if (found != -1)
     {
-      return points.elementAt(found).getSequence();
+      return sequencePoints.get(found).getSequence();
     }
     else
     {
@@ -787,7 +821,7 @@ public class RotatableCanvas extends JPanel implements MouseListener,
    */
   AlignmentPanel[] getAssociatedPanels()
   {
-    if (applyToAllViews)
+    if (isApplyToAllViews())
     {
       return PaintRefresher.getAssociatedPanels(av.getSequenceSetId());
     }
@@ -796,4 +830,115 @@ public class RotatableCanvas extends JPanel implements MouseListener,
       return new AlignmentPanel[] { ap };
     }
   }
+
+  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
+   */
+  public float[] getSeqMin()
+  {
+    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;
+  }
 }