JAL-2962 code tidying/documentation (non-functional) changes
[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.bin.Cache;
25 import jalview.datamodel.SequenceGroup;
26 import jalview.datamodel.SequenceI;
27 import jalview.datamodel.SequencePoint;
28 import jalview.math.RotatableMatrix;
29 import jalview.util.MessageManager;
30 import jalview.viewmodel.AlignmentViewport;
31
32 import java.awt.Color;
33 import java.awt.Dimension;
34 import java.awt.Font;
35 import java.awt.Graphics;
36 import java.awt.Graphics2D;
37 import java.awt.Image;
38 import java.awt.RenderingHints;
39 import java.awt.event.InputEvent;
40 import java.awt.event.KeyEvent;
41 import java.awt.event.KeyListener;
42 import java.awt.event.MouseEvent;
43 import java.awt.event.MouseListener;
44 import java.awt.event.MouseMotionListener;
45 import java.awt.event.MouseWheelEvent;
46 import java.awt.event.MouseWheelListener;
47 import java.util.Vector;
48
49 import javax.swing.JPanel;
50 import javax.swing.ToolTipManager;
51
52 /**
53  * DOCUMENT ME!
54  * 
55  * @author $author$
56  * @version $Revision$
57  */
58 public class RotatableCanvas extends JPanel implements MouseListener,
59         MouseMotionListener, KeyListener, RotatableCanvasI
60 {
61   RotatableMatrix idmat = new RotatableMatrix(3, 3);
62
63   RotatableMatrix objmat = new RotatableMatrix(3, 3);
64
65   RotatableMatrix rotmat = new RotatableMatrix(3, 3);
66
67   // RubberbandRectangle rubberband;
68   boolean drawAxes = true;
69
70   int omx = 0;
71
72   int mx = 0;
73
74   int omy = 0;
75
76   int my = 0;
77
78   Image img;
79
80   Graphics ig;
81
82   Dimension prefsize;
83
84   float[] centre = new float[3];
85
86   float[] width = new float[3];
87
88   float[] max = new float[3];
89
90   float[] min = new float[3];
91
92   float maxwidth;
93
94   float scale;
95
96   int npoint;
97
98   Vector<SequencePoint> points;
99
100   float[][] orig;
101
102   float[][] axes;
103
104   int startx;
105
106   int starty;
107
108   int lastx;
109
110   int lasty;
111
112   int rectx1;
113
114   int recty1;
115
116   int rectx2;
117
118   int recty2;
119
120   float scalefactor = 1;
121
122   AlignmentViewport av;
123
124   AlignmentPanel ap;
125
126   boolean showLabels = false;
127
128   Color bgColour = Color.black;
129
130   boolean applyToAllViews = false;
131
132   boolean first = true;
133
134   public RotatableCanvas(AlignmentPanel ap)
135   {
136     this.av = ap.av;
137     this.ap = ap;
138
139     addMouseWheelListener(new MouseWheelListener()
140     {
141       @Override
142       public void mouseWheelMoved(MouseWheelEvent e)
143       {
144         double wheelRotation = e.getPreciseWheelRotation();
145         if (wheelRotation > 0)
146         {
147           /*
148            * zoom in
149            */
150           scale = (float) (scale * 1.1);
151           repaint();
152         }
153         else if (wheelRotation < 0)
154         {
155           /*
156            * zoom out
157            */
158           scale = (float) (scale * 0.9);
159           repaint();
160         }
161       }
162     });
163
164   }
165
166   public void showLabels(boolean b)
167   {
168     showLabels = b;
169     repaint();
170   }
171
172   @Override
173   public void setPoints(Vector<SequencePoint> points, int npoint)
174   {
175     this.points = points;
176     this.npoint = npoint;
177     if (first)
178     {
179       ToolTipManager.sharedInstance().registerComponent(this);
180       ToolTipManager.sharedInstance().setInitialDelay(0);
181       ToolTipManager.sharedInstance().setDismissDelay(10000);
182     }
183     prefsize = getPreferredSize();
184     orig = new float[npoint][3];
185
186     for (int i = 0; i < npoint; i++)
187     {
188       SequencePoint sp = points.elementAt(i);
189
190       for (int j = 0; j < 3; j++)
191       {
192         orig[i][j] = sp.coord[j];
193       }
194     }
195
196     // Initialize the matrices to identity
197     for (int i = 0; i < 3; i++)
198     {
199       for (int j = 0; j < 3; j++)
200       {
201         if (i != j)
202         {
203           idmat.addElement(i, j, 0);
204           objmat.addElement(i, j, 0);
205           rotmat.addElement(i, j, 0);
206         }
207         else
208         {
209           idmat.addElement(i, j, 0);
210           objmat.addElement(i, j, 0);
211           rotmat.addElement(i, j, 0);
212         }
213       }
214     }
215
216     axes = new float[3][3];
217     initAxes();
218
219     findCentre();
220     findWidth();
221
222     scale = findScale();
223     if (first)
224     {
225
226       addMouseListener(this);
227
228       addMouseMotionListener(this);
229     }
230     first = false;
231   }
232
233   public void initAxes()
234   {
235     for (int i = 0; i < 3; i++)
236     {
237       for (int j = 0; j < 3; j++)
238       {
239         if (i != j)
240         {
241           axes[i][j] = 0;
242         }
243         else
244         {
245           axes[i][j] = 1;
246         }
247       }
248     }
249   }
250
251   /**
252    * DOCUMENT ME!
253    */
254   public void findWidth()
255   {
256     max = new float[3];
257     min = new float[3];
258
259     max[0] = (float) -1e30;
260     max[1] = (float) -1e30;
261     max[2] = (float) -1e30;
262
263     min[0] = (float) 1e30;
264     min[1] = (float) 1e30;
265     min[2] = (float) 1e30;
266
267     for (int i = 0; i < 3; i++)
268     {
269       for (int j = 0; j < npoint; j++)
270       {
271         SequencePoint sp = points.elementAt(j);
272
273         if (sp.coord[i] >= max[i])
274         {
275           max[i] = sp.coord[i];
276         }
277
278         if (sp.coord[i] <= min[i])
279         {
280           min[i] = sp.coord[i];
281         }
282       }
283     }
284
285     // System.out.println("xmax " + max[0] + " min " + min[0]);
286     // System.out.println("ymax " + max[1] + " min " + min[1]);
287     // System.out.println("zmax " + max[2] + " min " + min[2]);
288     width[0] = Math.abs(max[0] - min[0]);
289     width[1] = Math.abs(max[1] - min[1]);
290     width[2] = Math.abs(max[2] - min[2]);
291
292     maxwidth = width[0];
293
294     if (width[1] > width[0])
295     {
296       maxwidth = width[1];
297     }
298
299     if (width[2] > width[1])
300     {
301       maxwidth = width[2];
302     }
303
304     // System.out.println("Maxwidth = " + maxwidth);
305   }
306
307   /**
308    * DOCUMENT ME!
309    * 
310    * @return DOCUMENT ME!
311    */
312   public float findScale()
313   {
314     int dim;
315     int w;
316     int height;
317
318     if (getWidth() != 0)
319     {
320       w = getWidth();
321       height = getHeight();
322     }
323     else
324     {
325       w = prefsize.width;
326       height = prefsize.height;
327     }
328
329     if (w < height)
330     {
331       dim = w;
332     }
333     else
334     {
335       dim = height;
336     }
337
338     return (dim * scalefactor) / (2 * maxwidth);
339   }
340
341   /**
342    * DOCUMENT ME!
343    */
344   public void findCentre()
345   {
346     // Find centre coordinate
347     findWidth();
348
349     centre[0] = (max[0] + min[0]) / 2;
350     centre[1] = (max[1] + min[1]) / 2;
351     centre[2] = (max[2] + min[2]) / 2;
352
353     // System.out.println("Centre x " + centre[0]);
354     // System.out.println("Centre y " + centre[1]);
355     // System.out.println("Centre z " + centre[2]);
356   }
357
358   /**
359    * DOCUMENT ME!
360    * 
361    * @return DOCUMENT ME!
362    */
363   @Override
364   public Dimension getPreferredSize()
365   {
366     if (prefsize != null)
367     {
368       return prefsize;
369     }
370     else
371     {
372       return new Dimension(400, 400);
373     }
374   }
375
376   /**
377    * DOCUMENT ME!
378    * 
379    * @return DOCUMENT ME!
380    */
381   @Override
382   public Dimension getMinimumSize()
383   {
384     return getPreferredSize();
385   }
386
387   /**
388    * DOCUMENT ME!
389    * 
390    * @param g
391    *          DOCUMENT ME!
392    */
393   @Override
394   public void paintComponent(Graphics g1)
395   {
396
397     Graphics2D g = (Graphics2D) g1;
398
399     g.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
400             RenderingHints.VALUE_ANTIALIAS_ON);
401     if (points == null)
402     {
403       g.setFont(new Font("Verdana", Font.PLAIN, 18));
404       g.drawString(
405               MessageManager.getString("label.calculating_pca") + "....",
406               20, getHeight() / 2);
407     }
408     else
409     {
410       // Only create the image at the beginning -
411       if ((img == null) || (prefsize.width != getWidth())
412               || (prefsize.height != getHeight()))
413       {
414         prefsize.width = getWidth();
415         prefsize.height = getHeight();
416
417         scale = findScale();
418
419         // System.out.println("New scale = " + scale);
420         img = createImage(getWidth(), getHeight());
421         ig = img.getGraphics();
422       }
423
424       drawBackground(ig, bgColour);
425       drawScene(ig);
426
427       if (drawAxes)
428       {
429         drawAxes(ig);
430       }
431
432       g.drawImage(img, 0, 0, this);
433     }
434   }
435
436   /**
437    * DOCUMENT ME!
438    * 
439    * @param g
440    *          DOCUMENT ME!
441    */
442   public void drawAxes(Graphics g)
443   {
444
445     g.setColor(Color.yellow);
446
447     for (int i = 0; i < 3; i++)
448     {
449       g.drawLine(getWidth() / 2, getHeight() / 2,
450               (int) ((axes[i][0] * scale * max[0]) + (getWidth() / 2)),
451               (int) ((axes[i][1] * scale * max[1]) + (getHeight() / 2)));
452     }
453   }
454
455   /**
456    * DOCUMENT ME!
457    * 
458    * @param g
459    *          DOCUMENT ME!
460    * @param col
461    *          DOCUMENT ME!
462    */
463   public void drawBackground(Graphics g, Color col)
464   {
465     g.setColor(col);
466     g.fillRect(0, 0, prefsize.width, prefsize.height);
467   }
468
469   /**
470    * DOCUMENT ME!
471    * 
472    * @param g
473    *          DOCUMENT ME!
474    */
475   public void drawScene(Graphics g1)
476   {
477
478     Graphics2D g = (Graphics2D) g1;
479
480     g.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
481             RenderingHints.VALUE_ANTIALIAS_ON);
482
483     int halfwidth = getWidth() / 2;
484     int halfheight = getHeight() / 2;
485
486     for (int i = 0; i < npoint; i++)
487     {
488       SequencePoint sp = points.elementAt(i);
489       int x = (int) ((sp.coord[0] - centre[0]) * scale) + halfwidth;
490       int y = (int) ((sp.coord[1] - centre[1]) * scale)
491               + halfheight;
492       float z = sp.coord[1] - centre[2];
493
494       SequenceI sequence = sp.getSequence();
495       if (av.getSequenceColour(sequence) == Color.black)
496       {
497         g.setColor(Color.white);
498       }
499       else
500       {
501         g.setColor(av.getSequenceColour(sequence));
502       }
503
504       if (av.getSelectionGroup() != null)
505       {
506         if (av.getSelectionGroup().getSequences(null)
507                 .contains(sequence))
508         {
509           g.setColor(Color.gray);
510         }
511       }
512
513       if (z < 0)
514       {
515         g.setColor(g.getColor().darker());
516       }
517
518       g.fillRect(x - 3, y - 3, 6, 6);
519       if (showLabels)
520       {
521         g.setColor(Color.red);
522         g.drawString(sequence.getName(), x - 3, y - 4);
523       }
524     }
525
526     // //Now the rectangle
527     // if (rectx2 != -1 && recty2 != -1) {
528     // g.setColor(Color.white);
529     //
530     // g.drawRect(rectx1,recty1,rectx2-rectx1,recty2-recty1);
531     // }
532   }
533
534   /**
535    * DOCUMENT ME!
536    * 
537    * @return DOCUMENT ME!
538    */
539   public Dimension minimumsize()
540   {
541     return prefsize;
542   }
543
544   /**
545    * DOCUMENT ME!
546    * 
547    * @return DOCUMENT ME!
548    */
549   public Dimension preferredsize()
550   {
551     return prefsize;
552   }
553
554   /**
555    * DOCUMENT ME!
556    * 
557    * @param evt
558    *          DOCUMENT ME!
559    */
560   @Override
561   public void keyTyped(KeyEvent evt)
562   {
563   }
564
565   /**
566    * DOCUMENT ME!
567    * 
568    * @param evt
569    *          DOCUMENT ME!
570    */
571   @Override
572   public void keyReleased(KeyEvent evt)
573   {
574   }
575
576   /**
577    * DOCUMENT ME!
578    * 
579    * @param evt
580    *          DOCUMENT ME!
581    */
582   @Override
583   public void keyPressed(KeyEvent evt)
584   {
585     if (evt.getKeyCode() == KeyEvent.VK_UP)
586     {
587       scalefactor = (float) (scalefactor * 1.1);
588       scale = findScale();
589     }
590     else if (evt.getKeyCode() == KeyEvent.VK_DOWN)
591     {
592       scalefactor = (float) (scalefactor * 0.9);
593       scale = findScale();
594     }
595     else if (evt.getKeyChar() == 's')
596     {
597       Cache.log.warn("DEBUG: Rectangle selection");
598       // todo not yet enabled as rectx2, recty2 are always -1
599       // need to set them in mouseDragged
600       if ((rectx2 != -1) && (recty2 != -1))
601       {
602         rectSelect(rectx1, recty1, rectx2, recty2);
603       }
604     }
605
606     repaint();
607   }
608
609   @Override
610   public void mouseClicked(MouseEvent evt)
611   {
612   }
613
614   @Override
615   public void mouseEntered(MouseEvent evt)
616   {
617   }
618
619   @Override
620   public void mouseExited(MouseEvent evt)
621   {
622   }
623
624   @Override
625   public void mouseReleased(MouseEvent evt)
626   {
627   }
628
629   /**
630    * If the mouse press is at (within 2 pixels of) a sequence point, toggles
631    * (adds or removes) the corresponding sequence as a member of the viewport
632    * selection group. This supports configuring a group in the alignment by
633    * clicking on points in the PCA display.
634    */
635   @Override
636   public void mousePressed(MouseEvent evt)
637   {
638     int x = evt.getX();
639     int y = evt.getY();
640
641     mx = x;
642     my = y;
643
644     omx = mx;
645     omy = my;
646
647     startx = x;
648     starty = y;
649
650     rectx1 = x;
651     recty1 = y;
652
653     rectx2 = -1;
654     recty2 = -1;
655
656     SequenceI found = findSequenceAtPoint(x, y);
657
658     if (found != null)
659     {
660       AlignmentPanel[] aps = getAssociatedPanels();
661
662       for (int a = 0; a < aps.length; a++)
663       {
664         if (aps[a].av.getSelectionGroup() != null)
665         {
666           aps[a].av.getSelectionGroup().addOrRemove(found, true);
667         }
668         else
669         {
670           aps[a].av.setSelectionGroup(new SequenceGroup());
671           aps[a].av.getSelectionGroup().addOrRemove(found, true);
672           aps[a].av.getSelectionGroup()
673                   .setEndRes(aps[a].av.getAlignment().getWidth() - 1);
674         }
675       }
676       PaintRefresher.Refresh(this, av.getSequenceSetId());
677       // canonical selection is sent to other listeners
678       av.sendSelection();
679     }
680
681     repaint();
682   }
683
684   /**
685    * Sets the tooltip to the name of the sequence within 2 pixels of the mouse
686    * position, or clears the tooltip if none found
687    */
688   @Override
689   public void mouseMoved(MouseEvent evt)
690   {
691     SequenceI found = findSequenceAtPoint(evt.getX(), evt.getY());
692
693     this.setToolTipText(found == null ? null : found.getName());
694   }
695
696   /**
697    * DOCUMENT ME!
698    * 
699    * @param evt
700    *          DOCUMENT ME!
701    */
702   @Override
703   public void mouseDragged(MouseEvent evt)
704   {
705     mx = evt.getX();
706     my = evt.getY();
707
708     // Check if this is a rectangle drawing drag
709     if ((evt.getModifiers() & InputEvent.BUTTON2_MASK) != 0)
710     {
711       // rectx2 = evt.getX();
712       // recty2 = evt.getY();
713     }
714     else
715     {
716       rotmat.setIdentity();
717
718       rotmat.rotate(my - omy, 'x');
719       rotmat.rotate(mx - omx, 'y');
720
721       for (int i = 0; i < npoint; i++)
722       {
723         SequencePoint sp = points.elementAt(i);
724         sp.coord[0] -= centre[0];
725         sp.coord[1] -= centre[1];
726         sp.coord[2] -= centre[2];
727
728         // Now apply the rotation matrix
729         sp.coord = rotmat.vectorMultiply(sp.coord);
730
731         // Now translate back again
732         sp.coord[0] += centre[0];
733         sp.coord[1] += centre[1];
734         sp.coord[2] += centre[2];
735       }
736
737       for (int i = 0; i < 3; i++)
738       {
739         axes[i] = rotmat.vectorMultiply(axes[i]);
740       }
741
742       omx = mx;
743       omy = my;
744
745       paint(this.getGraphics());
746     }
747   }
748
749   /**
750    * Adds any sequences whose displayed points are within the given rectangle to
751    * the viewport's current selection. Intended for key 's' after dragging to
752    * select a region of the PCA.
753    * 
754    * @param x1
755    * @param y1
756    * @param x2
757    * @param y2
758    */
759   public void rectSelect(int x1, int y1, int x2, int y2)
760   {
761     for (int i = 0; i < npoint; i++)
762     {
763       SequencePoint sp = points.elementAt(i);
764       int tmp1 = (int) (((sp.coord[0] - centre[0]) * scale)
765               + (getWidth() / 2.0));
766       int tmp2 = (int) (((sp.coord[1] - centre[1]) * scale)
767               + (getHeight() / 2.0));
768
769       if ((tmp1 > x1) && (tmp1 < x2) && (tmp2 > y1) && (tmp2 < y2))
770       {
771         if (av != null)
772         {
773           SequenceI sequence = sp.getSequence();
774           if (!av.getSelectionGroup().getSequences(null)
775                   .contains(sequence))
776           {
777             av.getSelectionGroup().addSequence(sequence, true);
778           }
779         }
780       }
781     }
782   }
783
784   /**
785    * Answers the first sequence found whose point on the display is within 2
786    * pixels of the given coordinates, or null if none is found
787    * 
788    * @param x
789    * @param y
790    * 
791    * @return
792    */
793   public SequenceI findSequenceAtPoint(int x, int y)
794   {
795     int halfwidth = getWidth() / 2;
796     int halfheight = getHeight() / 2;
797
798     int found = -1;
799
800     for (int i = 0; i < npoint; i++)
801     {
802       SequencePoint sp = points.elementAt(i);
803       int px = (int) ((sp.coord[0] - centre[0]) * scale)
804               + halfwidth;
805       int py = (int) ((sp.coord[1] - centre[1]) * scale)
806               + halfheight;
807
808       if ((Math.abs(px - x) < 3) && (Math.abs(py - y) < 3))
809       {
810         found = i;
811       }
812     }
813
814     if (found != -1)
815     {
816       return points.elementAt(found).getSequence();
817     }
818     else
819     {
820       return null;
821     }
822   }
823
824   AlignmentPanel[] getAssociatedPanels()
825   {
826     if (applyToAllViews)
827     {
828       return PaintRefresher.getAssociatedPanels(av.getSequenceSetId());
829     }
830     else
831     {
832       return new AlignmentPanel[] { ap };
833     }
834   }
835
836   /**
837    * 
838    * @return x,y,z positions of point s (index into points) under current
839    *         transform.
840    */
841   public double[] getPointPosition(int s)
842   {
843     double[] pts = new double[3];
844     float[] p = points.elementAt(s).coord;
845     pts[0] = p[0];
846     pts[1] = p[1];
847     pts[2] = p[2];
848     return pts;
849   }
850
851 }