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