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