JAL-1767 refactoring and tidying of RotatableCanvas and related
[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     int halfwidth = getWidth() / 2;
431     int halfheight = getHeight() / 2;
432
433     for (int i = 0; i < npoint; i++)
434     {
435       SequencePoint sp = points.elementAt(i);
436       int x = (int) ((sp.coord.x - centre.x) * scale) + halfwidth;
437       int y = (int) ((sp.coord.y - centre.y) * scale)
438               + halfheight;
439       float z = sp.coord.y - centre.z; // todo sp.coord.z JAL-2963
440
441       SequenceI sequence = sp.getSequence();
442       if (av.getSequenceColour(sequence) == Color.black)
443       {
444         g.setColor(Color.white);
445       }
446       else
447       {
448         g.setColor(av.getSequenceColour(sequence));
449       }
450
451       if (av.getSelectionGroup() != null)
452       {
453         if (av.getSelectionGroup().getSequences(null)
454                 .contains(sequence))
455         {
456           g.setColor(Color.gray);
457         }
458       }
459
460       if (z < 0)
461       {
462         g.setColor(g.getColor().darker());
463       }
464
465       g.fillRect(x - 3, y - 3, 6, 6);
466       if (showLabels)
467       {
468         g.setColor(Color.red);
469         g.drawString(sequence.getName(), x - 3, y - 4);
470       }
471     }
472
473     // //Now the rectangle
474     // if (rectx2 != -1 && recty2 != -1) {
475     // g.setColor(Color.white);
476     //
477     // g.drawRect(rectx1,recty1,rectx2-rectx1,recty2-recty1);
478     // }
479   }
480
481   @Override
482   public void keyTyped(KeyEvent evt)
483   {
484   }
485
486   @Override
487   public void keyReleased(KeyEvent evt)
488   {
489   }
490
491   /**
492    * Responds to up or down arrow key by zooming in or out, respectively
493    * 
494    * @param evt
495    */
496   @Override
497   public void keyPressed(KeyEvent evt)
498   {
499     if (evt.getKeyCode() == KeyEvent.VK_UP)
500     {
501       scalefactor = (float) (scalefactor * 1.1);
502       scale = findScale();
503     }
504     else if (evt.getKeyCode() == KeyEvent.VK_DOWN)
505     {
506       scalefactor = (float) (scalefactor * 0.9);
507       scale = findScale();
508     }
509     else if (evt.getKeyChar() == 's')
510     {
511       // Cache.log.warn("DEBUG: Rectangle selection");
512       // todo not yet enabled as rectx2, recty2 are always -1
513       // need to set them in mouseDragged
514       if ((rectx2 != -1) && (recty2 != -1))
515       {
516         rectSelect(rectx1, recty1, rectx2, recty2);
517       }
518     }
519
520     repaint();
521   }
522
523   @Override
524   public void mouseClicked(MouseEvent evt)
525   {
526   }
527
528   @Override
529   public void mouseEntered(MouseEvent evt)
530   {
531   }
532
533   @Override
534   public void mouseExited(MouseEvent evt)
535   {
536   }
537
538   @Override
539   public void mouseReleased(MouseEvent evt)
540   {
541   }
542
543   /**
544    * If the mouse press is at (within 2 pixels of) a sequence point, toggles
545    * (adds or removes) the corresponding sequence as a member of the viewport
546    * selection group. This supports configuring a group in the alignment by
547    * clicking on points in the PCA display.
548    */
549   @Override
550   public void mousePressed(MouseEvent evt)
551   {
552     int x = evt.getX();
553     int y = evt.getY();
554
555     mouseX = x;
556     mouseY = y;
557
558     startx = x;
559     starty = y;
560
561     rectx1 = x;
562     recty1 = y;
563
564     rectx2 = -1;
565     recty2 = -1;
566
567     SequenceI found = findSequenceAtPoint(x, y);
568
569     if (found != null)
570     {
571       AlignmentPanel[] aps = getAssociatedPanels();
572
573       for (int a = 0; a < aps.length; a++)
574       {
575         if (aps[a].av.getSelectionGroup() != null)
576         {
577           aps[a].av.getSelectionGroup().addOrRemove(found, true);
578         }
579         else
580         {
581           aps[a].av.setSelectionGroup(new SequenceGroup());
582           aps[a].av.getSelectionGroup().addOrRemove(found, true);
583           aps[a].av.getSelectionGroup()
584                   .setEndRes(aps[a].av.getAlignment().getWidth() - 1);
585         }
586       }
587       PaintRefresher.Refresh(this, av.getSequenceSetId());
588       // canonical selection is sent to other listeners
589       av.sendSelection();
590     }
591
592     repaint();
593   }
594
595   /**
596    * Sets the tooltip to the name of the sequence within 2 pixels of the mouse
597    * position, or clears the tooltip if none found
598    */
599   @Override
600   public void mouseMoved(MouseEvent evt)
601   {
602     SequenceI found = findSequenceAtPoint(evt.getX(), evt.getY());
603
604     this.setToolTipText(found == null ? null : found.getName());
605   }
606
607   /**
608    * Action handler for a mouse drag. Rotates the display around the X axis (for
609    * up/down mouse movement) and/or the Y axis (for left/right mouse movement).
610    * 
611    * @param evt
612    */
613   @Override
614   public void mouseDragged(MouseEvent evt)
615   {
616     int xPos = evt.getX();
617     int yPos = evt.getY();
618
619     if (xPos == mouseX && yPos == mouseY)
620     {
621       return;
622     }
623
624     // Check if this is a rectangle drawing drag
625     if ((evt.getModifiers() & InputEvent.BUTTON2_MASK) != 0)
626     {
627       // rectx2 = evt.getX();
628       // recty2 = evt.getY();
629     }
630     else
631     {
632       /*
633        * get the identity transformation...
634        */
635       RotatableMatrix rotmat = new RotatableMatrix();
636
637       /*
638        * rotate around the X axis for change in Y
639        * (mouse movement up/down); note we are equating a
640        * number of pixels with degrees of rotation here!
641        */
642       if (yPos != mouseY)
643       {
644         rotmat.rotate(yPos - mouseY, Axis.X);
645       }
646
647       /*
648        * rotate around the Y axis for change in X
649        * (mouse movement left/right)
650        */
651       if (xPos != mouseX)
652       {
653         rotmat.rotate(xPos - mouseX, Axis.Y);
654       }
655
656       /*
657        * apply the composite transformation to sequence points
658        */
659       for (int i = 0; i < npoint; i++)
660       {
661         SequencePoint sp = points.elementAt(i);
662         sp.translateBack(centre);
663
664         // Now apply the rotation matrix
665         sp.coord = rotmat.vectorMultiply(sp.coord);
666
667         // Now translate back again
668         sp.translate(centre);
669       }
670
671       /*
672        * rotate the x/y/z axis positions
673        */
674       for (int i = 0; i < DIMS; i++)
675       {
676         axisEndPoints[i] = rotmat.vectorMultiply(axisEndPoints[i]);
677       }
678
679       mouseX = xPos;
680       mouseY = yPos;
681
682       paint(this.getGraphics());
683     }
684   }
685
686   /**
687    * Adds any sequences whose displayed points are within the given rectangle to
688    * the viewport's current selection. Intended for key 's' after dragging to
689    * select a region of the PCA.
690    * 
691    * @param x1
692    * @param y1
693    * @param x2
694    * @param y2
695    */
696   public void rectSelect(int x1, int y1, int x2, int y2)
697   {
698     for (int i = 0; i < npoint; i++)
699     {
700       SequencePoint sp = points.elementAt(i);
701       int tmp1 = (int) (((sp.coord.x - centre.x) * scale)
702               + (getWidth() / 2.0));
703       int tmp2 = (int) (((sp.coord.y - centre.y) * scale)
704               + (getHeight() / 2.0));
705
706       if ((tmp1 > x1) && (tmp1 < x2) && (tmp2 > y1) && (tmp2 < y2))
707       {
708         if (av != null)
709         {
710           SequenceI sequence = sp.getSequence();
711           if (!av.getSelectionGroup().getSequences(null)
712                   .contains(sequence))
713           {
714             av.getSelectionGroup().addSequence(sequence, true);
715           }
716         }
717       }
718     }
719   }
720
721   /**
722    * Answers the first sequence found whose point on the display is within 2
723    * pixels of the given coordinates, or null if none is found
724    * 
725    * @param x
726    * @param y
727    * 
728    * @return
729    */
730   public SequenceI findSequenceAtPoint(int x, int y)
731   {
732     int halfwidth = getWidth() / 2;
733     int halfheight = getHeight() / 2;
734
735     int found = -1;
736
737     for (int i = 0; i < npoint; i++)
738     {
739       SequencePoint sp = points.elementAt(i);
740       int px = (int) ((sp.coord.x - centre.x) * scale)
741               + halfwidth;
742       int py = (int) ((sp.coord.y - centre.y) * scale)
743               + halfheight;
744
745       if ((Math.abs(px - x) < 3) && (Math.abs(py - y) < 3))
746       {
747         found = i;
748         break;
749       }
750     }
751
752     if (found != -1)
753     {
754       return points.elementAt(found).getSequence();
755     }
756     else
757     {
758       return null;
759     }
760   }
761
762   /**
763    * Answers the panel the PCA is associated with (all panels for this alignment
764    * if 'associate with all panels' is selected).
765    * 
766    * @return
767    */
768   AlignmentPanel[] getAssociatedPanels()
769   {
770     if (applyToAllViews)
771     {
772       return PaintRefresher.getAssociatedPanels(av.getSequenceSetId());
773     }
774     else
775     {
776       return new AlignmentPanel[] { ap };
777     }
778   }
779 }