JAL-2223 don't add duplicate mappings to StructureSelectionManager
[jalview.git] / src / jalview / util / MapList.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.util;
22
23 import java.util.ArrayList;
24 import java.util.Arrays;
25 import java.util.List;
26
27 /**
28  * A simple way of bijectively mapping a non-contiguous linear range to another
29  * non-contiguous linear range.
30  * 
31  * Use at your own risk!
32  * 
33  * TODO: efficient implementation of private posMap method
34  * 
35  * TODO: test/ensure that sense of from and to ratio start position is conserved
36  * (codon start position recovery)
37  */
38 public class MapList
39 {
40
41   /*
42    * Subregions (base 1) described as { [start1, end1], [start2, end2], ...}
43    */
44   private List<int[]> fromShifts;
45
46   /*
47    * Same format as fromShifts, for the 'mapped to' sequence
48    */
49   private List<int[]> toShifts;
50
51   /*
52    * number of steps in fromShifts to one toRatio unit
53    */
54   private int fromRatio;
55
56   /*
57    * number of steps in toShifts to one fromRatio
58    */
59   private int toRatio;
60
61   /*
62    * lowest and highest value in the from Map
63    */
64   private int fromLowest;
65
66   private int fromHighest;
67
68   /*
69    * lowest and highest value in the to Map
70    */
71   private int toLowest;
72
73   private int toHighest;
74
75   /**
76    * Constructor
77    */
78   public MapList()
79   {
80     fromShifts = new ArrayList<>();
81     toShifts = new ArrayList<>();
82   }
83
84   /**
85    * Two MapList objects are equal if they are the same object, or they both
86    * have populated shift ranges and all values are the same.
87    */
88   @Override
89   public boolean equals(Object o)
90   {
91     if (o == null || !(o instanceof MapList))
92     {
93       return false;
94     }
95
96     MapList obj = (MapList) o;
97     if (obj == this)
98     {
99       return true;
100     }
101     if (obj.fromRatio != fromRatio || obj.toRatio != toRatio
102             || obj.fromShifts == null || obj.toShifts == null)
103     {
104       return false;
105     }
106     return Arrays.deepEquals(fromShifts.toArray(), obj.fromShifts.toArray())
107             && Arrays.deepEquals(toShifts.toArray(),
108                     obj.toShifts.toArray());
109   }
110
111   /**
112    * Returns a hashcode made from the fromRatio, toRatio, and from/to ranges
113    */
114   @Override
115   public int hashCode()
116   {
117     int hashCode = 31 * fromRatio;
118     hashCode = 31 * hashCode + toRatio;
119     for (int[] shift : fromShifts)
120     {
121       hashCode = 31 * hashCode + shift[0];
122       hashCode = 31 * hashCode + shift[1];
123     }
124     for (int[] shift : toShifts)
125     {
126       hashCode = 31 * hashCode + shift[0];
127       hashCode = 31 * hashCode + shift[1];
128     }
129
130     return hashCode;
131   }
132
133   /**
134    * Returns the 'from' ranges as {[start1, end1], [start2, end2], ...}
135    * 
136    * @return
137    */
138   public List<int[]> getFromRanges()
139   {
140     return fromShifts;
141   }
142
143   /**
144    * Returns the 'to' ranges as {[start1, end1], [start2, end2], ...}
145    * 
146    * @return
147    */
148   public List<int[]> getToRanges()
149   {
150     return toShifts;
151   }
152
153   /**
154    * Flattens a list of [start, end] into a single [start1, end1, start2,
155    * end2,...] array.
156    * 
157    * @param shifts
158    * @return
159    */
160   protected static int[] getRanges(List<int[]> shifts)
161   {
162     int[] rnges = new int[2 * shifts.size()];
163     int i = 0;
164     for (int[] r : shifts)
165     {
166       rnges[i++] = r[0];
167       rnges[i++] = r[1];
168     }
169     return rnges;
170   }
171
172   /**
173    * 
174    * @return length of mapped phrase in from
175    */
176   public int getFromRatio()
177   {
178     return fromRatio;
179   }
180
181   /**
182    * 
183    * @return length of mapped phrase in to
184    */
185   public int getToRatio()
186   {
187     return toRatio;
188   }
189
190   public int getFromLowest()
191   {
192     return fromLowest;
193   }
194
195   public int getFromHighest()
196   {
197     return fromHighest;
198   }
199
200   public int getToLowest()
201   {
202     return toLowest;
203   }
204
205   public int getToHighest()
206   {
207     return toHighest;
208   }
209
210   /**
211    * Constructor given from and to ranges as [start1, end1, start2, end2,...].
212    * If any end is equal to the next start, the ranges will be merged. There is
213    * no validation check that the ranges do not overlap each other.
214    * 
215    * @param from
216    *          contiguous regions as [start1, end1, start2, end2, ...]
217    * @param to
218    *          same format as 'from'
219    * @param fromRatio
220    *          phrase length in 'from' (e.g. 3 for dna)
221    * @param toRatio
222    *          phrase length in 'to' (e.g. 1 for protein)
223    */
224   public MapList(int from[], int to[], int fromRatio, int toRatio)
225   {
226     this();
227     this.fromRatio = fromRatio;
228     this.toRatio = toRatio;
229     fromLowest = Integer.MAX_VALUE;
230     fromHighest = Integer.MIN_VALUE;
231     int added = 0;
232
233     for (int i = 0; i < from.length; i += 2)
234     {
235       /*
236        * note lowest and highest values - bearing in mind the
237        * direction may be reversed
238        */
239       fromLowest = Math.min(fromLowest, Math.min(from[i], from[i + 1]));
240       fromHighest = Math.max(fromHighest, Math.max(from[i], from[i + 1]));
241       if (added > 0 && from[i] == fromShifts.get(added - 1)[1])
242       {
243         /*
244          * this range starts where the last ended - just extend it
245          */
246         fromShifts.get(added - 1)[1] = from[i + 1];
247       }
248       else
249       {
250         fromShifts.add(new int[] { from[i], from[i + 1] });
251         added++;
252       }
253     }
254
255     toLowest = Integer.MAX_VALUE;
256     toHighest = Integer.MIN_VALUE;
257     added = 0;
258     for (int i = 0; i < to.length; i += 2)
259     {
260       toLowest = Math.min(toLowest, Math.min(to[i], to[i + 1]));
261       toHighest = Math.max(toHighest, Math.max(to[i], to[i + 1]));
262       if (added > 0 && to[i] == toShifts.get(added - 1)[1])
263       {
264         toShifts.get(added - 1)[1] = to[i + 1];
265       }
266       else
267       {
268         toShifts.add(new int[] { to[i], to[i + 1] });
269         added++;
270       }
271     }
272   }
273
274   /**
275    * Copy constructor. Creates an identical mapping.
276    * 
277    * @param map
278    */
279   public MapList(MapList map)
280   {
281     this();
282     // TODO not used - remove?
283     this.fromLowest = map.fromLowest;
284     this.fromHighest = map.fromHighest;
285     this.toLowest = map.toLowest;
286     this.toHighest = map.toHighest;
287
288     this.fromRatio = map.fromRatio;
289     this.toRatio = map.toRatio;
290     if (map.fromShifts != null)
291     {
292       for (int[] r : map.fromShifts)
293       {
294         fromShifts.add(new int[] { r[0], r[1] });
295       }
296     }
297     if (map.toShifts != null)
298     {
299       for (int[] r : map.toShifts)
300       {
301         toShifts.add(new int[] { r[0], r[1] });
302       }
303     }
304   }
305
306   /**
307    * Constructor given ranges as lists of [start, end] positions. There is no
308    * validation check that the ranges do not overlap each other.
309    * 
310    * @param fromRange
311    * @param toRange
312    * @param fromRatio
313    * @param toRatio
314    */
315   public MapList(List<int[]> fromRange, List<int[]> toRange, int fromRatio,
316           int toRatio)
317   {
318     this();
319     fromRange = coalesceRanges(fromRange);
320     toRange = coalesceRanges(toRange);
321     this.fromShifts = fromRange;
322     this.toShifts = toRange;
323     this.fromRatio = fromRatio;
324     this.toRatio = toRatio;
325
326     fromLowest = Integer.MAX_VALUE;
327     fromHighest = Integer.MIN_VALUE;
328     for (int[] range : fromRange)
329     {
330       fromLowest = Math.min(fromLowest, Math.min(range[0], range[1]));
331       fromHighest = Math.max(fromHighest, Math.max(range[0], range[1]));
332     }
333
334     toLowest = Integer.MAX_VALUE;
335     toHighest = Integer.MIN_VALUE;
336     for (int[] range : toRange)
337     {
338       toLowest = Math.min(toLowest, Math.min(range[0], range[1]));
339       toHighest = Math.max(toHighest, Math.max(range[0], range[1]));
340     }
341   }
342
343   /**
344    * Consolidates a list of ranges so that any contiguous ranges are merged.
345    * This assumes the ranges are already in start order (does not sort them).
346    * 
347    * @param ranges
348    * @return the same list (if unchanged), else a new merged list, leaving the
349    *         input list unchanged
350    */
351   public static List<int[]> coalesceRanges(final List<int[]> ranges)
352   {
353     if (ranges == null || ranges.size() < 2)
354     {
355       return ranges;
356     }
357
358     boolean changed = false;
359     List<int[]> merged = new ArrayList<>();
360     int[] lastRange = ranges.get(0);
361     int lastDirection = lastRange[1] >= lastRange[0] ? 1 : -1;
362     lastRange = new int[] { lastRange[0], lastRange[1] };
363     merged.add(lastRange);
364     boolean first = true;
365
366     for (final int[] range : ranges)
367     {
368       if (first)
369       {
370         first = false;
371         continue;
372       }
373       if (range[0] == lastRange[0] && range[1] == lastRange[1])
374       {
375         // drop duplicate range
376         changed = true;
377         continue;
378       }
379
380       /*
381        * drop this range if it lies within the last range
382        */
383       if ((lastDirection == 1 && range[0] >= lastRange[0]
384               && range[0] <= lastRange[1] && range[1] >= lastRange[0]
385               && range[1] <= lastRange[1])
386               || (lastDirection == -1 && range[0] <= lastRange[0]
387                       && range[0] >= lastRange[1]
388                       && range[1] <= lastRange[0]
389                       && range[1] >= lastRange[1]))
390       {
391         changed = true;
392         continue;
393       }
394
395       int direction = range[1] >= range[0] ? 1 : -1;
396
397       /*
398        * if next range is in the same direction as last and contiguous,
399        * just update the end position of the last range
400        */
401       boolean sameDirection = range[1] == range[0]
402               || direction == lastDirection;
403       boolean extending = range[0] == lastRange[1] + lastDirection;
404       boolean overlapping = (lastDirection == 1 && range[0] >= lastRange[0]
405               && range[0] <= lastRange[1])
406               || (lastDirection == -1 && range[0] <= lastRange[0]
407                       && range[0] >= lastRange[1]);
408       if (sameDirection && (overlapping || extending))
409       {
410         lastRange[1] = range[1];
411         changed = true;
412       }
413       else
414       {
415         lastRange = new int[] { range[0], range[1] };
416         merged.add(lastRange);
417         // careful: merging [5, 5] after [7, 6] should keep negative direction
418         lastDirection = (range[1] == range[0]) ? lastDirection : direction;
419       }
420     }
421
422     return changed ? merged : ranges;
423   }
424
425   /**
426    * get all mapped positions from 'from' to 'to'
427    * 
428    * @return int[][] { int[] { fromStart, fromFinish, toStart, toFinish }, int
429    *         [fromFinish-fromStart+2] { toStart..toFinish mappings}}
430    */
431   protected int[][] makeFromMap()
432   {
433     // TODO not used - remove??
434     return posMap(fromShifts, fromRatio, toShifts, toRatio);
435   }
436
437   /**
438    * get all mapped positions from 'to' to 'from'
439    * 
440    * @return int[to position]=position mapped in from
441    */
442   protected int[][] makeToMap()
443   {
444     // TODO not used - remove??
445     return posMap(toShifts, toRatio, fromShifts, fromRatio);
446   }
447
448   /**
449    * construct an int map for intervals in intVals
450    * 
451    * @param shiftTo
452    * @return int[] { from, to pos in range }, int[range.to-range.from+1]
453    *         returning mapped position
454    */
455   private int[][] posMap(List<int[]> shiftTo, int ratio,
456           List<int[]> shiftFrom, int toRatio)
457   {
458     // TODO not used - remove??
459     int iv = 0, ivSize = shiftTo.size();
460     if (iv >= ivSize)
461     {
462       return null;
463     }
464     int[] intv = shiftTo.get(iv++);
465     int from = intv[0], to = intv[1];
466     if (from > to)
467     {
468       from = intv[1];
469       to = intv[0];
470     }
471     while (iv < ivSize)
472     {
473       intv = shiftTo.get(iv++);
474       if (intv[0] < from)
475       {
476         from = intv[0];
477       }
478       if (intv[1] < from)
479       {
480         from = intv[1];
481       }
482       if (intv[0] > to)
483       {
484         to = intv[0];
485       }
486       if (intv[1] > to)
487       {
488         to = intv[1];
489       }
490     }
491     int tF = 0, tT = 0;
492     int mp[][] = new int[to - from + 2][];
493     for (int i = 0; i < mp.length; i++)
494     {
495       int[] m = shift(i + from, shiftTo, ratio, shiftFrom, toRatio);
496       if (m != null)
497       {
498         if (i == 0)
499         {
500           tF = tT = m[0];
501         }
502         else
503         {
504           if (m[0] < tF)
505           {
506             tF = m[0];
507           }
508           if (m[0] > tT)
509           {
510             tT = m[0];
511           }
512         }
513       }
514       mp[i] = m;
515     }
516     int[][] map = new int[][] { new int[] { from, to, tF, tT },
517         new int[to - from + 2] };
518
519     map[0][2] = tF;
520     map[0][3] = tT;
521
522     for (int i = 0; i < mp.length; i++)
523     {
524       if (mp[i] != null)
525       {
526         map[1][i] = mp[i][0] - tF;
527       }
528       else
529       {
530         map[1][i] = -1; // indicates an out of range mapping
531       }
532     }
533     return map;
534   }
535
536   /**
537    * addShift
538    * 
539    * @param pos
540    *          start position for shift (in original reference frame)
541    * @param shift
542    *          length of shift
543    * 
544    *          public void addShift(int pos, int shift) { int sidx = 0; int[]
545    *          rshift=null; while (sidx<shifts.size() && (rshift=(int[])
546    *          shifts.elementAt(sidx))[0]<pos) sidx++; if (sidx==shifts.size())
547    *          shifts.insertElementAt(new int[] { pos, shift}, sidx); else
548    *          rshift[1]+=shift; }
549    */
550
551   /**
552    * shift from pos to To(pos)
553    * 
554    * @param pos
555    *          int
556    * @return int shifted position in To, frameshift in From, direction of mapped
557    *         symbol in To
558    */
559   public int[] shiftFrom(int pos)
560   {
561     return shift(pos, fromShifts, fromRatio, toShifts, toRatio);
562   }
563
564   /**
565    * inverse of shiftFrom - maps pos in To to a position in From
566    * 
567    * @param pos
568    *          (in To)
569    * @return shifted position in From, frameshift in To, direction of mapped
570    *         symbol in From
571    */
572   public int[] shiftTo(int pos)
573   {
574     return shift(pos, toShifts, toRatio, fromShifts, fromRatio);
575   }
576
577   /**
578    * 
579    * @param shiftTo
580    * @param fromRatio
581    * @param shiftFrom
582    * @param toRatio
583    * @return
584    */
585   protected static int[] shift(int pos, List<int[]> shiftTo, int fromRatio,
586           List<int[]> shiftFrom, int toRatio)
587   {
588     // TODO: javadoc; tests
589     int[] fromCount = countPos(shiftTo, pos);
590     if (fromCount == null)
591     {
592       return null;
593     }
594     int fromRemainder = (fromCount[0] - 1) % fromRatio;
595     int toCount = 1 + (((fromCount[0] - 1) / fromRatio) * toRatio);
596     int[] toPos = countToPos(shiftFrom, toCount);
597     if (toPos == null)
598     {
599       return null; // throw new Error("Bad Mapping!");
600     }
601     // System.out.println(fromCount[0]+" "+fromCount[1]+" "+toCount);
602     return new int[] { toPos[0], fromRemainder, toPos[1] };
603   }
604
605   /**
606    * count how many positions pos is along the series of intervals.
607    * 
608    * @param shiftTo
609    * @param pos
610    * @return number of positions or null if pos is not within intervals
611    */
612   protected static int[] countPos(List<int[]> shiftTo, int pos)
613   {
614     int count = 0, intv[], iv = 0, ivSize = shiftTo.size();
615     while (iv < ivSize)
616     {
617       intv = shiftTo.get(iv++);
618       if (intv[0] <= intv[1])
619       {
620         if (pos >= intv[0] && pos <= intv[1])
621         {
622           return new int[] { count + pos - intv[0] + 1, +1 };
623         }
624         else
625         {
626           count += intv[1] - intv[0] + 1;
627         }
628       }
629       else
630       {
631         if (pos >= intv[1] && pos <= intv[0])
632         {
633           return new int[] { count + intv[0] - pos + 1, -1 };
634         }
635         else
636         {
637           count += intv[0] - intv[1] + 1;
638         }
639       }
640     }
641     return null;
642   }
643
644   /**
645    * count out pos positions into a series of intervals and return the position
646    * 
647    * @param shiftFrom
648    * @param pos
649    * @return position pos in interval set
650    */
651   protected static int[] countToPos(List<int[]> shiftFrom, int pos)
652   {
653     int count = 0, diff = 0, iv = 0, ivSize = shiftFrom.size();
654     int[] intv = { 0, 0 };
655     while (iv < ivSize)
656     {
657       intv = shiftFrom.get(iv++);
658       diff = intv[1] - intv[0];
659       if (diff >= 0)
660       {
661         if (pos <= count + 1 + diff)
662         {
663           return new int[] { pos - count - 1 + intv[0], +1 };
664         }
665         else
666         {
667           count += 1 + diff;
668         }
669       }
670       else
671       {
672         if (pos <= count + 1 - diff)
673         {
674           return new int[] { intv[0] - (pos - count - 1), -1 };
675         }
676         else
677         {
678           count += 1 - diff;
679         }
680       }
681     }
682     return null;// (diff<0) ? (intv[1]-1) : (intv[0]+1);
683   }
684
685   /**
686    * find series of intervals mapping from start-end in the From map.
687    * 
688    * @param start
689    *          position mapped 'to'
690    * @param end
691    *          position mapped 'to'
692    * @return series of [start, end] ranges in sequence mapped 'from'
693    */
694   public int[] locateInFrom(int start, int end)
695   {
696     // inefficient implementation
697     int fromStart[] = shiftTo(start);
698     // needs to be inclusive of end of symbol position
699     int fromEnd[] = shiftTo(end);
700
701     return getIntervals(fromShifts, fromStart, fromEnd, fromRatio);
702   }
703
704   /**
705    * find series of intervals mapping from start-end in the to map.
706    * 
707    * @param start
708    *          position mapped 'from'
709    * @param end
710    *          position mapped 'from'
711    * @return series of [start, end] ranges in sequence mapped 'to'
712    */
713   public int[] locateInTo(int start, int end)
714   {
715     int toStart[] = shiftFrom(start);
716     int toEnd[] = shiftFrom(end);
717     return getIntervals(toShifts, toStart, toEnd, toRatio);
718   }
719
720   /**
721    * like shift - except returns the intervals in the given vector of shifts
722    * which were spanned in traversing fromStart to fromEnd
723    * 
724    * @param shiftFrom
725    * @param fromStart
726    * @param fromEnd
727    * @param fromRatio2
728    * @return series of from,to intervals from from first position of starting
729    *         region to final position of ending region inclusive
730    */
731   protected static int[] getIntervals(List<int[]> shiftFrom,
732           int[] fromStart, int[] fromEnd, int fromRatio2)
733   {
734     if (fromStart == null || fromEnd == null)
735     {
736       return null;
737     }
738     int startpos, endpos;
739     startpos = fromStart[0]; // first position in fromStart
740     endpos = fromEnd[0]; // last position in fromEnd
741     int endindx = (fromRatio2 - 1); // additional positions to get to last
742     // position from endpos
743     int intv = 0, intvSize = shiftFrom.size();
744     int iv[], i = 0, fs = -1, fe_s = -1, fe = -1; // containing intervals
745     // search intervals to locate ones containing startpos and count endindx
746     // positions on from endpos
747     while (intv < intvSize && (fs == -1 || fe == -1))
748     {
749       iv = shiftFrom.get(intv++);
750       if (fe_s > -1)
751       {
752         endpos = iv[0]; // start counting from beginning of interval
753         endindx--; // inclusive of endpos
754       }
755       if (iv[0] <= iv[1])
756       {
757         if (fs == -1 && startpos >= iv[0] && startpos <= iv[1])
758         {
759           fs = i;
760         }
761         if (endpos >= iv[0] && endpos <= iv[1])
762         {
763           if (fe_s == -1)
764           {
765             fe_s = i;
766           }
767           if (fe_s != -1)
768           {
769             if (endpos + endindx <= iv[1])
770             {
771               fe = i;
772               endpos = endpos + endindx; // end of end token is within this
773               // interval
774             }
775             else
776             {
777               endindx -= iv[1] - endpos; // skip all this interval too
778             }
779           }
780         }
781       }
782       else
783       {
784         if (fs == -1 && startpos <= iv[0] && startpos >= iv[1])
785         {
786           fs = i;
787         }
788         if (endpos <= iv[0] && endpos >= iv[1])
789         {
790           if (fe_s == -1)
791           {
792             fe_s = i;
793           }
794           if (fe_s != -1)
795           {
796             if (endpos - endindx >= iv[1])
797             {
798               fe = i;
799               endpos = endpos - endindx; // end of end token is within this
800               // interval
801             }
802             else
803             {
804               endindx -= endpos - iv[1]; // skip all this interval too
805             }
806           }
807         }
808       }
809       i++;
810     }
811     if (fs == fe && fe == -1)
812     {
813       return null;
814     }
815     List<int[]> ranges = new ArrayList<>();
816     if (fs <= fe)
817     {
818       intv = fs;
819       i = fs;
820       // truncate initial interval
821       iv = shiftFrom.get(intv++);
822       iv = new int[] { iv[0], iv[1] };// clone
823       if (i == fs)
824       {
825         iv[0] = startpos;
826       }
827       while (i != fe)
828       {
829         ranges.add(iv); // add initial range
830         iv = shiftFrom.get(intv++); // get next interval
831         iv = new int[] { iv[0], iv[1] };// clone
832         i++;
833       }
834       if (i == fe)
835       {
836         iv[1] = endpos;
837       }
838       ranges.add(iv); // add only - or final range
839     }
840     else
841     {
842       // walk from end of interval.
843       i = shiftFrom.size() - 1;
844       while (i > fs)
845       {
846         i--;
847       }
848       iv = shiftFrom.get(i);
849       iv = new int[] { iv[1], iv[0] };// reverse and clone
850       // truncate initial interval
851       if (i == fs)
852       {
853         iv[0] = startpos;
854       }
855       while (--i != fe)
856       { // fix apparent logic bug when fe==-1
857         ranges.add(iv); // add (truncated) reversed interval
858         iv = shiftFrom.get(i);
859         iv = new int[] { iv[1], iv[0] }; // reverse and clone
860       }
861       if (i == fe)
862       {
863         // interval is already reversed
864         iv[1] = endpos;
865       }
866       ranges.add(iv); // add only - or final range
867     }
868     // create array of start end intervals.
869     int[] range = null;
870     if (ranges != null && ranges.size() > 0)
871     {
872       range = new int[ranges.size() * 2];
873       intv = 0;
874       intvSize = ranges.size();
875       i = 0;
876       while (intv < intvSize)
877       {
878         iv = ranges.get(intv);
879         range[i++] = iv[0];
880         range[i++] = iv[1];
881         ranges.set(intv++, null); // remove
882       }
883     }
884     return range;
885   }
886
887   /**
888    * get the 'initial' position of mpos in To
889    * 
890    * @param mpos
891    *          position in from
892    * @return position of first word in to reference frame
893    */
894   public int getToPosition(int mpos)
895   {
896     // TODO not used - remove??
897     int[] mp = shiftTo(mpos);
898     if (mp != null)
899     {
900       return mp[0];
901     }
902     return mpos;
903   }
904
905   /**
906    * get range of positions in To frame for the mpos word in From
907    * 
908    * @param mpos
909    *          position in From
910    * @return null or int[] first position in To for mpos, last position in to
911    *         for Mpos
912    */
913   public int[] getToWord(int mpos)
914   {
915     int[] mp = shiftTo(mpos);
916     if (mp != null)
917     {
918       return new int[] { mp[0], mp[0] + mp[2] * (getFromRatio() - 1) };
919     }
920     return null;
921   }
922
923   /**
924    * get From position in the associated reference frame for position pos in the
925    * associated sequence.
926    * 
927    * @param pos
928    * @return
929    */
930   public int getMappedPosition(int pos)
931   {
932     // TODO not used - remove??
933     int[] mp = shiftFrom(pos);
934     if (mp != null)
935     {
936       return mp[0];
937     }
938     return pos;
939   }
940
941   public int[] getMappedWord(int pos)
942   {
943     // TODO not used - remove??
944     int[] mp = shiftFrom(pos);
945     if (mp != null)
946     {
947       return new int[] { mp[0], mp[0] + mp[2] * (getToRatio() - 1) };
948     }
949     return null;
950   }
951
952   /**
953    * 
954    * @return a MapList whose From range is this maplist's To Range, and vice
955    *         versa
956    */
957   public MapList getInverse()
958   {
959     return new MapList(getToRanges(), getFromRanges(), getToRatio(),
960             getFromRatio());
961   }
962
963   /**
964    * test for containment rather than equivalence to another mapping
965    * 
966    * @param map
967    *          to be tested for containment
968    * @return true if local or mapped range map contains or is contained by this
969    *         mapping
970    */
971   public boolean containsEither(boolean local, MapList map)
972   {
973     // TODO not used - remove?
974     if (local)
975     {
976       return ((getFromLowest() >= map.getFromLowest()
977               && getFromHighest() <= map.getFromHighest())
978               || (getFromLowest() <= map.getFromLowest()
979                       && getFromHighest() >= map.getFromHighest()));
980     }
981     else
982     {
983       return ((getToLowest() >= map.getToLowest()
984               && getToHighest() <= map.getToHighest())
985               || (getToLowest() <= map.getToLowest()
986                       && getToHighest() >= map.getToHighest()));
987     }
988   }
989
990   /**
991    * String representation - for debugging, not guaranteed not to change
992    */
993   @Override
994   public String toString()
995   {
996     StringBuilder sb = new StringBuilder(64);
997     sb.append("[");
998     for (int[] shift : fromShifts)
999     {
1000       sb.append(" ").append(Arrays.toString(shift));
1001     }
1002     sb.append(" ] ");
1003     sb.append(fromRatio).append(":").append(toRatio);
1004     sb.append(" to [");
1005     for (int[] shift : toShifts)
1006     {
1007       sb.append(" ").append(Arrays.toString(shift));
1008     }
1009     sb.append(" ]");
1010     return sb.toString();
1011   }
1012
1013   /**
1014    * Extend this map list by adding the given map's ranges. There is no
1015    * validation check that the ranges do not overlap existing ranges (or each
1016    * other), but contiguous ranges are merged.
1017    * 
1018    * @param map
1019    */
1020   public void addMapList(MapList map)
1021   {
1022     if (this.equals(map))
1023     {
1024       return;
1025     }
1026     this.fromLowest = Math.min(fromLowest, map.fromLowest);
1027     this.toLowest = Math.min(toLowest, map.toLowest);
1028     this.fromHighest = Math.max(fromHighest, map.fromHighest);
1029     this.toHighest = Math.max(toHighest, map.toHighest);
1030
1031     for (int[] range : map.getFromRanges())
1032     {
1033       addRange(range, fromShifts);
1034     }
1035     for (int[] range : map.getToRanges())
1036     {
1037       addRange(range, toShifts);
1038     }
1039   }
1040
1041   /**
1042    * Adds the given range to a list of ranges. If the new range just extends
1043    * existing ranges, the current endpoint is updated instead.
1044    * 
1045    * @param range
1046    * @param addTo
1047    */
1048   static void addRange(int[] range, List<int[]> addTo)
1049   {
1050     /*
1051      * list is empty - add to it!
1052      */
1053     if (addTo.size() == 0)
1054     {
1055       addTo.add(range);
1056       return;
1057     }
1058
1059     int[] last = addTo.get(addTo.size() - 1);
1060     boolean lastForward = last[1] >= last[0];
1061     boolean newForward = range[1] >= range[0];
1062
1063     /*
1064      * contiguous range in the same direction - just update endpoint
1065      */
1066     if (lastForward == newForward && last[1] == range[0])
1067     {
1068       last[1] = range[1];
1069       return;
1070     }
1071
1072     /*
1073      * next range starts at +1 in forward sense - update endpoint
1074      */
1075     if (lastForward && newForward && range[0] == last[1] + 1)
1076     {
1077       last[1] = range[1];
1078       return;
1079     }
1080
1081     /*
1082      * next range starts at -1 in reverse sense - update endpoint
1083      */
1084     if (!lastForward && !newForward && range[0] == last[1] - 1)
1085     {
1086       last[1] = range[1];
1087       return;
1088     }
1089
1090     /*
1091      * just add the new range
1092      */
1093     addTo.add(range);
1094   }
1095
1096   /**
1097    * Returns true if mapping is from forward strand, false if from reverse
1098    * strand. Result is just based on the first 'from' range that is not a single
1099    * position. Default is true unless proven to be false. Behaviour is not well
1100    * defined if the mapping has a mixture of forward and reverse ranges.
1101    * 
1102    * @return
1103    */
1104   public boolean isFromForwardStrand()
1105   {
1106     boolean forwardStrand = true;
1107     for (int[] range : getFromRanges())
1108     {
1109       if (range[1] > range[0])
1110       {
1111         break; // forward strand confirmed
1112       }
1113       else if (range[1] < range[0])
1114       {
1115         forwardStrand = false;
1116         break; // reverse strand confirmed
1117       }
1118     }
1119     return forwardStrand;
1120   }
1121
1122   /**
1123    * 
1124    * @return true if from, or to is a three to 1 mapping
1125    */
1126   public boolean isTripletMap()
1127   {
1128     return (toRatio == 3 && fromRatio == 1)
1129             || (fromRatio == 3 && toRatio == 1);
1130   }
1131
1132 }