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