Merge branch 'develop' into update_212_Dec_merge_with_21125_chamges
[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     int[] oldvalues = updateStartEndSeq(start, end);
240     int oldstartseq = oldvalues[0];
241     int oldendseq = oldvalues[1];
242
243     changeSupport.firePropertyChange(STARTSEQ, oldstartseq, startSeq);
244     if (oldstartseq == startSeq)
245     {
246       // event won't be fired if start positions are the same
247       // fire in case the end positions changed
248       changeSupport.firePropertyChange(ENDSEQ, oldendseq, endSeq);
249     }
250   }
251
252   /**
253    * Update start and end sequence values, adjusting for height constraints if
254    * necessary
255    * 
256    * @param start
257    *          start sequence
258    * @param end
259    *          end sequence
260    * @return array containing old start and end sequence values
261    */
262   private int[] updateStartEndSeq(int start, int end)
263   {
264
265 //    if (end == 3 && this.endSeq == 14 || end == 13 && this.endSeq == 3) {
266 //      new NullPointerException().printStackTrace(System.out);
267 //      System.out.println("ViewportRange updateStartEndSeq " + start + " " + end + " " + Thread.currentThread());
268 //    }
269     int oldstartseq = this.startSeq;
270     int oldendseq = this.endSeq;
271     int max = getVisibleAlignmentHeight() - 1;
272     startSeq = Math.max(0,  Math.min(start, max));
273     endSeq = Math.max(0, Math.min(end, max));
274     return new int[] { oldstartseq, oldendseq };
275   }
276
277   /**
278    * Set the last sequence visible in the viewport. Fires a property change
279    * event.
280    * 
281    * @param seq
282    *          sequence position in the range [0, height)
283    */
284   public void setEndSeq(int seq)
285   {
286     // BH 2018.04.18 added safety for seq < 0; comment about not being >= height
287     setStartEndSeq(Math.max(0, seq + 1 - getViewportHeight()), seq);
288   }
289
290   /**
291    * Set start residue and start sequence together (fires single event). The
292    * event supplies a pair of old values and a pair of new values: [old start
293    * residue, old start sequence] and [new start residue, new start sequence]
294    * 
295    * @param res
296    *          the start residue
297    * @param seq
298    *          the start sequence
299    */
300   public void setStartResAndSeq(int res, int seq)
301   {
302     int width = getViewportWidth();
303     int[] oldresvalues = updateStartEndRes(res, res + width - 1);
304
305     int startseq = seq;
306     int height = getViewportHeight();
307     if (startseq + height - 1 > getVisibleAlignmentHeight() - 1)
308     {
309       startseq = getVisibleAlignmentHeight() - height;
310     }
311     int[] oldseqvalues = updateStartEndSeq(startseq, startseq + height - 1);
312
313     int[] old = new int[] { oldresvalues[0], oldseqvalues[0] };
314     int[] newresseq = new int[] { startRes, startSeq };
315     changeSupport.firePropertyChange(STARTRESANDSEQ, old, newresseq);
316   }
317
318   /**
319    * Get start residue of viewport
320    */
321   public int getStartRes()
322   {
323     return startRes;
324   }
325
326   /**
327    * Get end residue of viewport
328    */
329   public int getEndRes()
330   {
331     return endRes;
332   }
333
334   /**
335    * Get start sequence of viewport
336    */
337   public int getStartSeq()
338   {
339     return startSeq;
340   }
341
342   /**
343    * Get end sequence of viewport
344    */
345   public int getEndSeq()
346   {
347     return endSeq;
348   }
349
350   /**
351    * Set viewport width in residues, without changing startRes. Use in
352    * preference to calculating endRes from the width, to avoid out by one
353    * errors! Fires a property change event.
354    * 
355    * @param w
356    *          width in residues
357    */
358   public void setViewportWidth(int w)
359   {
360     setStartEndRes(startRes, startRes + w - 1);
361   }
362
363   /**
364    * Set viewport height in residues, without changing startSeq. Use in
365    * preference to calculating endSeq from the height, to avoid out by one
366    * errors! Fires a property change event.
367    * 
368    * @param h
369    *          height in sequences
370    */
371   public void setViewportHeight(int h)
372   {
373     setStartEndSeq(startSeq, startSeq + h - 1);
374   }
375
376   /**
377    * Set viewport horizontal start position and width. Use in preference to
378    * calculating endRes from the width, to avoid out by one errors! Fires a
379    * property change event.
380    * 
381    * @param start
382    *          start residue
383    * @param w
384    *          width in residues
385    */
386   public void setViewportStartAndWidth(int start, int w)
387   {
388     int vpstart = Math.max(0, start);
389      
390     if (!wrappedMode)
391     {
392       // if not wrapped, don't leave white space at the right margin
393       int maxStart = getVisibleAlignmentWidth() - w;
394       if (maxStart >= 0)
395       {
396         vpstart = Math.min(vpstart, maxStart);
397       }
398
399     }
400     setStartEndRes(vpstart, vpstart + w - 1);
401   }
402
403   /**
404    * Set viewport vertical start position and height. Use in preference to
405    * calculating endSeq from the height, to avoid out by one errors! Fires a
406    * property change event.
407    * 
408    * @param start
409    *          start sequence
410    * @param h
411    *          height in sequences
412    */
413   public void setViewportStartAndHeight(int start, int h)
414   {
415     int vpstart = Math.max(0, start);
416     int maxStart = getVisibleAlignmentHeight() - h;
417     if (maxStart > 0)
418     {
419       // can't start higher than vertical extent will allow
420       // (viewport height is less than the full alignment
421       // and we are running off the bottom)
422       vpstart = Math.min(vpstart, maxStart);
423     }
424     setStartEndSeq(vpstart, vpstart + h - 1);
425   }
426
427   /**
428    * Get width of viewport in residues
429    * 
430    * @return width of viewport
431    */
432   public int getViewportWidth()
433   {
434     return (endRes - startRes + 1);
435   }
436
437   /**
438    * Get height of viewport in residues
439    * 
440    * @return height of viewport
441    */
442   public int getViewportHeight()
443   {
444     return (endSeq - startSeq + 1);
445   }
446
447   /**
448    * Scroll the viewport range vertically. Fires a property change event.
449    * 
450    * @param up
451    *          true if scrolling up, false if down
452    * 
453    * @return true if the scroll is valid
454    */
455   public boolean scrollUp(boolean up)
456   {
457     /*
458      * if in unwrapped mode, scroll up or down one sequence row;
459      * if in wrapped mode, scroll by one visible width of columns
460      */
461     if (up)
462     {
463       if (wrappedMode)
464       {
465         pageUp();
466       }
467       else
468       {
469         if (startSeq < 1)
470         {
471           return false;
472         }
473         setStartSeq(startSeq - 1);
474       }
475     }
476     else
477     {
478       if (wrappedMode)
479       {
480         pageDown();
481       }
482       else
483       {
484         if (endSeq >= getVisibleAlignmentHeight() - 1)
485         {
486           return false;
487         }
488         setStartSeq(startSeq + 1);
489       }
490     }
491     return true;
492   }
493
494   /**
495    * Scroll the viewport range horizontally. Fires a property change event.
496    * 
497    * @param right
498    *          true if scrolling right, false if left
499    * 
500    * @return true if the scroll is valid
501    */
502   public boolean scrollRight(boolean right)
503   {
504     if (!right)
505     {
506       if (startRes < 1)
507       {
508         return false;
509       }
510
511       setStartRes(startRes - 1);
512     }
513     else
514     {
515       if (endRes >= getVisibleAlignmentWidth() - 1)
516       {
517         return false;
518       }
519
520       setStartRes(startRes + 1);
521     }
522
523     return true;
524   }
525
526   /**
527    * Scroll a wrapped alignment so that the specified residue is in the first
528    * repeat of the wrapped view. Fires a property change event. Answers true if
529    * the startRes changed, else false.
530    * 
531    * @param res
532    *          residue position to scroll to NB visible position not absolute
533    *          alignment position
534    * @return
535    */
536   public boolean scrollToWrappedVisible(int res)
537   {
538     int newStartRes = calcWrappedStartResidue(res);
539     if (newStartRes == startRes)
540     {
541       return false;
542     }
543     setStartRes(newStartRes);
544
545     return true;
546   }
547
548   /**
549    * Calculate wrapped start residue from visible start residue
550    * 
551    * @param res
552    *          visible start residue
553    * @return left column of panel res will be located in
554    */
555   private int calcWrappedStartResidue(int res)
556   {
557     int oldStartRes = startRes;
558     int width = getViewportWidth();
559
560     boolean up = res < oldStartRes;
561     int widthsToScroll = Math.abs((res - oldStartRes) / width);
562     if (up)
563     {
564       widthsToScroll++;
565     }
566
567     int residuesToScroll = width * widthsToScroll;
568     int newStartRes = up ? oldStartRes - residuesToScroll
569             : oldStartRes + residuesToScroll;
570     if (newStartRes < 0)
571     {
572       newStartRes = 0;
573     }
574     return newStartRes;
575   }
576
577   /**
578    * Scroll so that (x,y) is visible. Fires a property change event.
579    * 
580    * @param x
581    *          x position in alignment (absolute position)
582    * @param y
583    *          y position in alignment (absolute position)
584    */
585   public void scrollToVisible(int x, int y)
586   {
587     while (y < startSeq)
588     {
589       scrollUp(true);
590     }
591     while (y > endSeq)
592     {
593       scrollUp(false);
594     }
595
596     HiddenColumns hidden = al.getHiddenColumns();
597     while (x < hidden.visibleToAbsoluteColumn(startRes))
598     {
599       if (!scrollRight(false))
600       {
601         break;
602       }
603     }
604     while (x > hidden.visibleToAbsoluteColumn(endRes))
605     {
606       if (!scrollRight(true))
607       {
608         break;
609       }
610     }
611   }
612
613   /**
614    * Set the viewport location so that a position is visible
615    * 
616    * @param x
617    *          column to be visible: absolute position in alignment
618    * @param y
619    *          row to be visible: absolute position in alignment
620    */
621   public boolean setViewportLocation(int x, int y)
622   {
623     boolean changedLocation = false;
624
625     // convert the x,y location to visible coordinates
626     int visX = al.getHiddenColumns().absoluteToVisibleColumn(x);
627     int visY = al.getHiddenSequences().findIndexWithoutHiddenSeqs(y);
628
629     // if (vis_x,vis_y) is already visible don't do anything
630     if (startRes > visX || visX > endRes
631             || startSeq > visY && visY > endSeq)
632     {
633       int[] old = new int[] { startRes, startSeq };
634       int[] newresseq;
635       if (wrappedMode)
636       {
637         int newstartres = calcWrappedStartResidue(visX);
638         setStartRes(newstartres);
639         newresseq = new int[] { startRes, startSeq };
640       }
641       else
642       {
643         // set the viewport x location to contain vis_x
644         int newstartres = visX;
645         int width = getViewportWidth();
646         if (newstartres + width - 1 > getVisibleAlignmentWidth() - 1)
647         {
648           newstartres = getVisibleAlignmentWidth() - width;
649         }
650         updateStartEndRes(newstartres, newstartres + width - 1);
651
652         // set the viewport y location to contain vis_y
653         int newstartseq = visY;
654         int height = getViewportHeight();
655         if (newstartseq + height - 1 > getVisibleAlignmentHeight() - 1)
656         {
657           newstartseq = getVisibleAlignmentHeight() - height;
658         }
659         updateStartEndSeq(newstartseq, newstartseq + height - 1);
660
661         newresseq = new int[] { startRes, startSeq };
662       }
663       changedLocation = true;
664       changeSupport.firePropertyChange(MOVE_VIEWPORT, old, newresseq);
665     }
666     return changedLocation;
667   }
668
669   /**
670    * Adjust sequence position for page up. Fires a property change event.
671    */
672   public void pageUp()
673   {
674     if (wrappedMode)
675     {
676       setStartRes(Math.max(0, getStartRes() - getViewportWidth()));
677     }
678     else
679     {
680       setViewportStartAndHeight(startSeq - (endSeq - startSeq),
681               getViewportHeight());
682     }
683   }
684
685   /**
686    * Adjust sequence position for page down. Fires a property change event.
687    */
688   public void pageDown()
689   {
690     if (wrappedMode)
691     {
692       /*
693        * if height is more than width (i.e. not all sequences fit on screen),
694        * increase page down to height
695        */
696       int newStart = getStartRes()
697               + Math.max(getViewportHeight(), getViewportWidth());
698
699       /*
700        * don't page down beyond end of alignment, or if not all
701        * sequences fit in the visible height
702        */
703       if (newStart < getVisibleAlignmentWidth())
704       {
705         setStartRes(newStart);
706       }
707     }
708     else
709     {
710       setViewportStartAndHeight(endSeq, getViewportHeight());
711     }
712   }
713
714   public void setWrappedMode(boolean wrapped)
715   {
716     wrappedMode = wrapped;
717   }
718
719   public boolean isWrappedMode()
720   {
721     return wrappedMode;
722   }
723
724   /**
725    * Answers the vertical scroll position (0..) to set, given the visible column
726    * that is at top left.
727    * 
728    * <pre>
729    * Example:
730    *    viewport width 40 columns (0-39, 40-79, 80-119...)
731    *    column 0 returns scroll position 0
732    *    columns 1-40 return scroll position 1
733    *    columns 41-80 return scroll position 2
734    *    etc
735    * </pre>
736    * 
737    * @param topLeftColumn
738    *          (0..)
739    * @return
740    */
741   public int getWrappedScrollPosition(final int topLeftColumn)
742   {
743     int w = getViewportWidth();
744
745     /*
746      * visible whole widths
747      */
748     int scroll = topLeftColumn / w;
749
750     /*
751      * add 1 for a part width if there is one
752      */
753     scroll += topLeftColumn % w > 0 ? 1 : 0;
754
755     return scroll;
756   }
757
758   /**
759    * Answers the maximum wrapped vertical scroll value, given the column
760    * position (0..) to show at top left of the visible region.
761    * 
762    * @param topLeftColumn
763    * @return
764    */
765   public int getWrappedMaxScroll(int topLeftColumn)
766   {
767     int scrollPosition = getWrappedScrollPosition(topLeftColumn);
768
769     /*
770      * how many more widths could be drawn after this one?
771      */
772     int columnsRemaining = getVisibleAlignmentWidth() - topLeftColumn;
773     int width = getViewportWidth();
774     int widthsRemaining = columnsRemaining / width
775             + (columnsRemaining % width > 0 ? 1 : 0) - 1;
776     int maxScroll = scrollPosition + widthsRemaining;
777
778     return maxScroll;
779   }
780   
781
782   @Override
783   public String toString() {
784      return "[ViewportRange " + startSeq + "-" + endSeq + ", "+ startRes + "-" + endRes + "]";
785   }
786 }