Build #8 test showing thread activity on ViewportRanges.endSeq.
[jalview.git] / src / jalview / viewmodel / ViewportRanges.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.viewmodel;
22
23 import jalview.datamodel.AlignmentI;
24 import jalview.datamodel.HiddenColumns;
25
26 /**
27  * Supplies and updates viewport properties relating to position such as: start
28  * and end residues and sequences; ideally will serve hidden columns/rows too.
29  * Intention also to support calculations for positioning, scrolling etc. such
30  * as finding the middle of the viewport, checking for scrolls off screen
31  */
32 public class ViewportRanges extends ViewportProperties
33 {
34   public static final String STARTRES = "startres";
35
36   public static final String ENDRES = "endres";
37
38   public static final String STARTSEQ = "startseq";
39
40   public static final String ENDSEQ = "endseq";
41
42   public static final String STARTRESANDSEQ = "startresandseq";
43
44   public static final String MOVE_VIEWPORT = "move_viewport";
45
46   private boolean wrappedMode = false;
47
48   // start residue of viewport
49   private int startRes;
50
51   // end residue of viewport
52   private int endRes;
53
54   // start sequence of viewport
55   private int startSeq;
56
57   // end sequence of viewport
58   private int endSeq;
59
60   // alignment
61   private AlignmentI al;
62
63   /**
64    * Constructor
65    * 
66    * @param alignment
67    *          the viewport's alignment
68    */
69   public ViewportRanges(AlignmentI alignment)
70   {
71     // initial values of viewport settings
72     this.startRes = 0;
73     this.endRes = alignment.getWidth() - 1;
74     this.startSeq = 0;
75     this.setEndSeqTest(alignment.getHeight() - 1);
76     this.al = alignment;
77   }
78
79   public static String sTest = "";
80
81   private void setEndSeqTest(int val)
82   {
83     sTest += "ViewPortRanges.setEndseqTest " + val + " "
84             + Thread.currentThread() + "\n";
85     endSeq = val;
86   }
87   /**
88    * Get alignment width in cols, including hidden cols
89    */
90   public int getAbsoluteAlignmentWidth()
91   {
92     return al.getWidth();
93   }
94
95   /**
96    * Get alignment height in rows, including hidden rows
97    */
98   public int getAbsoluteAlignmentHeight()
99   {
100     return al.getHeight() + al.getHiddenSequences().getSize();
101   }
102
103   /**
104    * Get alignment width in cols, excluding hidden cols
105    */
106   public int getVisibleAlignmentWidth()
107   {
108     return al.getVisibleWidth();
109   }
110
111   /**
112    * Get alignment height in rows, excluding hidden rows
113    */
114   public int getVisibleAlignmentHeight()
115   {
116     return al.getHeight();
117   }
118
119   /**
120    * Set first residue visible in the viewport, and retain the current width.
121    * Fires a property change event.
122    * 
123    * @param res
124    *          residue position
125    */
126   public void setStartRes(int res)
127   {
128     int width = getViewportWidth();
129     setStartEndRes(res, res + width - 1);
130   }
131
132   /**
133    * Set start and end residues at the same time. This method only fires one
134    * event for the two changes, and should be used in preference to separate
135    * calls to setStartRes and setEndRes.
136    * 
137    * @param start
138    *          the start residue
139    * @param end
140    *          the end residue
141    */
142   public void setStartEndRes(int start, int end)
143   {
144     int[] oldvalues = updateStartEndRes(start, end);
145     int oldstartres = oldvalues[0];
146     int oldendres = oldvalues[1];
147
148     if (oldstartres == startRes && oldendres == endRes)
149     {
150       return; // BH 2019.07.27 standard check for no changes
151     }
152
153     // "STARTRES" is a misnomer here -- really "STARTORENDRES"
154     // note that this could be "no change" if the range is just being expanded
155     changeSupport.firePropertyChange(STARTRES, oldstartres, startRes);
156     if (oldstartres == startRes)
157     {
158       // No listener cares about this
159       // "ENDRES" is a misnomer here -- really "ENDONLYRES"
160       // BH 2019.07.27 adds end change check
161       // fire only if only the end is changed
162       changeSupport.firePropertyChange(ENDRES, oldendres, endRes);
163     }
164   }
165
166   /**
167    * Update start and end residue values, adjusting for width constraints if
168    * necessary
169    * 
170    * @param start
171    *          start residue
172    * @param end
173    *          end residue
174    * @return array containing old start and end residue values
175    */
176   private int[] updateStartEndRes(int start, int end)
177   {
178     int oldstartres = this.startRes;
179
180     /*
181      * if not wrapped, don't leave white space at the right margin
182      */
183     int lastColumn = getVisibleAlignmentWidth() - 1;
184     if (!wrappedMode && (start > lastColumn))
185     {
186       startRes = Math.max(lastColumn, 0);
187     }
188     else if (start < 0)
189     {
190       startRes = 0;
191     }
192     else
193     {
194       startRes = start;
195     }
196
197     int oldendres = this.endRes;
198     if (end < 0)
199     {
200       endRes = 0;
201     }
202     else if (!wrappedMode && (end > lastColumn))
203     {
204       endRes = Math.max(lastColumn, 0);
205     }
206     else
207     {
208       endRes = end;
209     }
210     return new int[] { oldstartres, oldendres };
211   }
212
213   /**
214    * Set the first sequence visible in the viewport, maintaining the height. If
215    * the viewport would extend past the last sequence, sets the viewport so it
216    * sits at the bottom of the alignment. Fires a property change event.
217    * 
218    * @param seq
219    *          sequence position
220    */
221   public void setStartSeq(int seq)
222   {
223     int height = getViewportHeight();
224     int startseq = Math.min(seq, getVisibleAlignmentHeight() - height);
225     // BH 2019.07.27 cosmetic only -- was:
226     // if (startseq + height - 1 > getVisibleAlignmentHeight() - 1)
227     // {
228     // startseq = getVisibleAlignmentHeight() - height;
229     // }
230     setStartEndSeq(startseq, startseq + height - 1);
231   }
232
233   /**
234    * Set start and end sequences at the same time. The viewport height may
235    * change. This method only fires one event for the two changes, and should be
236    * used in preference to separate calls to setStartSeq and setEndSeq.
237    * 
238    * @param start
239    *          the start sequence
240    * @param end
241    *          the end sequence
242    */
243   public void setStartEndSeq(int start, int end)
244   {
245     // System.out.println("ViewportRange setStartEndSeq " + start + " " + end);
246     int[] oldvalues = updateStartEndSeq(start, end);
247     int oldstartseq = oldvalues[0];
248     int oldendseq = oldvalues[1];
249
250     if (oldstartseq == startSeq && oldendseq == endSeq)
251     {
252       return; // BH 2019.07.27 standard check for no changes
253     }
254
255     // "STARTSEQ" is a misnomer here -- really "STARTORENDSEQ"
256     changeSupport.firePropertyChange(STARTSEQ, oldstartseq, startSeq);
257     if (oldstartseq == startSeq)
258     {
259       // Note that all listeners ignore this - could be removed, or there is a
260       // bug.
261       // "ENDSEQ" is a misnomer here -- really "ENDONLYSEQ"
262       // additional fire, only if only the end is changed
263       changeSupport.firePropertyChange(ENDSEQ, oldendseq, endSeq);
264     }
265   }
266
267   /**
268    * Update start and end sequence values, adjusting for height constraints if
269    * necessary
270    * 
271    * @param start
272    *          start sequence
273    * @param end
274    *          end sequence
275    * @return array containing old start and end sequence values
276    */
277   private int[] updateStartEndSeq(int start, int end)
278   {
279     int oldstartseq = this.startSeq;
280     int visibleHeight = getVisibleAlignmentHeight();
281     if (start > visibleHeight - 1)
282     {
283       startSeq = Math.max(visibleHeight - 1, 0);
284     }
285     else if (start < 0)
286     {
287       startSeq = 0;
288     }
289     else
290     {
291       startSeq = start;
292     }
293
294     int oldendseq = this.endSeq;
295     if (end >= visibleHeight)
296     {
297       setEndSeqTest(Math.max(visibleHeight - 1, 0));
298     }
299     else if (end < 0)
300     {
301       setEndSeqTest(0);
302     }
303     else
304     {
305       setEndSeqTest(end);
306     }
307     return new int[] { oldstartseq, oldendseq };
308   }
309
310   /**
311    * Set the last sequence visible in the viewport. Fires a property change
312    * event.
313    * 
314    * @param seq
315    *          sequence position in the range [0, height)
316    */
317   public void setEndSeq(int seq)
318   {
319     // BH 2018.04.18 added safety for seq < 0; comment about not being >= height
320     setStartEndSeq(Math.max(0, seq + 1 - getViewportHeight()), seq);
321   }
322
323   /**
324    * Set start residue and start sequence together (fires single event). The
325    * event supplies a pair of old values and a pair of new values: [old start
326    * residue, old start sequence] and [new start residue, new start sequence]
327    * 
328    * @param res
329    *          the start residue
330    * @param seq
331    *          the start sequence
332    */
333   public void setStartResAndSeq(int res, int seq)
334   {
335     int width = getViewportWidth();
336     int[] oldresvalues = updateStartEndRes(res, res + width - 1);
337
338     int startseq = seq;
339     int height = getViewportHeight();
340     if (startseq + height - 1 > getVisibleAlignmentHeight() - 1)
341     {
342       startseq = getVisibleAlignmentHeight() - height;
343     }
344     int[] oldseqvalues = updateStartEndSeq(startseq, startseq + height - 1);
345
346     int[] oldvalues = new int[] { oldresvalues[0], oldseqvalues[0] };
347     int[] newvalues = new int[] { startRes, startSeq };
348     changeSupport.firePropertyChange(STARTRESANDSEQ, oldvalues, newvalues);
349   }
350
351   /**
352    * Get start residue of viewport
353    */
354   public int getStartRes()
355   {
356     return startRes;
357   }
358
359   /**
360    * Get end residue of viewport
361    */
362   public int getEndRes()
363   {
364     return endRes;
365   }
366
367   /**
368    * Get start sequence of viewport
369    */
370   public int getStartSeq()
371   {
372     return startSeq;
373   }
374
375   /**
376    * Get end sequence of viewport
377    */
378   public int getEndSeq()
379   {
380     return endSeq;
381   }
382
383   /**
384    * Set viewport width in residues, without changing startRes. Use in
385    * preference to calculating endRes from the width, to avoid out by one
386    * errors! Fires a property change event.
387    * 
388    * @param w
389    *          width in residues
390    */
391   public void setViewportWidth(int w)
392   {
393     setStartEndRes(startRes, startRes + w - 1);
394   }
395
396   /**
397    * Set viewport height in residues, without changing startSeq. Use in
398    * preference to calculating endSeq from the height, to avoid out by one
399    * errors! Fires a property change event.
400    * 
401    * @param h
402    *          height in sequences
403    */
404   public void setViewportHeight(int h)
405   {
406     setStartEndSeq(startSeq, startSeq + h - 1);
407   }
408
409   /**
410    * Set viewport horizontal start position and width. Use in preference to
411    * calculating endRes from the width, to avoid out by one errors! Fires a
412    * property change event.
413    * 
414    * @param start
415    *          start residue
416    * @param w
417    *          width in residues
418    */
419   public void setViewportStartAndWidth(int start, int w)
420   {
421     int vpstart = start;
422     if (vpstart < 0)
423     {
424       vpstart = 0;
425     }
426
427     /*
428      * if not wrapped, don't leave white space at the right margin
429      */
430     if (!wrappedMode)
431     {
432       if ((w <= getVisibleAlignmentWidth())
433               && (vpstart + w - 1 > getVisibleAlignmentWidth() - 1))
434       {
435         vpstart = getVisibleAlignmentWidth() - w;
436       }
437
438     }
439     setStartEndRes(vpstart, vpstart + w - 1);
440   }
441
442   /**
443    * Set viewport vertical start position and height. Use in preference to
444    * calculating endSeq from the height, to avoid out by one errors! Fires a
445    * property change event.
446    * 
447    * @param start
448    *          start sequence
449    * @param h
450    *          height in sequences
451    */
452   public void setViewportStartAndHeight(int start, int h)
453   {
454     int vpstart = start;
455
456     int visHeight = getVisibleAlignmentHeight();
457     if (vpstart < 0)
458     {
459       vpstart = 0;
460     }
461     else if (h <= visHeight && vpstart + h > visHeight)
462     // viewport height is less than the full alignment and we are running off
463     // the bottom
464     {
465       vpstart = visHeight - h;
466     }
467
468     setStartEndSeq(vpstart, vpstart + h - 1);
469   }
470
471   /**
472    * Get width of viewport in residues
473    * 
474    * @return width of viewport
475    */
476   public int getViewportWidth()
477   {
478     return (endRes - startRes + 1);
479   }
480
481   /**
482    * Get height of viewport in residues
483    * 
484    * @return height of viewport
485    */
486   public int getViewportHeight()
487   {
488     return (endSeq - startSeq + 1);
489   }
490
491   /**
492    * Scroll the viewport range vertically. Fires a property change event.
493    * 
494    * @param up
495    *          true if scrolling up, false if down
496    * 
497    * @return true if the scroll is valid
498    */
499   public boolean scrollUp(boolean up)
500   {
501     /*
502      * if in unwrapped mode, scroll up or down one sequence row;
503      * if in wrapped mode, scroll by one visible width of columns
504      */
505     if (up)
506     {
507       if (wrappedMode)
508       {
509         pageUp();
510       }
511       else
512       {
513         if (startSeq < 1)
514         {
515           return false;
516         }
517         setStartSeq(startSeq - 1);
518       }
519     }
520     else
521     {
522       if (wrappedMode)
523       {
524         pageDown();
525       }
526       else
527       {
528         if (endSeq >= getVisibleAlignmentHeight() - 1)
529         {
530           return false;
531         }
532         setStartSeq(startSeq + 1);
533       }
534     }
535     return true;
536   }
537
538   /**
539    * Scroll the viewport range horizontally. Fires a property change event.
540    * 
541    * @param right
542    *          true if scrolling right, false if left
543    * 
544    * @return true if the scroll is valid
545    */
546   public boolean scrollRight(boolean right)
547   {
548     if (!right)
549     {
550       if (startRes < 1)
551       {
552         return false;
553       }
554
555       setStartRes(startRes - 1);
556     }
557     else
558     {
559       if (endRes >= getVisibleAlignmentWidth() - 1)
560       {
561         return false;
562       }
563
564       setStartRes(startRes + 1);
565     }
566
567     return true;
568   }
569
570   /**
571    * Scroll a wrapped alignment so that the specified residue is in the first
572    * repeat of the wrapped view. Fires a property change event. Answers true if
573    * the startRes changed, else false.
574    * 
575    * @param res
576    *          residue position to scroll to NB visible position not absolute
577    *          alignment position
578    * @return
579    */
580   public boolean scrollToWrappedVisible(int res)
581   {
582     int newStartRes = calcWrappedStartResidue(res);
583     if (newStartRes == startRes)
584     {
585       return false;
586     }
587     setStartRes(newStartRes);
588
589     return true;
590   }
591
592   /**
593    * Calculate wrapped start residue from visible start residue
594    * 
595    * @param res
596    *          visible start residue
597    * @return left column of panel res will be located in
598    */
599   private int calcWrappedStartResidue(int res)
600   {
601     int oldStartRes = startRes;
602     int width = getViewportWidth();
603
604     boolean up = res < oldStartRes;
605     int widthsToScroll = Math.abs((res - oldStartRes) / width);
606     if (up)
607     {
608       widthsToScroll++;
609     }
610
611     int residuesToScroll = width * widthsToScroll;
612     int newStartRes = up ? oldStartRes - residuesToScroll : oldStartRes
613             + residuesToScroll;
614     if (newStartRes < 0)
615     {
616       newStartRes = 0;
617     }
618     return newStartRes;
619   }
620
621   /**
622    * Scroll so that (x,y) is visible. Fires a property change event.
623    * 
624    * @param x
625    *          x position in alignment (absolute position)
626    * @param y
627    *          y position in alignment (absolute position)
628    */
629   public void scrollToVisible(int x, int y)
630   {
631     while (y < startSeq)
632     {
633       scrollUp(true);
634     }
635     while (y > endSeq)
636     {
637       scrollUp(false);
638     }
639     
640     HiddenColumns hidden = al.getHiddenColumns();
641     while (x < hidden.visibleToAbsoluteColumn(startRes))
642     {
643       if (!scrollRight(false))
644       {
645         break;
646       }
647     }
648     while (x > hidden.visibleToAbsoluteColumn(endRes))
649     {
650       if (!scrollRight(true))
651       {
652         break;
653       }
654     }
655   }
656
657   /**
658    * Set the viewport location so that a position is visible
659    * 
660    * @param x
661    *          column to be visible: absolute position in alignment
662    * @param y
663    *          row to be visible: absolute position in alignment
664    */
665   public boolean setViewportLocation(int x, int y)
666   {
667     boolean changedLocation = false;
668
669     // convert the x,y location to visible coordinates
670     int visX = al.getHiddenColumns().absoluteToVisibleColumn(x);
671     int visY = al.getHiddenSequences().findIndexWithoutHiddenSeqs(y);
672
673     // if (vis_x,vis_y) is already visible don't do anything
674     if (startRes > visX || visX > endRes
675             || startSeq > visY && visY > endSeq)
676     {
677       int[] old = new int[] { startRes, startSeq };
678       int[] newresseq;
679       if (wrappedMode)
680       {
681         int newstartres = calcWrappedStartResidue(visX);
682         setStartRes(newstartres);
683         newresseq = new int[] { startRes, startSeq };
684       }
685       else
686       {
687         // set the viewport x location to contain vis_x
688         int newstartres = visX;
689         int width = getViewportWidth();
690         if (newstartres + width - 1 > getVisibleAlignmentWidth() - 1)
691         {
692           newstartres = getVisibleAlignmentWidth() - width;
693         }
694         updateStartEndRes(newstartres, newstartres + width - 1);
695
696         // set the viewport y location to contain vis_y
697         int newstartseq = visY;
698         int height = getViewportHeight();
699         if (newstartseq + height - 1 > getVisibleAlignmentHeight() - 1)
700         {
701           newstartseq = getVisibleAlignmentHeight() - height;
702         }
703         updateStartEndSeq(newstartseq, newstartseq + height - 1);
704
705         newresseq = new int[] { startRes, startSeq };
706       }
707       changedLocation = true;
708       changeSupport.firePropertyChange(MOVE_VIEWPORT, old, newresseq);
709     }
710     return changedLocation;
711   }
712
713   /**
714    * Adjust sequence position for page up. Fires a property change event.
715    */
716   public void pageUp()
717   {
718     if (wrappedMode)
719     {
720       setStartRes(Math.max(0, getStartRes() - getViewportWidth()));
721     }
722     else
723     {
724       setViewportStartAndHeight(startSeq - (endSeq - startSeq),
725               getViewportHeight());
726     }
727   }
728
729   /**
730    * Adjust sequence position for page down. Fires a property change event.
731    */
732   public void pageDown()
733   {
734     if (wrappedMode)
735     {
736       /*
737        * if height is more than width (i.e. not all sequences fit on screen),
738        * increase page down to height
739        */
740       int newStart = getStartRes()
741               + Math.max(getViewportHeight(), getViewportWidth());
742
743       /*
744        * don't page down beyond end of alignment, or if not all
745        * sequences fit in the visible height
746        */
747       if (newStart < getVisibleAlignmentWidth())
748       {
749         setStartRes(newStart);
750       }
751     }
752     else
753     {
754       setViewportStartAndHeight(endSeq, getViewportHeight());
755     }
756   }
757
758   public void setWrappedMode(boolean wrapped)
759   {
760     wrappedMode = wrapped;
761   }
762
763   public boolean isWrappedMode()
764   {
765     return wrappedMode;
766   }
767
768   /**
769    * Answers the vertical scroll position (0..) to set, given the visible column
770    * that is at top left.
771    * 
772    * <pre>
773    * Example:
774    *    viewport width 40 columns (0-39, 40-79, 80-119...)
775    *    column 0 returns scroll position 0
776    *    columns 1-40 return scroll position 1
777    *    columns 41-80 return scroll position 2
778    *    etc
779    * </pre>
780    * 
781    * @param topLeftColumn
782    *          (0..)
783    * @return
784    */
785   public int getWrappedScrollPosition(final int topLeftColumn)
786   {
787     int w = getViewportWidth();
788
789     /*
790      * visible whole widths
791      */
792     int scroll = topLeftColumn / w;
793
794     /*
795      * add 1 for a part width if there is one
796      */
797     scroll += topLeftColumn % w > 0 ? 1 : 0;
798
799     return scroll;
800   }
801
802   /**
803    * Answers the maximum wrapped vertical scroll value, given the column
804    * position (0..) to show at top left of the visible region.
805    * 
806    * @param topLeftColumn
807    * @return
808    */
809   public int getWrappedMaxScroll(int topLeftColumn)
810   {
811     int scrollPosition = getWrappedScrollPosition(topLeftColumn);
812
813     /*
814      * how many more widths could be drawn after this one?
815      */
816     int columnsRemaining = getVisibleAlignmentWidth() - topLeftColumn;
817     int width = getViewportWidth();
818     int widthsRemaining = columnsRemaining / width
819             + (columnsRemaining % width > 0 ? 1 : 0) - 1;
820     int maxScroll = scrollPosition + widthsRemaining;
821
822     return maxScroll;
823   }
824
825   @Override
826   public String toString()
827   {
828     return "[ViewportRange startRes=" + startRes + " endRes=" + endRes
829             + " startSeq=" + startSeq + " endSeq=" + endSeq + "]";
830   }
831 }