JAL-1691 applet SplitFrame scrolling; pull up of 5 fields to
[jalview.git] / src / jalview / appletgui / SeqCanvas.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.appletgui;
22
23 import jalview.datamodel.AlignmentI;
24 import jalview.datamodel.SearchResults;
25 import jalview.datamodel.SequenceGroup;
26 import jalview.datamodel.SequenceI;
27 import jalview.viewmodel.AlignmentViewport;
28
29 import java.awt.Color;
30 import java.awt.FontMetrics;
31 import java.awt.Graphics;
32 import java.awt.Image;
33 import java.awt.Panel;
34
35 public class SeqCanvas extends Panel
36 {
37   FeatureRenderer fr;
38
39   SequenceRenderer sr;
40
41   Image img;
42
43   Graphics gg;
44
45   int imgWidth;
46
47   int imgHeight;
48
49   AlignViewport av;
50
51   SearchResults searchResults = null;
52
53   boolean fastPaint = false;
54
55   int cursorX = 0;
56
57   int cursorY = 0;
58
59   public SeqCanvas(AlignViewport av)
60   {
61     this.av = av;
62     fr = new FeatureRenderer(av);
63     sr = new SequenceRenderer(av);
64     PaintRefresher.Register(this, av.getSequenceSetId());
65     updateViewport();
66   }
67
68   int avcharHeight = 0, avcharWidth = 0;
69
70   private void updateViewport()
71   {
72     avcharHeight = av.getCharHeight();
73     avcharWidth = av.getCharWidth();
74   }
75
76   public AlignmentViewport getViewport()
77   {
78     return av;
79   }
80
81   public FeatureRenderer getFeatureRenderer()
82   {
83     return fr;
84   }
85
86   public SequenceRenderer getSequenceRenderer()
87   {
88     return sr;
89   }
90
91   private void drawNorthScale(Graphics g, int startx, int endx, int ypos)
92   {
93     int scalestartx = startx - startx % 10 + 10;
94
95     g.setColor(Color.black);
96
97     // NORTH SCALE
98     for (int i = scalestartx; i < endx; i += 10)
99     {
100       int value = i;
101       if (av.hasHiddenColumns())
102       {
103         value = av.getColumnSelection().adjustForHiddenColumns(value);
104       }
105
106       g.drawString(String.valueOf(value), (i - startx - 1) * avcharWidth,
107               ypos - (avcharHeight / 2));
108
109       g.drawLine(((i - startx - 1) * avcharWidth) + (avcharWidth / 2),
110               (ypos + 2) - (avcharHeight / 2),
111               ((i - startx - 1) * avcharWidth) + (avcharWidth / 2),
112               ypos - 2);
113     }
114   }
115
116   private void drawWestScale(Graphics g, int startx, int endx, int ypos)
117   {
118     FontMetrics fm = getFontMetrics(av.getFont());
119     ypos += avcharHeight;
120     if (av.hasHiddenColumns())
121     {
122       startx = av.getColumnSelection().adjustForHiddenColumns(startx);
123       endx = av.getColumnSelection().adjustForHiddenColumns(endx);
124     }
125
126     int maxwidth = av.getAlignment().getWidth();
127     if (av.hasHiddenColumns())
128     {
129       maxwidth = av.getColumnSelection().findColumnPosition(maxwidth) - 1;
130     }
131
132     // WEST SCALE
133     for (int i = 0; i < av.getAlignment().getHeight(); i++)
134     {
135       SequenceI seq = av.getAlignment().getSequenceAt(i);
136       int index = startx;
137       int value = -1;
138
139       while (index < endx)
140       {
141         if (jalview.util.Comparison.isGap(seq.getCharAt(index)))
142         {
143           index++;
144
145           continue;
146         }
147
148         value = av.getAlignment().getSequenceAt(i).findPosition(index);
149
150         break;
151       }
152
153       if (value != -1)
154       {
155         int x = LABEL_WEST - fm.stringWidth(String.valueOf(value))
156                 - avcharWidth / 2;
157         g.drawString(value + "", x, (ypos + (i * avcharHeight))
158                 - (avcharHeight / 5));
159       }
160     }
161   }
162
163   private void drawEastScale(Graphics g, int startx, int endx, int ypos)
164   {
165     ypos += avcharHeight;
166
167     if (av.hasHiddenColumns())
168     {
169       endx = av.getColumnSelection().adjustForHiddenColumns(endx);
170     }
171
172     SequenceI seq;
173     // EAST SCALE
174     for (int i = 0; i < av.getAlignment().getHeight(); i++)
175     {
176       seq = av.getAlignment().getSequenceAt(i);
177       int index = endx;
178       int value = -1;
179
180       while (index > startx)
181       {
182         if (jalview.util.Comparison.isGap(seq.getCharAt(index)))
183         {
184           index--;
185
186           continue;
187         }
188
189         value = seq.findPosition(index);
190
191         break;
192       }
193
194       if (value != -1)
195       {
196         g.drawString(String.valueOf(value), 0, (ypos + (i * avcharHeight))
197                 - (avcharHeight / 5));
198       }
199     }
200   }
201
202   int lastsr = 0;
203
204   void fastPaint(int horizontal, int vertical)
205   {
206     if (fastPaint || gg == null)
207     {
208       return;
209     }
210
211     updateViewport();
212
213     // Its possible on certain browsers that the call to fastpaint
214     // is faster than it can paint, so this check here catches
215     // this possibility
216     if (lastsr + horizontal != av.startRes)
217     {
218       horizontal = av.startRes - lastsr;
219     }
220
221     lastsr = av.startRes;
222
223     fastPaint = true;
224     gg.copyArea(horizontal * avcharWidth, vertical * avcharHeight, imgWidth
225             - horizontal * avcharWidth,
226             imgHeight - vertical * avcharHeight, -horizontal * avcharWidth,
227             -vertical * avcharHeight);
228
229     int sr = av.startRes, er = av.endRes, ss = av.startSeq, es = av.endSeq, transX = 0, transY = 0;
230
231     if (horizontal > 0) // scrollbar pulled right, image to the left
232     {
233       transX = (er - sr - horizontal) * avcharWidth;
234       sr = er - horizontal;
235     }
236     else if (horizontal < 0)
237     {
238       er = sr - horizontal;
239     }
240
241     else if (vertical > 0) // scroll down
242     {
243       ss = es - vertical;
244       if (ss < av.startSeq) // ie scrolling too fast, more than a page at a time
245       {
246         ss = av.startSeq;
247       }
248       else
249       {
250         transY = imgHeight - vertical * avcharHeight;
251       }
252     }
253     else if (vertical < 0)
254     {
255       es = ss - vertical;
256       if (es > av.endSeq)
257       {
258         es = av.endSeq;
259       }
260     }
261
262     gg.translate(transX, transY);
263
264     drawPanel(gg, sr, er, ss, es, 0);
265     gg.translate(-transX, -transY);
266
267     repaint();
268
269   }
270
271   /**
272    * Definitions of startx and endx (hopefully): SMJS This is what I'm working
273    * towards! startx is the first residue (starting at 0) to display. endx is
274    * the last residue to display (starting at 0). starty is the first sequence
275    * to display (starting at 0). endy is the last sequence to display (starting
276    * at 0). NOTE 1: The av limits are set in setFont in this class and in the
277    * adjustment listener in SeqPanel when the scrollbars move.
278    */
279   public void update(Graphics g)
280   {
281     paint(g);
282   }
283
284   @Override
285   public void paint(Graphics g)
286   {
287
288     if (img != null
289             && (fastPaint || (getSize().width != g.getClipBounds().width) || (getSize().height != g
290                     .getClipBounds().height)))
291     {
292       g.drawImage(img, 0, 0, this);
293       fastPaint = false;
294       return;
295     }
296
297     if (fastPaint)
298     {
299       g.drawImage(img, 0, 0, this);
300       fastPaint = false;
301       return;
302     }
303
304     updateViewport();
305     // this draws the whole of the alignment
306     imgWidth = this.getSize().width;
307     imgHeight = this.getSize().height;
308
309     imgWidth -= imgWidth % avcharWidth;
310     imgHeight -= imgHeight % avcharHeight;
311
312     if (imgWidth < 1 || imgHeight < 1)
313     {
314       return;
315     }
316
317     if (img == null || imgWidth != img.getWidth(this)
318             || imgHeight != img.getHeight(this))
319     {
320       img = createImage(imgWidth, imgHeight);
321       gg = img.getGraphics();
322       gg.setFont(av.getFont());
323     }
324
325     gg.setColor(Color.white);
326     gg.fillRect(0, 0, imgWidth, imgHeight);
327
328     if (av.getWrapAlignment())
329     {
330       drawWrappedPanel(gg, imgWidth, imgHeight, av.startRes);
331     }
332     else
333     {
334       drawPanel(gg, av.startRes, av.endRes, av.startSeq, av.endSeq, 0);
335     }
336
337     g.drawImage(img, 0, 0, this);
338
339   }
340
341   int LABEL_WEST, LABEL_EAST;
342
343   public int getWrappedCanvasWidth(int cwidth)
344   {
345     cwidth -= cwidth % av.getCharWidth();
346
347     FontMetrics fm = getFontMetrics(av.getFont());
348
349     LABEL_EAST = 0;
350     LABEL_WEST = 0;
351
352     if (av.getScaleRightWrapped())
353     {
354       LABEL_EAST = fm.stringWidth(getMask());
355     }
356
357     if (av.getScaleLeftWrapped())
358     {
359       LABEL_WEST = fm.stringWidth(getMask());
360     }
361
362     return (cwidth - LABEL_EAST - LABEL_WEST) / av.getCharWidth();
363   }
364
365   /**
366    * Generates a string of zeroes.
367    * 
368    * @return String
369    */
370   String getMask()
371   {
372     String mask = "0";
373     int maxWidth = 0;
374     int tmp;
375     AlignmentI alignment = av.getAlignment();
376     for (int i = 0; i < alignment.getHeight(); i++)
377     {
378       tmp = alignment.getSequenceAt(i).getEnd();
379       if (tmp > maxWidth)
380       {
381         maxWidth = tmp;
382       }
383     }
384
385     for (int i = maxWidth; i > 0; i /= 10)
386     {
387       mask += "0";
388     }
389     return mask;
390   }
391
392   private void drawWrappedPanel(Graphics g, int canvasWidth,
393           int canvasHeight, int startRes)
394   {
395     AlignmentI al = av.getAlignment();
396
397     FontMetrics fm = getFontMetrics(av.getFont());
398
399     if (av.getScaleRightWrapped())
400     {
401       LABEL_EAST = fm.stringWidth(getMask());
402     }
403
404     if (av.getScaleLeftWrapped())
405     {
406       LABEL_WEST = fm.stringWidth(getMask());
407     }
408
409     int hgap = avcharHeight;
410     if (av.getScaleAboveWrapped())
411     {
412       hgap += avcharHeight;
413     }
414
415     int cWidth = (canvasWidth - LABEL_EAST - LABEL_WEST) / avcharWidth;
416     int cHeight = av.getAlignment().getHeight() * avcharHeight;
417
418     av.setWrappedWidth(cWidth);
419
420     av.endRes = av.startRes + cWidth;
421
422     int endx;
423     int ypos = hgap;
424
425     int maxwidth = av.getAlignment().getWidth() - 1;
426
427     if (av.hasHiddenColumns())
428     {
429       maxwidth = av.getColumnSelection().findColumnPosition(maxwidth) - 1;
430     }
431
432     while ((ypos <= canvasHeight) && (startRes < maxwidth))
433     {
434       endx = startRes + cWidth - 1;
435
436       if (endx > maxwidth)
437       {
438         endx = maxwidth;
439       }
440
441       g.setColor(Color.black);
442
443       if (av.getScaleLeftWrapped())
444       {
445         drawWestScale(g, startRes, endx, ypos);
446       }
447
448       if (av.getScaleRightWrapped())
449       {
450         g.translate(canvasWidth - LABEL_EAST, 0);
451         drawEastScale(g, startRes, endx, ypos);
452         g.translate(-(canvasWidth - LABEL_EAST), 0);
453       }
454
455       g.translate(LABEL_WEST, 0);
456
457       if (av.getScaleAboveWrapped())
458       {
459         drawNorthScale(g, startRes, endx, ypos);
460       }
461       if (av.hasHiddenColumns() && av.getShowHiddenMarkers())
462       {
463         g.setColor(Color.blue);
464         int res;
465         for (int i = 0; i < av.getColumnSelection().getHiddenColumns()
466                 .size(); i++)
467         {
468           res = av.getColumnSelection().findHiddenRegionPosition(i)
469                   - startRes;
470
471           if (res < 0 || res > endx - startRes)
472           {
473             continue;
474           }
475
476           gg.fillPolygon(new int[]
477           { res * avcharWidth - avcharHeight / 4,
478               res * avcharWidth + avcharHeight / 4, res * avcharWidth },
479                   new int[]
480                   { ypos - (avcharHeight / 2), ypos - (avcharHeight / 2),
481                       ypos - (avcharHeight / 2) + 8 }, 3);
482
483         }
484       }
485
486       if (g.getClip() == null)
487       {
488         g.setClip(0, 0, cWidth * avcharWidth, canvasHeight);
489       }
490
491       drawPanel(g, startRes, endx, 0, al.getHeight(), ypos);
492       g.setClip(null);
493
494       if (av.isShowAnnotation())
495       {
496         g.translate(0, cHeight + ypos + 4);
497         if (annotations == null)
498         {
499           annotations = new AnnotationPanel(av);
500         }
501
502         annotations.drawComponent(g, startRes, endx + 1);
503         g.translate(0, -cHeight - ypos - 4);
504       }
505       g.translate(-LABEL_WEST, 0);
506
507       ypos += cHeight + getAnnotationHeight() + hgap;
508
509       startRes += cWidth;
510     }
511
512   }
513
514   AnnotationPanel annotations;
515
516   int getAnnotationHeight()
517   {
518     if (!av.isShowAnnotation())
519     {
520       return 0;
521     }
522
523     if (annotations == null)
524     {
525       annotations = new AnnotationPanel(av);
526     }
527
528     return annotations.adjustPanelHeight();
529   }
530
531   private void drawPanel(Graphics g1, int startRes, int endRes,
532           int startSeq,
533           int endSeq, int offset)
534   {
535
536
537     if (!av.hasHiddenColumns())
538     {
539       draw(g1, startRes, endRes, startSeq, endSeq, offset);
540     }
541     else
542     {
543
544       int screenY = 0;
545       int blockStart = startRes;
546       int blockEnd = endRes;
547
548       if (av.hasHiddenColumns())
549       {
550         for (int[] region : av.getColumnSelection().getHiddenColumns())
551         {
552           int hideStart = region[0];
553           int hideEnd = region[1];
554
555           if (hideStart <= blockStart)
556           {
557             blockStart += (hideEnd - hideStart) + 1;
558             continue;
559           }
560
561           blockEnd = hideStart - 1;
562
563           g1.translate(screenY * avcharWidth, 0);
564
565           draw(g1, blockStart, blockEnd, startSeq, endSeq, offset);
566
567           if (av.getShowHiddenMarkers())
568           {
569             g1.setColor(Color.blue);
570             g1.drawLine((blockEnd - blockStart + 1) * avcharWidth - 1,
571                     0 + offset, (blockEnd - blockStart + 1) * avcharWidth
572                             - 1, (endSeq - startSeq) * avcharHeight
573                             + offset);
574           }
575
576           g1.translate(-screenY * avcharWidth, 0);
577           screenY += blockEnd - blockStart + 1;
578           blockStart = hideEnd + 1;
579         }
580       }
581       if (screenY <= (endRes - startRes))
582       {
583         blockEnd = blockStart + (endRes - startRes) - screenY;
584         g1.translate(screenY * avcharWidth, 0);
585         draw(g1, blockStart, blockEnd, startSeq, endSeq, offset);
586
587         g1.translate(-screenY * avcharWidth, 0);
588       }
589     }
590
591   }
592
593   // int startRes, int endRes, int startSeq, int endSeq, int x, int y,
594   // int x1, int x2, int y1, int y2, int startx, int starty,
595   void draw(Graphics g, int startRes, int endRes, int startSeq, int endSeq,
596           int offset)
597   {
598     g.setFont(av.getFont());
599     sr.prepare(g, av.isRenderGaps());
600     updateViewport();
601     SequenceI nextSeq;
602
603     // / First draw the sequences
604     // ///////////////////////////
605     for (int i = startSeq; i < endSeq; i++)
606     {
607       nextSeq = av.getAlignment().getSequenceAt(i);
608
609       if (nextSeq == null)
610       {
611         continue;
612       }
613
614       sr.drawSequence(nextSeq, av.getAlignment().findAllGroups(nextSeq),
615               startRes, endRes, offset + ((i - startSeq) * avcharHeight));
616
617       if (av.isShowSequenceFeatures())
618       {
619         fr.drawSequence(g, nextSeq, startRes, endRes, offset
620                 + ((i - startSeq) * avcharHeight));
621       }
622
623       // / Highlight search Results once all sequences have been drawn
624       // ////////////////////////////////////////////////////////
625       if (searchResults != null)
626       {
627         int[] visibleResults = searchResults.getResults(nextSeq, startRes,
628                 endRes);
629         if (visibleResults != null)
630         {
631           for (int r = 0; r < visibleResults.length; r += 2)
632           {
633             sr.drawHighlightedText(nextSeq, visibleResults[r],
634                     visibleResults[r + 1], (visibleResults[r] - startRes)
635                             * avcharWidth, offset
636                             + ((i - startSeq) * avcharHeight));
637           }
638         }
639       }
640
641       if (av.cursorMode && cursorY == i && cursorX >= startRes
642               && cursorX <= endRes)
643       {
644         sr.drawCursor(nextSeq, cursorX, (cursorX - startRes) * avcharWidth,
645                 offset + ((i - startSeq) * avcharHeight));
646       }
647     }
648
649     if (av.getSelectionGroup() != null
650             || av.getAlignment().getGroups().size() > 0)
651     {
652       drawGroupsBoundaries(g, startRes, endRes, startSeq, endSeq, offset);
653     }
654
655   }
656
657   private void drawGroupsBoundaries(Graphics g, int startRes, int endRes,
658           int startSeq, int endSeq, int offset)
659   {
660     //
661     // ///////////////////////////////////
662     // Now outline any areas if necessary
663     // ///////////////////////////////////
664     SequenceGroup group = av.getSelectionGroup();
665
666     int sx = -1;
667     int sy = -1;
668     int ex = -1;
669     int groupIndex = -1;
670
671     if ((group == null) && (av.getAlignment().getGroups().size() > 0))
672     {
673       group = av.getAlignment().getGroups().get(0);
674       groupIndex = 0;
675     }
676
677     if (group != null)
678     {
679       do
680       {
681         int oldY = -1;
682         int i = 0;
683         boolean inGroup = false;
684         int top = -1;
685         int bottom = -1;
686         int alHeight = av.getAlignment().getHeight() - 1;
687
688         for (i = startSeq; i < endSeq; i++)
689         {
690           sx = (group.getStartRes() - startRes) * avcharWidth;
691           sy = offset + ((i - startSeq) * avcharHeight);
692           ex = (((group.getEndRes() + 1) - group.getStartRes()) * avcharWidth) - 1;
693
694           if (sx + ex < 0 || sx > imgWidth)
695           {
696             continue;
697           }
698
699           if ((sx <= (endRes - startRes) * avcharWidth)
700                   && group.getSequences(null).contains(
701                           av.getAlignment().getSequenceAt(i)))
702           {
703             if ((bottom == -1)
704                     && (i >= alHeight || !group.getSequences(null)
705                             .contains(
706                                     av.getAlignment().getSequenceAt(i + 1))))
707             {
708               bottom = sy + avcharHeight;
709             }
710
711             if (!inGroup)
712             {
713               if (((top == -1) && (i == 0))
714                       || !group.getSequences(null).contains(
715                               av.getAlignment().getSequenceAt(i - 1)))
716               {
717                 top = sy;
718               }
719
720               oldY = sy;
721               inGroup = true;
722
723               if (group == av.getSelectionGroup())
724               {
725                 g.setColor(Color.red);
726               }
727               else
728               {
729                 g.setColor(group.getOutlineColour());
730               }
731             }
732           }
733           else
734           {
735             if (inGroup)
736             {
737               if (sx >= 0 && sx < imgWidth)
738               {
739                 g.drawLine(sx, oldY, sx, sy);
740               }
741
742               if (sx + ex < imgWidth)
743               {
744                 g.drawLine(sx + ex, oldY, sx + ex, sy);
745               }
746
747               if (sx < 0)
748               {
749                 ex += sx;
750                 sx = 0;
751               }
752
753               if (sx + ex > imgWidth)
754               {
755                 ex = imgWidth;
756               }
757
758               else if (sx + ex >= (endRes - startRes + 1) * avcharWidth)
759               {
760                 ex = (endRes - startRes + 1) * avcharWidth;
761               }
762
763               if (top != -1)
764               {
765                 g.drawLine(sx, top, sx + ex, top);
766                 top = -1;
767               }
768
769               if (bottom != -1)
770               {
771                 g.drawLine(sx, bottom, sx + ex, bottom);
772                 bottom = -1;
773               }
774
775               inGroup = false;
776             }
777           }
778         }
779
780         if (inGroup)
781         {
782           sy = offset + ((i - startSeq) * avcharHeight);
783           if (sx >= 0 && sx < imgWidth)
784           {
785             g.drawLine(sx, oldY, sx, sy);
786           }
787
788           if (sx + ex < imgWidth)
789           {
790             g.drawLine(sx + ex, oldY, sx + ex, sy);
791           }
792
793           if (sx < 0)
794           {
795             ex += sx;
796             sx = 0;
797           }
798
799           if (sx + ex > imgWidth)
800           {
801             ex = imgWidth;
802           }
803           else if (sx + ex >= (endRes - startRes + 1) * avcharWidth)
804           {
805             ex = (endRes - startRes + 1) * avcharWidth;
806           }
807
808           if (top != -1)
809           {
810             g.drawLine(sx, top, sx + ex, top);
811             top = -1;
812           }
813
814           if (bottom != -1)
815           {
816             g.drawLine(sx, bottom - 1, sx + ex, bottom - 1);
817             bottom = -1;
818           }
819
820           inGroup = false;
821         }
822
823         groupIndex++;
824
825         if (groupIndex >= av.getAlignment().getGroups().size())
826         {
827           break;
828         }
829
830         group = av.getAlignment().getGroups()
831                 .get(groupIndex);
832       } while (groupIndex < av.getAlignment().getGroups().size());
833
834     }
835   }
836
837   public void highlightSearchResults(SearchResults results)
838   {
839     searchResults = results;
840
841     repaint();
842   }
843
844 }