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