JAL-1722 fix: don’t draw visible regions outside viewport view
[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   @Override
280   public void update(Graphics g)
281   {
282     paint(g);
283   }
284
285   @Override
286   public void paint(Graphics g)
287   {
288
289     if (img != null
290             && (fastPaint || (getSize().width != g.getClipBounds().width) || (getSize().height != g
291                     .getClipBounds().height)))
292     {
293       g.drawImage(img, 0, 0, this);
294       fastPaint = false;
295       return;
296     }
297
298     if (fastPaint)
299     {
300       g.drawImage(img, 0, 0, this);
301       fastPaint = false;
302       return;
303     }
304
305     updateViewport();
306     // this draws the whole of the alignment
307     imgWidth = this.getSize().width;
308     imgHeight = this.getSize().height;
309
310     imgWidth -= imgWidth % avcharWidth;
311     imgHeight -= imgHeight % avcharHeight;
312
313     if (imgWidth < 1 || imgHeight < 1)
314     {
315       return;
316     }
317
318     if (img == null || imgWidth != img.getWidth(this)
319             || imgHeight != img.getHeight(this))
320     {
321       img = createImage(imgWidth, imgHeight);
322       gg = img.getGraphics();
323       gg.setFont(av.getFont());
324     }
325
326     gg.setColor(Color.white);
327     gg.fillRect(0, 0, imgWidth, imgHeight);
328
329     if (av.getWrapAlignment())
330     {
331       drawWrappedPanel(gg, imgWidth, imgHeight, av.startRes);
332     }
333     else
334     {
335       drawPanel(gg, av.startRes, av.endRes, av.startSeq, av.endSeq, 0);
336     }
337
338     g.drawImage(img, 0, 0, this);
339
340   }
341
342   int LABEL_WEST, LABEL_EAST;
343
344   public int getWrappedCanvasWidth(int cwidth)
345   {
346     cwidth -= cwidth % av.getCharWidth();
347
348     FontMetrics fm = getFontMetrics(av.getFont());
349
350     LABEL_EAST = 0;
351     LABEL_WEST = 0;
352
353     if (av.getScaleRightWrapped())
354     {
355       LABEL_EAST = fm.stringWidth(getMask());
356     }
357
358     if (av.getScaleLeftWrapped())
359     {
360       LABEL_WEST = fm.stringWidth(getMask());
361     }
362
363     return (cwidth - LABEL_EAST - LABEL_WEST) / av.getCharWidth();
364   }
365
366   /**
367    * Generates a string of zeroes.
368    * 
369    * @return String
370    */
371   String getMask()
372   {
373     String mask = "0";
374     int maxWidth = 0;
375     int tmp;
376     AlignmentI alignment = av.getAlignment();
377     for (int i = 0; i < alignment.getHeight(); i++)
378     {
379       tmp = alignment.getSequenceAt(i).getEnd();
380       if (tmp > maxWidth)
381       {
382         maxWidth = tmp;
383       }
384     }
385
386     for (int i = maxWidth; i > 0; i /= 10)
387     {
388       mask += "0";
389     }
390     return mask;
391   }
392
393   private void drawWrappedPanel(Graphics g, int canvasWidth,
394           int canvasHeight, int startRes)
395   {
396     AlignmentI al = av.getAlignment();
397
398     FontMetrics fm = getFontMetrics(av.getFont());
399
400     if (av.getScaleRightWrapped())
401     {
402       LABEL_EAST = fm.stringWidth(getMask());
403     }
404
405     if (av.getScaleLeftWrapped())
406     {
407       LABEL_WEST = fm.stringWidth(getMask());
408     }
409
410     int hgap = avcharHeight;
411     if (av.getScaleAboveWrapped())
412     {
413       hgap += avcharHeight;
414     }
415
416     int cWidth = (canvasWidth - LABEL_EAST - LABEL_WEST) / avcharWidth;
417     int cHeight = av.getAlignment().getHeight() * avcharHeight;
418
419     av.setWrappedWidth(cWidth);
420
421     av.endRes = av.startRes + cWidth;
422
423     int endx;
424     int ypos = hgap;
425
426     int maxwidth = av.getAlignment().getWidth() - 1;
427
428     if (av.hasHiddenColumns())
429     {
430       maxwidth = av.getColumnSelection().findColumnPosition(maxwidth) - 1;
431     }
432
433     while ((ypos <= canvasHeight) && (startRes < maxwidth))
434     {
435       endx = startRes + cWidth - 1;
436
437       if (endx > maxwidth)
438       {
439         endx = maxwidth;
440       }
441
442       g.setColor(Color.black);
443
444       if (av.getScaleLeftWrapped())
445       {
446         drawWestScale(g, startRes, endx, ypos);
447       }
448
449       if (av.getScaleRightWrapped())
450       {
451         g.translate(canvasWidth - LABEL_EAST, 0);
452         drawEastScale(g, startRes, endx, ypos);
453         g.translate(-(canvasWidth - LABEL_EAST), 0);
454       }
455
456       g.translate(LABEL_WEST, 0);
457
458       if (av.getScaleAboveWrapped())
459       {
460         drawNorthScale(g, startRes, endx, ypos);
461       }
462       if (av.hasHiddenColumns() && av.getShowHiddenMarkers())
463       {
464         g.setColor(Color.blue);
465         int res;
466         for (int i = 0; i < av.getColumnSelection().getHiddenColumns()
467                 .size(); i++)
468         {
469           res = av.getColumnSelection().findHiddenRegionPosition(i)
470                   - startRes;
471
472           if (res < 0 || res > endx - startRes)
473           {
474             continue;
475           }
476
477           gg.fillPolygon(new int[] { res * avcharWidth - avcharHeight / 4,
478               res * avcharWidth + avcharHeight / 4, res * avcharWidth },
479                   new int[] { ypos - (avcharHeight / 2),
480                       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, int endSeq, int offset)
533   {
534
535     if (!av.hasHiddenColumns())
536     {
537       draw(g1, startRes, endRes, startSeq, endSeq, offset);
538     }
539     else
540     {
541
542       int screenY = 0;
543       int blockStart = startRes;
544       int blockEnd = endRes;
545
546       if (av.hasHiddenColumns())
547       {
548         for (int[] region : av.getColumnSelection().getHiddenColumns())
549         {
550           int hideStart = region[0];
551           int hideEnd = region[1];
552
553           if (hideStart <= blockStart)
554           {
555             blockStart += (hideEnd - hideStart) + 1;
556             continue;
557           }
558
559           blockEnd = hideStart - 1;
560
561           g1.translate(screenY * avcharWidth, 0);
562
563           draw(g1, blockStart, blockEnd, startSeq, endSeq, offset);
564
565           if (av.getShowHiddenMarkers())
566           {
567             g1.setColor(Color.blue);
568             g1.drawLine((blockEnd - blockStart + 1) * avcharWidth - 1,
569                     0 + offset, (blockEnd - blockStart + 1) * avcharWidth
570                             - 1, (endSeq - startSeq) * avcharHeight
571                             + offset);
572           }
573
574           g1.translate(-screenY * avcharWidth, 0);
575           screenY += blockEnd - blockStart + 1;
576           blockStart = hideEnd + 1;
577
578           if (screenY > (endRes - startRes))
579           {
580             // already rendered last block
581             return;
582           }
583         }
584       }
585       if (screenY <= (endRes - startRes))
586       {
587         // remaining visible region to render
588         blockEnd = blockStart + (endRes - startRes) - screenY;
589         g1.translate(screenY * avcharWidth, 0);
590         draw(g1, blockStart, blockEnd, startSeq, endSeq, offset);
591
592         g1.translate(-screenY * avcharWidth, 0);
593       }
594     }
595
596   }
597
598   // int startRes, int endRes, int startSeq, int endSeq, int x, int y,
599   // int x1, int x2, int y1, int y2, int startx, int starty,
600   void draw(Graphics g, int startRes, int endRes, int startSeq, int endSeq,
601           int offset)
602   {
603     g.setFont(av.getFont());
604     sr.prepare(g, av.isRenderGaps());
605     updateViewport();
606     SequenceI nextSeq;
607
608     // / First draw the sequences
609     // ///////////////////////////
610     for (int i = startSeq; i < endSeq; i++)
611     {
612       nextSeq = av.getAlignment().getSequenceAt(i);
613
614       if (nextSeq == null)
615       {
616         continue;
617       }
618
619       sr.drawSequence(nextSeq, av.getAlignment().findAllGroups(nextSeq),
620               startRes, endRes, offset + ((i - startSeq) * avcharHeight));
621
622       if (av.isShowSequenceFeatures())
623       {
624         fr.drawSequence(g, nextSeq, startRes, endRes, offset
625                 + ((i - startSeq) * avcharHeight));
626       }
627
628       // / Highlight search Results once all sequences have been drawn
629       // ////////////////////////////////////////////////////////
630       if (searchResults != null)
631       {
632         int[] visibleResults = searchResults.getResults(nextSeq, startRes,
633                 endRes);
634         if (visibleResults != null)
635         {
636           for (int r = 0; r < visibleResults.length; r += 2)
637           {
638             sr.drawHighlightedText(nextSeq, visibleResults[r],
639                     visibleResults[r + 1], (visibleResults[r] - startRes)
640                             * avcharWidth, offset
641                             + ((i - startSeq) * avcharHeight));
642           }
643         }
644       }
645
646       if (av.cursorMode && cursorY == i && cursorX >= startRes
647               && cursorX <= endRes)
648       {
649         sr.drawCursor(nextSeq, cursorX, (cursorX - startRes) * avcharWidth,
650                 offset + ((i - startSeq) * avcharHeight));
651       }
652     }
653
654     if (av.getSelectionGroup() != null
655             || av.getAlignment().getGroups().size() > 0)
656     {
657       drawGroupsBoundaries(g, startRes, endRes, startSeq, endSeq, offset);
658     }
659
660   }
661
662   private void drawGroupsBoundaries(Graphics g, int startRes, int endRes,
663           int startSeq, int endSeq, int offset)
664   {
665     //
666     // ///////////////////////////////////
667     // Now outline any areas if necessary
668     // ///////////////////////////////////
669     SequenceGroup group = av.getSelectionGroup();
670
671     int sx = -1;
672     int sy = -1;
673     int ex = -1;
674     int groupIndex = -1;
675
676     if ((group == null) && (av.getAlignment().getGroups().size() > 0))
677     {
678       group = av.getAlignment().getGroups().get(0);
679       groupIndex = 0;
680     }
681
682     if (group != null)
683     {
684       do
685       {
686         int oldY = -1;
687         int i = 0;
688         boolean inGroup = false;
689         int top = -1;
690         int bottom = -1;
691         int alHeight = av.getAlignment().getHeight() - 1;
692
693         for (i = startSeq; i < endSeq; i++)
694         {
695           sx = (group.getStartRes() - startRes) * avcharWidth;
696           sy = offset + ((i - startSeq) * avcharHeight);
697           ex = (((group.getEndRes() + 1) - group.getStartRes()) * avcharWidth) - 1;
698
699           if (sx + ex < 0 || sx > imgWidth)
700           {
701             continue;
702           }
703
704           if ((sx <= (endRes - startRes) * avcharWidth)
705                   && group.getSequences(null).contains(
706                           av.getAlignment().getSequenceAt(i)))
707           {
708             if ((bottom == -1)
709                     && (i >= alHeight || !group.getSequences(null)
710                             .contains(
711                                     av.getAlignment().getSequenceAt(i + 1))))
712             {
713               bottom = sy + avcharHeight;
714             }
715
716             if (!inGroup)
717             {
718               if (((top == -1) && (i == 0))
719                       || !group.getSequences(null).contains(
720                               av.getAlignment().getSequenceAt(i - 1)))
721               {
722                 top = sy;
723               }
724
725               oldY = sy;
726               inGroup = true;
727
728               if (group == av.getSelectionGroup())
729               {
730                 g.setColor(Color.red);
731               }
732               else
733               {
734                 g.setColor(group.getOutlineColour());
735               }
736             }
737           }
738           else
739           {
740             if (inGroup)
741             {
742               if (sx >= 0 && sx < imgWidth)
743               {
744                 g.drawLine(sx, oldY, sx, sy);
745               }
746
747               if (sx + ex < imgWidth)
748               {
749                 g.drawLine(sx + ex, oldY, sx + ex, sy);
750               }
751
752               if (sx < 0)
753               {
754                 ex += sx;
755                 sx = 0;
756               }
757
758               if (sx + ex > imgWidth)
759               {
760                 ex = imgWidth;
761               }
762
763               else if (sx + ex >= (endRes - startRes + 1) * avcharWidth)
764               {
765                 ex = (endRes - startRes + 1) * avcharWidth;
766               }
767
768               if (top != -1)
769               {
770                 g.drawLine(sx, top, sx + ex, top);
771                 top = -1;
772               }
773
774               if (bottom != -1)
775               {
776                 g.drawLine(sx, bottom, sx + ex, bottom);
777                 bottom = -1;
778               }
779
780               inGroup = false;
781             }
782           }
783         }
784
785         if (inGroup)
786         {
787           sy = offset + ((i - startSeq) * avcharHeight);
788           if (sx >= 0 && sx < imgWidth)
789           {
790             g.drawLine(sx, oldY, sx, sy);
791           }
792
793           if (sx + ex < imgWidth)
794           {
795             g.drawLine(sx + ex, oldY, sx + ex, sy);
796           }
797
798           if (sx < 0)
799           {
800             ex += sx;
801             sx = 0;
802           }
803
804           if (sx + ex > imgWidth)
805           {
806             ex = imgWidth;
807           }
808           else if (sx + ex >= (endRes - startRes + 1) * avcharWidth)
809           {
810             ex = (endRes - startRes + 1) * avcharWidth;
811           }
812
813           if (top != -1)
814           {
815             g.drawLine(sx, top, sx + ex, top);
816             top = -1;
817           }
818
819           if (bottom != -1)
820           {
821             g.drawLine(sx, bottom - 1, sx + ex, bottom - 1);
822             bottom = -1;
823           }
824
825           inGroup = false;
826         }
827
828         groupIndex++;
829
830         if (groupIndex >= av.getAlignment().getGroups().size())
831         {
832           break;
833         }
834
835         group = av.getAlignment().getGroups().get(groupIndex);
836       } while (groupIndex < av.getAlignment().getGroups().size());
837
838     }
839   }
840
841   public void highlightSearchResults(SearchResults results)
842   {
843     searchResults = results;
844
845     repaint();
846   }
847
848 }