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