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