JAL-2963 correctly dim PCA points with negative z-coordinates
[jalview.git] / src / jalview / gui / RotatableCanvas.java
1 /*
2  * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
3  * Copyright (C) $$Year-Rel$$ The Jalview Authors
4  * 
5  * This file is part of Jalview.
6  * 
7  * Jalview is free software: you can redistribute it and/or
8  * modify it under the terms of the GNU General Public License 
9  * as published by the Free Software Foundation, either version 3
10  * of the License, or (at your option) any later version.
11  *  
12  * Jalview is distributed in the hope that it will be useful, but 
13  * WITHOUT ANY WARRANTY; without even the implied warranty 
14  * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
15  * PURPOSE.  See the GNU General Public License for more details.
16  * 
17  * You should have received a copy of the GNU General Public License
18  * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
19  * The Jalview Authors are detailed in the 'AUTHORS' file.
20  */
21 package jalview.gui;
22
23 import jalview.api.RotatableCanvasI;
24 import jalview.datamodel.Point;
25 import jalview.datamodel.SequenceGroup;
26 import jalview.datamodel.SequenceI;
27 import jalview.datamodel.SequencePoint;
28 import jalview.math.RotatableMatrix;
29 import jalview.math.RotatableMatrix.Axis;
30 import jalview.util.MessageManager;
31 import jalview.viewmodel.AlignmentViewport;
32
33 import java.awt.Color;
34 import java.awt.Dimension;
35 import java.awt.Font;
36 import java.awt.Graphics;
37 import java.awt.Graphics2D;
38 import java.awt.Image;
39 import java.awt.RenderingHints;
40 import java.awt.event.InputEvent;
41 import java.awt.event.KeyEvent;
42 import java.awt.event.KeyListener;
43 import java.awt.event.MouseEvent;
44 import java.awt.event.MouseListener;
45 import java.awt.event.MouseMotionListener;
46 import java.awt.event.MouseWheelEvent;
47 import java.awt.event.MouseWheelListener;
48 import java.util.Vector;
49
50 import javax.swing.JPanel;
51 import javax.swing.ToolTipManager;
52
53 /**
54  * Models a Panel on which a set of points, and optionally x/y/z axes, can be
55  * drawn, and rotated or zoomed with the mouse
56  */
57 public class RotatableCanvas extends JPanel implements MouseListener,
58         MouseMotionListener, KeyListener, RotatableCanvasI
59 {
60   private static final int DIMS = 3;
61
62   // RubberbandRectangle rubberband;
63   boolean drawAxes = true;
64
65   int mouseX = 0;
66
67   int mouseY = 0;
68
69   Image img;
70
71   Graphics ig;
72
73   Dimension prefsize;
74
75   Point centre;
76
77   float[] width = new float[DIMS];
78
79   float[] max = new float[DIMS];
80
81   float[] min = new float[DIMS];
82
83   float maxwidth;
84
85   float scale;
86
87   int npoint;
88
89   Vector<SequencePoint> points;
90
91   Point[] orig;
92
93   Point[] axisEndPoints;
94
95   int startx;
96
97   int starty;
98
99   int lastx;
100
101   int lasty;
102
103   int rectx1;
104
105   int recty1;
106
107   int rectx2;
108
109   int recty2;
110
111   float scalefactor = 1;
112
113   AlignmentViewport av;
114
115   AlignmentPanel ap;
116
117   boolean showLabels = false;
118
119   Color bgColour = Color.black;
120
121   boolean applyToAllViews = false;
122
123   boolean first = true;
124
125   /**
126    * Constructor
127    * 
128    * @param panel
129    */
130   public RotatableCanvas(AlignmentPanel panel)
131   {
132     this.av = panel.av;
133     this.ap = panel;
134     axisEndPoints = new Point[DIMS];
135
136     addMouseWheelListener(new MouseWheelListener()
137     {
138       @Override
139       public void mouseWheelMoved(MouseWheelEvent e)
140       {
141         double wheelRotation = e.getPreciseWheelRotation();
142         if (wheelRotation > 0)
143         {
144           /*
145            * zoom in
146            */
147           scale = (float) (scale * 1.1);
148           repaint();
149         }
150         else if (wheelRotation < 0)
151         {
152           /*
153            * zoom out
154            */
155           scale = (float) (scale * 0.9);
156           repaint();
157         }
158       }
159     });
160
161   }
162
163   public void showLabels(boolean b)
164   {
165     showLabels = b;
166     repaint();
167   }
168
169   @Override
170   public void setPoints(Vector<SequencePoint> points, int npoint)
171   {
172     this.points = points;
173     this.npoint = npoint;
174     if (first)
175     {
176       ToolTipManager.sharedInstance().registerComponent(this);
177       ToolTipManager.sharedInstance().setInitialDelay(0);
178       ToolTipManager.sharedInstance().setDismissDelay(10000);
179     }
180     prefsize = getPreferredSize();
181     orig = new Point[npoint];
182
183     for (int i = 0; i < npoint; i++)
184     {
185       SequencePoint sp = points.elementAt(i);
186       orig[i] = sp.coord;
187     }
188
189     resetAxes();
190
191     findCentre();
192     findWidth();
193
194     scale = findScale();
195     if (first)
196     {
197       addMouseListener(this);
198       addMouseMotionListener(this);
199     }
200     first = false;
201   }
202
203   /**
204    * Resets axes to the initial state: x-axis to the right, y-axis up, z-axis to
205    * back (so obscured in a 2-D display)
206    */
207   public void resetAxes()
208   {
209     axisEndPoints[0] = new Point(1f, 0f, 0f);
210     axisEndPoints[1] = new Point(0f, 1f, 0f);
211     axisEndPoints[2] = new Point(0f, 0f, 1f);
212   }
213
214   /**
215    * Computes and saves the maximum and minimum (x, y, z) positions of any
216    * sequence point, and also the min-max range (width) for each dimension, and
217    * the maximum width for all dimensions
218    */
219   public void findWidth()
220   {
221     max = new float[DIMS];
222     min = new float[DIMS];
223
224     max[0] = Float.MIN_VALUE;
225     max[1] = Float.MIN_VALUE;
226     max[2] = Float.MIN_VALUE;
227
228     min[0] = Float.MAX_VALUE;
229     min[1] = Float.MAX_VALUE;
230     min[2] = Float.MAX_VALUE;
231
232     for (SequencePoint sp : points)
233     {
234       max[0] = Math.max(max[0], sp.coord.x);
235       max[1] = Math.max(max[1], sp.coord.y);
236       max[2] = Math.max(max[2], sp.coord.z);
237       min[0] = Math.min(min[0], sp.coord.x);
238       min[1] = Math.min(min[1], sp.coord.y);
239       min[2] = Math.min(min[2], sp.coord.z);
240     }
241
242     width[0] = Math.abs(max[0] - min[0]);
243     width[1] = Math.abs(max[1] - min[1]);
244     width[2] = Math.abs(max[2] - min[2]);
245
246     maxwidth = Math.max(width[0], Math.max(width[1], width[2]));
247   }
248
249   /**
250    * DOCUMENT ME!
251    * 
252    * @return DOCUMENT ME!
253    */
254   public float findScale()
255   {
256     int dim;
257     int w;
258     int height;
259
260     if (getWidth() != 0)
261     {
262       w = getWidth();
263       height = getHeight();
264     }
265     else
266     {
267       w = prefsize.width;
268       height = prefsize.height;
269     }
270
271     if (w < height)
272     {
273       dim = w;
274     }
275     else
276     {
277       dim = height;
278     }
279
280     return (dim * scalefactor) / (2 * maxwidth);
281   }
282
283   /**
284    * Computes and saves the position of the centre of the view
285    */
286   public void findCentre()
287   {
288     findWidth();
289
290     float x = (max[0] + min[0]) / 2;
291     float y = (max[1] + min[1]) / 2;
292     float z = (max[2] + min[2]) / 2;
293
294     centre = new Point(x, y, z);
295   }
296
297   /**
298    * DOCUMENT ME!
299    * 
300    * @return DOCUMENT ME!
301    */
302   @Override
303   public Dimension getPreferredSize()
304   {
305     if (prefsize != null)
306     {
307       return prefsize;
308     }
309     else
310     {
311       return new Dimension(400, 400);
312     }
313   }
314
315   /**
316    * DOCUMENT ME!
317    * 
318    * @return DOCUMENT ME!
319    */
320   @Override
321   public Dimension getMinimumSize()
322   {
323     return getPreferredSize();
324   }
325
326   /**
327    * DOCUMENT ME!
328    * 
329    * @param g
330    *          DOCUMENT ME!
331    */
332   @Override
333   public void paintComponent(Graphics g1)
334   {
335
336     Graphics2D g = (Graphics2D) g1;
337
338     g.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
339             RenderingHints.VALUE_ANTIALIAS_ON);
340     if (points == null)
341     {
342       g.setFont(new Font("Verdana", Font.PLAIN, 18));
343       g.drawString(
344               MessageManager.getString("label.calculating_pca") + "....",
345               20, getHeight() / 2);
346     }
347     else
348     {
349       // Only create the image at the beginning -
350       if ((img == null) || (prefsize.width != getWidth())
351               || (prefsize.height != getHeight()))
352       {
353         prefsize.width = getWidth();
354         prefsize.height = getHeight();
355
356         scale = findScale();
357
358         // System.out.println("New scale = " + scale);
359         img = createImage(getWidth(), getHeight());
360         ig = img.getGraphics();
361       }
362
363       drawBackground(ig, bgColour);
364       drawScene(ig);
365
366       if (drawAxes)
367       {
368         drawAxes(ig);
369       }
370
371       g.drawImage(img, 0, 0, this);
372     }
373   }
374
375   /**
376    * Resets the view to initial state (no rotation)
377    */
378   public void resetView()
379   {
380     img = null;
381     resetAxes();
382   }
383
384   /**
385    * Draws lines for the x, y, z axes
386    * 
387    * @param g
388    */
389   public void drawAxes(Graphics g)
390   {
391
392     g.setColor(Color.yellow);
393
394     for (int i = 0; i < DIMS; i++)
395     {
396       g.drawLine(getWidth() / 2, getHeight() / 2,
397               (int) ((axisEndPoints[i].x * scale * max[0]) + (getWidth() / 2)),
398               (int) ((axisEndPoints[i].y * scale * max[1]) + (getHeight() / 2)));
399     }
400   }
401
402   /**
403    * Fills the background with the specified colour
404    * 
405    * @param g
406    * @param col
407    */
408   public void drawBackground(Graphics g, Color col)
409   {
410     g.setColor(col);
411     g.fillRect(0, 0, prefsize.width, prefsize.height);
412   }
413
414   /**
415    * Draws points (6x6 squares) for the sequences of the PCA, and labels
416    * (sequence names) if configured to do so. The sequence points colours are
417    * taken from the sequence ids in the alignment (converting black to white).
418    * Sequences 'at the back' (z-coordinate is negative) are shaded slightly
419    * darker to help give a 3-D sensation.
420    * 
421    * @param g
422    */
423   public void drawScene(Graphics g1)
424   {
425     Graphics2D g = (Graphics2D) g1;
426
427     g.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
428             RenderingHints.VALUE_ANTIALIAS_ON);
429
430     for (int i = 0; i < npoint; i++)
431     {
432       /*
433        * sequence point colour as sequence id, but
434        * gray if sequence is currently selected
435        */
436       SequencePoint sp = points.elementAt(i);
437       SequenceI sequence = sp.getSequence();
438       Color sequenceColour = av.getSequenceColour(sequence);
439       g.setColor(
440               sequenceColour == Color.black ? Color.white : sequenceColour);
441       if (av.getSelectionGroup() != null)
442       {
443         if (av.getSelectionGroup().getSequences(null)
444                 .contains(sequence))
445         {
446           g.setColor(Color.gray);
447         }
448       }
449
450       /*
451        * dim sequence points 'at the back'
452        */
453       if (sp.coord.z < centre.z)
454       {
455         g.setColor(g.getColor().darker());
456       }
457
458       int halfwidth = getWidth() / 2;
459       int halfheight = getHeight() / 2;
460       int x = (int) ((sp.coord.x - centre.x) * scale) + halfwidth;
461       int y = (int) ((sp.coord.y - centre.y) * scale) + halfheight;
462       g.fillRect(x - 3, y - 3, 6, 6);
463
464       if (showLabels)
465       {
466         g.setColor(Color.red);
467         g.drawString(sequence.getName(), x - 3, y - 4);
468       }
469     }
470
471     // //Now the rectangle
472     // if (rectx2 != -1 && recty2 != -1) {
473     // g.setColor(Color.white);
474     //
475     // g.drawRect(rectx1,recty1,rectx2-rectx1,recty2-recty1);
476     // }
477   }
478
479   @Override
480   public void keyTyped(KeyEvent evt)
481   {
482   }
483
484   @Override
485   public void keyReleased(KeyEvent evt)
486   {
487   }
488
489   /**
490    * Responds to up or down arrow key by zooming in or out, respectively
491    * 
492    * @param evt
493    */
494   @Override
495   public void keyPressed(KeyEvent evt)
496   {
497     if (evt.getKeyCode() == KeyEvent.VK_UP)
498     {
499       scalefactor = (float) (scalefactor * 1.1);
500       scale = findScale();
501     }
502     else if (evt.getKeyCode() == KeyEvent.VK_DOWN)
503     {
504       scalefactor = (float) (scalefactor * 0.9);
505       scale = findScale();
506     }
507     else if (evt.getKeyChar() == 's')
508     {
509       // Cache.log.warn("DEBUG: Rectangle selection");
510       // todo not yet enabled as rectx2, recty2 are always -1
511       // need to set them in mouseDragged
512       if ((rectx2 != -1) && (recty2 != -1))
513       {
514         rectSelect(rectx1, recty1, rectx2, recty2);
515       }
516     }
517
518     repaint();
519   }
520
521   @Override
522   public void mouseClicked(MouseEvent evt)
523   {
524   }
525
526   @Override
527   public void mouseEntered(MouseEvent evt)
528   {
529   }
530
531   @Override
532   public void mouseExited(MouseEvent evt)
533   {
534   }
535
536   @Override
537   public void mouseReleased(MouseEvent evt)
538   {
539   }
540
541   /**
542    * If the mouse press is at (within 2 pixels of) a sequence point, toggles
543    * (adds or removes) the corresponding sequence as a member of the viewport
544    * selection group. This supports configuring a group in the alignment by
545    * clicking on points in the PCA display.
546    */
547   @Override
548   public void mousePressed(MouseEvent evt)
549   {
550     int x = evt.getX();
551     int y = evt.getY();
552
553     mouseX = x;
554     mouseY = y;
555
556     startx = x;
557     starty = y;
558
559     rectx1 = x;
560     recty1 = y;
561
562     rectx2 = -1;
563     recty2 = -1;
564
565     SequenceI found = findSequenceAtPoint(x, y);
566
567     if (found != null)
568     {
569       AlignmentPanel[] aps = getAssociatedPanels();
570
571       for (int a = 0; a < aps.length; a++)
572       {
573         if (aps[a].av.getSelectionGroup() != null)
574         {
575           aps[a].av.getSelectionGroup().addOrRemove(found, true);
576         }
577         else
578         {
579           aps[a].av.setSelectionGroup(new SequenceGroup());
580           aps[a].av.getSelectionGroup().addOrRemove(found, true);
581           aps[a].av.getSelectionGroup()
582                   .setEndRes(aps[a].av.getAlignment().getWidth() - 1);
583         }
584       }
585       PaintRefresher.Refresh(this, av.getSequenceSetId());
586       // canonical selection is sent to other listeners
587       av.sendSelection();
588     }
589
590     repaint();
591   }
592
593   /**
594    * Sets the tooltip to the name of the sequence within 2 pixels of the mouse
595    * position, or clears the tooltip if none found
596    */
597   @Override
598   public void mouseMoved(MouseEvent evt)
599   {
600     SequenceI found = findSequenceAtPoint(evt.getX(), evt.getY());
601
602     this.setToolTipText(found == null ? null : found.getName());
603   }
604
605   /**
606    * Action handler for a mouse drag. Rotates the display around the X axis (for
607    * up/down mouse movement) and/or the Y axis (for left/right mouse movement).
608    * 
609    * @param evt
610    */
611   @Override
612   public void mouseDragged(MouseEvent evt)
613   {
614     int xPos = evt.getX();
615     int yPos = evt.getY();
616
617     if (xPos == mouseX && yPos == mouseY)
618     {
619       return;
620     }
621
622     // Check if this is a rectangle drawing drag
623     if ((evt.getModifiers() & InputEvent.BUTTON2_MASK) != 0)
624     {
625       // rectx2 = evt.getX();
626       // recty2 = evt.getY();
627     }
628     else
629     {
630       /*
631        * get the identity transformation...
632        */
633       RotatableMatrix rotmat = new RotatableMatrix();
634
635       /*
636        * rotate around the X axis for change in Y
637        * (mouse movement up/down); note we are equating a
638        * number of pixels with degrees of rotation here!
639        */
640       if (yPos != mouseY)
641       {
642         rotmat.rotate(yPos - mouseY, Axis.X);
643       }
644
645       /*
646        * rotate around the Y axis for change in X
647        * (mouse movement left/right)
648        */
649       if (xPos != mouseX)
650       {
651         rotmat.rotate(xPos - mouseX, Axis.Y);
652       }
653
654       /*
655        * apply the composite transformation to sequence points
656        */
657       for (int i = 0; i < npoint; i++)
658       {
659         SequencePoint sp = points.elementAt(i);
660         sp.translateBack(centre);
661
662         // Now apply the rotation matrix
663         sp.coord = rotmat.vectorMultiply(sp.coord);
664
665         // Now translate back again
666         sp.translate(centre);
667       }
668
669       /*
670        * rotate the x/y/z axis positions
671        */
672       for (int i = 0; i < DIMS; i++)
673       {
674         axisEndPoints[i] = rotmat.vectorMultiply(axisEndPoints[i]);
675       }
676
677       mouseX = xPos;
678       mouseY = yPos;
679
680       paint(this.getGraphics());
681     }
682   }
683
684   /**
685    * Adds any sequences whose displayed points are within the given rectangle to
686    * the viewport's current selection. Intended for key 's' after dragging to
687    * select a region of the PCA.
688    * 
689    * @param x1
690    * @param y1
691    * @param x2
692    * @param y2
693    */
694   public void rectSelect(int x1, int y1, int x2, int y2)
695   {
696     for (int i = 0; i < npoint; i++)
697     {
698       SequencePoint sp = points.elementAt(i);
699       int tmp1 = (int) (((sp.coord.x - centre.x) * scale)
700               + (getWidth() / 2.0));
701       int tmp2 = (int) (((sp.coord.y - centre.y) * scale)
702               + (getHeight() / 2.0));
703
704       if ((tmp1 > x1) && (tmp1 < x2) && (tmp2 > y1) && (tmp2 < y2))
705       {
706         if (av != null)
707         {
708           SequenceI sequence = sp.getSequence();
709           if (!av.getSelectionGroup().getSequences(null)
710                   .contains(sequence))
711           {
712             av.getSelectionGroup().addSequence(sequence, true);
713           }
714         }
715       }
716     }
717   }
718
719   /**
720    * Answers the first sequence found whose point on the display is within 2
721    * pixels of the given coordinates, or null if none is found
722    * 
723    * @param x
724    * @param y
725    * 
726    * @return
727    */
728   public SequenceI findSequenceAtPoint(int x, int y)
729   {
730     int halfwidth = getWidth() / 2;
731     int halfheight = getHeight() / 2;
732
733     int found = -1;
734
735     for (int i = 0; i < npoint; i++)
736     {
737       SequencePoint sp = points.elementAt(i);
738       int px = (int) ((sp.coord.x - centre.x) * scale)
739               + halfwidth;
740       int py = (int) ((sp.coord.y - centre.y) * scale)
741               + halfheight;
742
743       if ((Math.abs(px - x) < 3) && (Math.abs(py - y) < 3))
744       {
745         found = i;
746         break;
747       }
748     }
749
750     if (found != -1)
751     {
752       return points.elementAt(found).getSequence();
753     }
754     else
755     {
756       return null;
757     }
758   }
759
760   /**
761    * Answers the panel the PCA is associated with (all panels for this alignment
762    * if 'associate with all panels' is selected).
763    * 
764    * @return
765    */
766   AlignmentPanel[] getAssociatedPanels()
767   {
768     if (applyToAllViews)
769     {
770       return PaintRefresher.getAssociatedPanels(av.getSequenceSetId());
771     }
772     else
773     {
774       return new AlignmentPanel[] { ap };
775     }
776   }
777 }