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