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