JAL-4062 a few more tests to check append honours distinct sequences and doesn't...
[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,
491                       res * avcharWidth + avcharHeight / 4,
492                       res * avcharWidth },
493                   new int[]
494                   { ypos - (avcharHeight / 2), ypos - (avcharHeight / 2),
495                       ypos - (avcharHeight / 2) + 8 },
496                   3);
497         }
498       }
499
500       if (g.getClip() == null)
501       {
502         g.setClip(0, 0, cWidth * avcharWidth, canvasHeight);
503       }
504
505       drawPanel(g, startRes, endx, 0, al.getHeight() - 1, ypos);
506       g.setClip(null);
507
508       if (av.isShowAnnotation())
509       {
510         g.translate(0, cHeight + ypos + 4);
511         if (annotations == null)
512         {
513           annotations = new AnnotationPanel(av);
514         }
515
516         annotations.drawComponent(g, startRes, endx + 1);
517         g.translate(0, -cHeight - ypos - 4);
518       }
519       g.translate(-LABEL_WEST, 0);
520
521       ypos += cHeight + getAnnotationHeight() + hgap;
522
523       startRes += cWidth;
524     }
525
526   }
527
528   AnnotationPanel annotations;
529
530   int getAnnotationHeight()
531   {
532     if (!av.isShowAnnotation())
533     {
534       return 0;
535     }
536
537     if (annotations == null)
538     {
539       annotations = new AnnotationPanel(av);
540     }
541
542     return annotations.adjustPanelHeight();
543   }
544
545   private void drawPanel(Graphics g1, final int startRes, final int endRes,
546           final int startSeq, final int endSeq, final int offset)
547   {
548
549     if (!av.hasHiddenColumns())
550     {
551       draw(g1, startRes, endRes, startSeq, endSeq, offset);
552     }
553     else
554     {
555       int screenY = 0;
556       int blockStart;
557       int blockEnd;
558
559       HiddenColumns hidden = av.getAlignment().getHiddenColumns();
560       VisibleContigsIterator regions = hidden
561               .getVisContigsIterator(startRes, endRes + 1, true);
562
563       while (regions.hasNext())
564       {
565         int[] region = regions.next();
566         blockEnd = region[1];
567         blockStart = region[0];
568
569         /*
570          * draw up to just before the next hidden region, or the end of
571          * the visible region, whichever comes first
572          */
573         g1.translate(screenY * avcharWidth, 0);
574
575         draw(g1, blockStart, blockEnd, startSeq, endSeq, offset);
576
577         /*
578          * draw the downline of the hidden column marker (ScalePanel draws the
579          * triangle on top) if we reached it
580          */
581         if (av.getShowHiddenMarkers()
582                 && (regions.hasNext() || regions.endsAtHidden()))
583         {
584           g1.setColor(Color.blue);
585           g1.drawLine((blockEnd - blockStart + 1) * avcharWidth - 1,
586                   0 + offset, (blockEnd - blockStart + 1) * avcharWidth - 1,
587                   (endSeq - startSeq + 1) * avcharHeight + offset);
588         }
589
590         g1.translate(-screenY * avcharWidth, 0);
591         screenY += blockEnd - blockStart + 1;
592       }
593     }
594   }
595
596   // int startRes, int endRes, int startSeq, int endSeq, int x, int y,
597   // int x1, int x2, int y1, int y2, int startx, int starty,
598   void draw(Graphics g, int startRes, int endRes, int startSeq, int endSeq,
599           int offset)
600   {
601     g.setFont(av.getFont());
602     sr.prepare(g, av.isRenderGaps());
603     updateViewport();
604     SequenceI nextSeq;
605
606     // / First draw the sequences
607     // ///////////////////////////
608     for (int i = startSeq; i <= endSeq; i++)
609     {
610       nextSeq = av.getAlignment().getSequenceAt(i);
611
612       if (nextSeq == null)
613       {
614         continue;
615       }
616
617       sr.drawSequence(nextSeq, av.getAlignment().findAllGroups(nextSeq),
618               startRes, endRes, offset + ((i - startSeq) * avcharHeight));
619
620       if (av.isShowSequenceFeatures())
621       {
622         fr.drawSequence(g, nextSeq, startRes, endRes,
623                 offset + ((i - startSeq) * avcharHeight), false);
624       }
625
626       // / Highlight search Results once all sequences have been drawn
627       // ////////////////////////////////////////////////////////
628       if (av.hasSearchResults())
629       {
630         int[] visibleResults = av.getSearchResults().getResults(nextSeq,
631                 startRes, endRes);
632         if (visibleResults != null)
633         {
634           for (int r = 0; r < visibleResults.length; r += 2)
635           {
636             sr.drawHighlightedText(nextSeq, visibleResults[r],
637                     visibleResults[r + 1],
638                     (visibleResults[r] - startRes) * avcharWidth,
639                     offset + ((i - startSeq) * avcharHeight));
640           }
641         }
642       }
643
644       if (av.cursorMode && cursorY == i && cursorX >= startRes
645               && cursorX <= endRes)
646       {
647         sr.drawCursor(nextSeq, cursorX, (cursorX - startRes) * avcharWidth,
648                 offset + ((i - startSeq) * avcharHeight));
649       }
650     }
651
652     if (av.getSelectionGroup() != null
653             || av.getAlignment().getGroups().size() > 0)
654     {
655       drawGroupsBoundaries(g, startRes, endRes, startSeq, endSeq, offset);
656     }
657
658   }
659
660   private void drawGroupsBoundaries(Graphics g, int startRes, int endRes,
661           int startSeq, int endSeq, int offset)
662   {
663     //
664     // ///////////////////////////////////
665     // Now outline any areas if necessary
666     // ///////////////////////////////////
667     SequenceGroup group = av.getSelectionGroup();
668
669     int sx = -1;
670     int sy = -1;
671     int ex = -1;
672     int groupIndex = -1;
673
674     if ((group == null) && (av.getAlignment().getGroups().size() > 0))
675     {
676       group = av.getAlignment().getGroups().get(0);
677       groupIndex = 0;
678     }
679
680     if (group != null)
681     {
682       do
683       {
684         int oldY = -1;
685         int i = 0;
686         boolean inGroup = false;
687         int top = -1;
688         int bottom = -1;
689         int alHeight = av.getAlignment().getHeight() - 1;
690
691         for (i = startSeq; i <= endSeq; i++)
692         {
693           sx = (group.getStartRes() - startRes) * avcharWidth;
694           sy = offset + ((i - startSeq) * avcharHeight);
695           ex = (((group.getEndRes() + 1) - group.getStartRes())
696                   * avcharWidth) - 1;
697
698           if (sx + ex < 0 || sx > imgWidth)
699           {
700             continue;
701           }
702
703           if ((sx <= (endRes - startRes) * avcharWidth)
704                   && group.getSequences(null)
705                           .contains(av.getAlignment().getSequenceAt(i)))
706           {
707             if ((bottom == -1)
708                     && (i >= alHeight || !group.getSequences(null).contains(
709                             av.getAlignment().getSequenceAt(i + 1))))
710             {
711               bottom = sy + avcharHeight;
712             }
713
714             if (!inGroup)
715             {
716               if (((top == -1) && (i == 0)) || !group.getSequences(null)
717                       .contains(av.getAlignment().getSequenceAt(i - 1)))
718               {
719                 top = sy;
720               }
721
722               oldY = sy;
723               inGroup = true;
724
725               if (group == av.getSelectionGroup())
726               {
727                 g.setColor(Color.red);
728               }
729               else
730               {
731                 g.setColor(group.getOutlineColour());
732               }
733             }
734           }
735           else
736           {
737             if (inGroup)
738             {
739               if (sx >= 0 && sx < imgWidth)
740               {
741                 g.drawLine(sx, oldY, sx, sy);
742               }
743
744               if (sx + ex < imgWidth)
745               {
746                 g.drawLine(sx + ex, oldY, sx + ex, sy);
747               }
748
749               if (sx < 0)
750               {
751                 ex += sx;
752                 sx = 0;
753               }
754
755               if (sx + ex > imgWidth)
756               {
757                 ex = imgWidth;
758               }
759
760               else if (sx + ex >= (endRes - startRes + 1) * avcharWidth)
761               {
762                 ex = (endRes - startRes + 1) * avcharWidth;
763               }
764
765               if (top != -1)
766               {
767                 g.drawLine(sx, top, sx + ex, top);
768                 top = -1;
769               }
770
771               if (bottom != -1)
772               {
773                 g.drawLine(sx, bottom, sx + ex, bottom);
774                 bottom = -1;
775               }
776
777               inGroup = false;
778             }
779           }
780         }
781
782         if (inGroup)
783         {
784           sy = offset + ((i - startSeq) * avcharHeight);
785           if (sx >= 0 && sx < imgWidth)
786           {
787             g.drawLine(sx, oldY, sx, sy);
788           }
789
790           if (sx + ex < imgWidth)
791           {
792             g.drawLine(sx + ex, oldY, sx + ex, sy);
793           }
794
795           if (sx < 0)
796           {
797             ex += sx;
798             sx = 0;
799           }
800
801           if (sx + ex > imgWidth)
802           {
803             ex = imgWidth;
804           }
805           else if (sx + ex >= (endRes - startRes + 1) * avcharWidth)
806           {
807             ex = (endRes - startRes + 1) * avcharWidth;
808           }
809
810           if (top != -1)
811           {
812             g.drawLine(sx, top, sx + ex, top);
813             top = -1;
814           }
815
816           if (bottom != -1)
817           {
818             g.drawLine(sx, bottom - 1, sx + ex, bottom - 1);
819             bottom = -1;
820           }
821
822           inGroup = false;
823         }
824
825         groupIndex++;
826
827         if (groupIndex >= av.getAlignment().getGroups().size())
828         {
829           break;
830         }
831
832         group = av.getAlignment().getGroups().get(groupIndex);
833       } while (groupIndex < av.getAlignment().getGroups().size());
834
835     }
836   }
837
838   public void highlightSearchResults(SearchResultsI results)
839   {
840     av.setSearchResults(results);
841     repaint();
842   }
843
844   @Override
845   public void propertyChange(PropertyChangeEvent evt)
846   {
847     String eventName = evt.getPropertyName();
848
849     if (eventName.equals(SequenceGroup.SEQ_GROUP_CHANGED))
850     {
851       fastPaint = true;
852       repaint();
853       return;
854     }
855     else if (eventName.equals(ViewportRanges.MOVE_VIEWPORT))
856     {
857       fastPaint = false;
858       repaint();
859       return;
860     }
861
862     if (!av.getWrapAlignment())
863     {
864       int scrollX = 0;
865       if (eventName.equals(ViewportRanges.STARTRES)
866               || eventName.equals(ViewportRanges.STARTRESANDSEQ))
867       {
868         // Make sure we're not trying to draw a panel
869         // larger than the visible window
870         if (eventName.equals(ViewportRanges.STARTRES))
871         {
872           scrollX = (int) evt.getNewValue() - (int) evt.getOldValue();
873         }
874         else
875         {
876           scrollX = ((int[]) evt.getNewValue())[0]
877                   - ((int[]) evt.getOldValue())[0];
878         }
879         ViewportRanges vpRanges = av.getRanges();
880         int range = vpRanges.getEndRes() - vpRanges.getStartRes();
881         if (scrollX > range)
882         {
883           scrollX = range;
884         }
885         else if (scrollX < -range)
886         {
887           scrollX = -range;
888         }
889       }
890
891       // Both scrolling and resizing change viewport ranges: scrolling changes
892       // both start and end points, but resize only changes end values.
893       // Here we only want to fastpaint on a scroll, with resize using a normal
894       // paint, so scroll events are identified as changes to the horizontal or
895       // vertical start value.
896       if (eventName.equals(ViewportRanges.STARTRES))
897       {
898         // scroll - startres and endres both change
899         fastPaint(scrollX, 0);
900       }
901       else if (eventName.equals(ViewportRanges.STARTSEQ))
902       {
903         // scroll
904         fastPaint(0, (int) evt.getNewValue() - (int) evt.getOldValue());
905       }
906       else if (eventName.equals(ViewportRanges.STARTRESANDSEQ))
907       {
908         fastPaint(scrollX, 0);
909       }
910     }
911   }
912
913   /**
914    * Ensure that a full paint is done next, for whatever reason. This was
915    * necessary for JavaScript; apparently in Java the timing is just right on
916    * multiple threads (EventQueue-0, Consensus, Conservation) that we can get
917    * away with one fast paint before the others, but this ensures that in the
918    * end we get a full paint. Problem arose in relation to copy/paste, where the
919    * paste was not finalized with a full paint.
920    * 
921    * @author hansonr 2019.04.17
922    */
923   public void clearFastPaint()
924   {
925     fastPaint = false;
926   }
927
928 }