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