/*
* Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
* Copyright (C) $$Year-Rel$$ The Jalview Authors
*
* This file is part of Jalview.
*
* Jalview is free software: you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation, either version 3
* of the License, or (at your option) any later version.
*
* Jalview is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty
* of MERCHANTABILITY or FITNESS FOR A PARTICULAR
* PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Jalview. If not, see .
* The Jalview Authors are detailed in the 'AUTHORS' file.
*/
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;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.RenderingHints;
import java.awt.event.InputEvent;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.MouseEvent;
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 javax.swing.JPanel;
import javax.swing.ToolTipManager;
/**
* DOCUMENT ME!
*
* @author $author$
* @version $Revision$
*/
public class RotatableCanvas extends JPanel implements MouseListener,
MouseMotionListener, KeyListener, RotatableCanvasI
{
RotatableMatrix idmat = new RotatableMatrix(3, 3);
RotatableMatrix objmat = new RotatableMatrix(3, 3);
RotatableMatrix rotmat = new RotatableMatrix(3, 3);
// RubberbandRectangle rubberband;
boolean drawAxes = true;
int omx = 0;
int mx = 0;
int omy = 0;
int my = 0;
Image img;
Graphics ig;
Dimension prefsize;
float[] centre = new float[3];
float[] width = new float[3];
float[] max = new float[3];
float[] min = new float[3];
float maxwidth;
float scale;
int npoint;
Vector points;
float[][] orig;
float[][] axes;
int startx;
int starty;
int lastx;
int lasty;
int rectx1;
int recty1;
int rectx2;
int recty2;
float scalefactor = 1;
AlignmentViewport av;
AlignmentPanel ap;
boolean showLabels = false;
Color bgColour = Color.black;
boolean applyToAllViews = false;
boolean first = true;
public RotatableCanvas(AlignmentPanel ap)
{
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();
}
}
});
}
public void showLabels(boolean b)
{
showLabels = b;
repaint();
}
@Override
public void setPoints(Vector points, int npoint)
{
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];
for (int i = 0; i < npoint; i++)
{
SequencePoint sp = points.elementAt(i);
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;
}
}
}
}
/**
* DOCUMENT ME!
*/
public void findWidth()
{
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);
}
/**
* DOCUMENT ME!
*
* @return DOCUMENT ME!
*/
public 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);
}
/**
* 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!
*
* @return DOCUMENT ME!
*/
@Override
public Dimension getPreferredSize()
{
if (prefsize != null)
{
return prefsize;
}
else
{
return new Dimension(400, 400);
}
}
/**
* DOCUMENT ME!
*
* @return DOCUMENT ME!
*/
@Override
public Dimension getMinimumSize()
{
return getPreferredSize();
}
/**
* DOCUMENT ME!
*
* @param g
* DOCUMENT ME!
*/
@Override
public void paintComponent(Graphics g1)
{
Graphics2D g = (Graphics2D) g1;
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
if (points == null)
{
g.setFont(new Font("Verdana", Font.PLAIN, 18));
g.drawString(
MessageManager.getString("label.calculating_pca") + "....",
20, getHeight() / 2);
}
else
{
// Only create the image at the beginning -
if ((img == null) || (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);
drawScene(ig);
if (drawAxes)
{
drawAxes(ig);
}
g.drawImage(img, 0, 0, this);
}
}
/**
* DOCUMENT ME!
*
* @param g
* DOCUMENT ME!
*/
public void drawAxes(Graphics g)
{
g.setColor(Color.yellow);
for (int i = 0; i < 3; 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)));
}
}
/**
* DOCUMENT ME!
*
* @param g
* DOCUMENT ME!
* @param col
* DOCUMENT ME!
*/
public void drawBackground(Graphics g, Color col)
{
g.setColor(col);
g.fillRect(0, 0, prefsize.width, prefsize.height);
}
/**
* DOCUMENT ME!
*
* @param g
* DOCUMENT ME!
*/
public void drawScene(Graphics g1)
{
Graphics2D g = (Graphics2D) g1;
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
int halfwidth = getWidth() / 2;
int halfheight = getHeight() / 2;
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);
}
}
if (z < 0)
{
g.setColor(g.getColor().darker());
}
g.fillRect(x - 3, y - 3, 6, 6);
if (showLabels)
{
g.setColor(Color.red);
g.drawString(sequence.getName(), x - 3, y - 4);
}
}
// //Now the rectangle
// if (rectx2 != -1 && recty2 != -1) {
// g.setColor(Color.white);
//
// g.drawRect(rectx1,recty1,rectx2-rectx1,recty2-recty1);
// }
}
/**
* DOCUMENT ME!
*
* @return DOCUMENT ME!
*/
public Dimension minimumsize()
{
return prefsize;
}
/**
* DOCUMENT ME!
*
* @return DOCUMENT ME!
*/
public Dimension preferredsize()
{
return prefsize;
}
/**
* 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!
*
* @param evt
* DOCUMENT ME!
*/
@Override
public void keyPressed(KeyEvent evt)
{
if (evt.getKeyCode() == KeyEvent.VK_UP)
{
scalefactor = (float) (scalefactor * 1.1);
scale = findScale();
}
else if (evt.getKeyCode() == KeyEvent.VK_DOWN)
{
scalefactor = (float) (scalefactor * 0.9);
scale = findScale();
}
else if (evt.getKeyChar() == 's')
{
Cache.log.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);
}
}
repaint();
}
@Override
public void mouseClicked(MouseEvent evt)
{
}
@Override
public void mouseEntered(MouseEvent evt)
{
}
@Override
public void mouseExited(MouseEvent evt)
{
}
@Override
public void mouseReleased(MouseEvent evt)
{
}
/**
* If the mouse press is at (within 2 pixels of) a sequence point, toggles
* (adds or removes) the corresponding sequence as a member of the viewport
* selection group. This supports configuring a group in the alignment by
* clicking on points in the PCA display.
*/
@Override
public void mousePressed(MouseEvent evt)
{
int x = evt.getX();
int y = evt.getY();
mx = x;
my = y;
omx = mx;
omy = my;
startx = x;
starty = y;
rectx1 = x;
recty1 = y;
rectx2 = -1;
recty2 = -1;
SequenceI found = findSequenceAtPoint(x, y);
if (found != null)
{
AlignmentPanel[] aps = getAssociatedPanels();
for (int a = 0; a < aps.length; a++)
{
if (aps[a].av.getSelectionGroup() != null)
{
aps[a].av.getSelectionGroup().addOrRemove(found, true);
}
else
{
aps[a].av.setSelectionGroup(new SequenceGroup());
aps[a].av.getSelectionGroup().addOrRemove(found, true);
aps[a].av.getSelectionGroup()
.setEndRes(aps[a].av.getAlignment().getWidth() - 1);
}
}
PaintRefresher.Refresh(this, av.getSequenceSetId());
// canonical selection is sent to other listeners
av.sendSelection();
}
repaint();
}
/**
* Sets the tooltip to the name of the sequence within 2 pixels of the mouse
* position, or clears the tooltip if none found
*/
@Override
public void mouseMoved(MouseEvent evt)
{
SequenceI found = findSequenceAtPoint(evt.getX(), evt.getY());
this.setToolTipText(found == null ? null : found.getName());
}
/**
* DOCUMENT ME!
*
* @param evt
* DOCUMENT ME!
*/
@Override
public void mouseDragged(MouseEvent evt)
{
mx = evt.getX();
my = evt.getY();
// Check if this is a rectangle drawing drag
if ((evt.getModifiers() & InputEvent.BUTTON2_MASK) != 0)
{
// rectx2 = evt.getX();
// recty2 = evt.getY();
}
else
{
rotmat.setIdentity();
rotmat.rotate(my - omy, 'x');
rotmat.rotate(mx - omx, 'y');
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];
}
for (int i = 0; i < 3; i++)
{
axes[i] = rotmat.vectorMultiply(axes[i]);
}
omx = mx;
omy = my;
paint(this.getGraphics());
}
}
/**
* Adds any sequences whose displayed points are within the given rectangle to
* the viewport's current selection. Intended for key 's' after dragging to
* select a region of the PCA.
*
* @param x1
* @param y1
* @param x2
* @param y2
*/
public void rectSelect(int x1, int y1, int x2, int y2)
{
for (int i = 0; i < npoint; i++)
{
SequencePoint sp = points.elementAt(i);
int tmp1 = (int) (((sp.coord[0] - centre[0]) * scale)
+ (getWidth() / 2.0));
int tmp2 = (int) (((sp.coord[1] - centre[1]) * scale)
+ (getHeight() / 2.0));
if ((tmp1 > x1) && (tmp1 < x2) && (tmp2 > y1) && (tmp2 < y2))
{
if (av != null)
{
SequenceI sequence = sp.getSequence();
if (!av.getSelectionGroup().getSequences(null)
.contains(sequence))
{
av.getSelectionGroup().addSequence(sequence, true);
}
}
}
}
}
/**
* Answers the first sequence found whose point on the display is within 2
* pixels of the given coordinates, or null if none is found
*
* @param x
* @param y
*
* @return
*/
public SequenceI findSequenceAtPoint(int x, int y)
{
int halfwidth = getWidth() / 2;
int halfheight = getHeight() / 2;
int found = -1;
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;
if ((Math.abs(px - x) < 3) && (Math.abs(py - y) < 3))
{
found = i;
}
}
if (found != -1)
{
return points.elementAt(found).getSequence();
}
else
{
return null;
}
}
AlignmentPanel[] getAssociatedPanels()
{
if (applyToAllViews)
{
return PaintRefresher.getAssociatedPanels(av.getSequenceSetId());
}
else
{
return new AlignmentPanel[] { ap };
}
}
/**
*
* @return x,y,z positions of point s (index into points) under current
* transform.
*/
public double[] getPointPosition(int s)
{
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;
}
}