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