JAL-3761 failing test, formatting, method renamed
[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    * There is no validation check that the ranges do not overlap each other.
213    * 
214    * @param from
215    *          contiguous regions as [start1, end1, start2, end2, ...]
216    * @param to
217    *          same format as 'from'
218    * @param fromRatio
219    *          phrase length in 'from' (e.g. 3 for dna)
220    * @param toRatio
221    *          phrase length in 'to' (e.g. 1 for protein)
222    */
223   public MapList(int from[], int to[], int fromRatio, int toRatio)
224   {
225     this();
226     this.fromRatio = fromRatio;
227     this.toRatio = toRatio;
228     fromLowest = Integer.MAX_VALUE;
229     fromHighest = Integer.MIN_VALUE;
230
231     for (int i = 0; i < from.length; i += 2)
232     {
233       /*
234        * note lowest and highest values - bearing in mind the
235        * direction may be reversed
236        */
237       fromLowest = Math.min(fromLowest, Math.min(from[i], from[i + 1]));
238       fromHighest = Math.max(fromHighest, Math.max(from[i], from[i + 1]));
239       fromShifts.add(new int[] { from[i], from[i + 1] });
240     }
241
242     toLowest = Integer.MAX_VALUE;
243     toHighest = Integer.MIN_VALUE;
244     for (int i = 0; i < to.length; i += 2)
245     {
246       toLowest = Math.min(toLowest, Math.min(to[i], to[i + 1]));
247       toHighest = Math.max(toHighest, Math.max(to[i], to[i + 1]));
248       toShifts.add(new int[] { to[i], to[i + 1] });
249     }
250   }
251
252   /**
253    * Copy constructor. Creates an identical mapping.
254    * 
255    * @param map
256    */
257   public MapList(MapList map)
258   {
259     this();
260     // TODO not used - remove?
261     this.fromLowest = map.fromLowest;
262     this.fromHighest = map.fromHighest;
263     this.toLowest = map.toLowest;
264     this.toHighest = map.toHighest;
265
266     this.fromRatio = map.fromRatio;
267     this.toRatio = map.toRatio;
268     if (map.fromShifts != null)
269     {
270       for (int[] r : map.fromShifts)
271       {
272         fromShifts.add(new int[] { r[0], r[1] });
273       }
274     }
275     if (map.toShifts != null)
276     {
277       for (int[] r : map.toShifts)
278       {
279         toShifts.add(new int[] { r[0], r[1] });
280       }
281     }
282   }
283
284   /**
285    * Constructor given ranges as lists of [start, end] positions. There is no
286    * validation check that the ranges do not overlap each other.
287    * 
288    * @param fromRange
289    * @param toRange
290    * @param fromRatio
291    * @param toRatio
292    */
293   public MapList(List<int[]> fromRange, List<int[]> toRange, int fromRatio,
294           int toRatio)
295   {
296     this();
297     fromRange = coalesceRanges(fromRange);
298     toRange = coalesceRanges(toRange);
299     this.fromShifts = fromRange;
300     this.toShifts = toRange;
301     this.fromRatio = fromRatio;
302     this.toRatio = toRatio;
303
304     fromLowest = Integer.MAX_VALUE;
305     fromHighest = Integer.MIN_VALUE;
306     for (int[] range : fromRange)
307     {
308       if (range.length != 2)
309       {
310         // throw new IllegalArgumentException(range);
311         System.err.println("Invalid format for fromRange "
312                 + Arrays.toString(range) + " may cause errors");
313       }
314       fromLowest = Math.min(fromLowest, Math.min(range[0], range[1]));
315       fromHighest = Math.max(fromHighest, Math.max(range[0], range[1]));
316     }
317
318     toLowest = Integer.MAX_VALUE;
319     toHighest = Integer.MIN_VALUE;
320     for (int[] range : toRange)
321     {
322       if (range.length != 2)
323       {
324         // throw new IllegalArgumentException(range);
325         System.err.println("Invalid format for toRange "
326                 + Arrays.toString(range) + " may cause errors");
327       }
328       toLowest = Math.min(toLowest, Math.min(range[0], range[1]));
329       toHighest = Math.max(toHighest, Math.max(range[0], range[1]));
330     }
331   }
332
333   /**
334    * Consolidates a list of ranges so that any contiguous ranges are merged.
335    * This assumes the ranges are already in start order (does not sort them).
336    * <p>
337    * The main use case for this method is when mapping cDNA sequence to its
338    * protein product, based on CDS feature ranges which derive from spliced
339    * exons, but are contiguous on the cDNA sequence. For example
340    * 
341    * <pre>
342    *   CDS 1-20  // from exon1
343    *   CDS 21-35 // from exon2
344    *   CDS 36-71 // from exon3
345    * 'coalesce' to range 1-71
346    * </pre>
347    * 
348    * @param ranges
349    * @return the same list (if unchanged), else a new merged list, leaving the
350    *         input list unchanged
351    */
352   public static List<int[]> coalesceRanges(final List<int[]> ranges)
353   {
354     if (ranges == null || ranges.size() < 2)
355     {
356       return ranges;
357     }
358
359     boolean changed = false;
360     List<int[]> merged = new ArrayList<>();
361     int[] lastRange = ranges.get(0);
362     int lastDirection = lastRange[1] >= lastRange[0] ? 1 : -1;
363     lastRange = new int[] { lastRange[0], lastRange[1] };
364     merged.add(lastRange);
365     boolean first = true;
366
367     for (final int[] range : ranges)
368     {
369       if (first)
370       {
371         first = false;
372         continue;
373       }
374
375       int direction = range[1] >= range[0] ? 1 : -1;
376
377       /*
378        * if next range is in the same direction as last and contiguous,
379        * just update the end position of the last range
380        */
381       boolean sameDirection = range[1] == range[0]
382               || direction == lastDirection;
383       boolean extending = range[0] == lastRange[1] + lastDirection;
384       if (sameDirection && extending)
385       {
386         lastRange[1] = range[1];
387         changed = true;
388       }
389       else
390       {
391         lastRange = new int[] { range[0], range[1] };
392         merged.add(lastRange);
393         // careful: merging [5, 5] after [7, 6] should keep negative direction
394         lastDirection = (range[1] == range[0]) ? lastDirection : direction;
395       }
396     }
397
398     return changed ? merged : ranges;
399   }
400
401   /**
402    * get all mapped positions from 'from' to 'to'
403    * 
404    * @return int[][] { int[] { fromStart, fromFinish, toStart, toFinish }, int
405    *         [fromFinish-fromStart+2] { toStart..toFinish mappings}}
406    */
407   protected int[][] makeFromMap()
408   {
409     // TODO not used - remove??
410     return posMap(fromShifts, fromRatio, toShifts, toRatio);
411   }
412
413   /**
414    * get all mapped positions from 'to' to 'from'
415    * 
416    * @return int[to position]=position mapped in from
417    */
418   protected int[][] makeToMap()
419   {
420     // TODO not used - remove??
421     return posMap(toShifts, toRatio, fromShifts, fromRatio);
422   }
423
424   /**
425    * construct an int map for intervals in intVals
426    * 
427    * @param shiftTo
428    * @return int[] { from, to pos in range }, int[range.to-range.from+1]
429    *         returning mapped position
430    */
431   private int[][] posMap(List<int[]> shiftTo, int ratio,
432           List<int[]> shiftFrom, int toRatio)
433   {
434     // TODO not used - remove??
435     int iv = 0, ivSize = shiftTo.size();
436     if (iv >= ivSize)
437     {
438       return null;
439     }
440     int[] intv = shiftTo.get(iv++);
441     int from = intv[0], to = intv[1];
442     if (from > to)
443     {
444       from = intv[1];
445       to = intv[0];
446     }
447     while (iv < ivSize)
448     {
449       intv = shiftTo.get(iv++);
450       if (intv[0] < from)
451       {
452         from = intv[0];
453       }
454       if (intv[1] < from)
455       {
456         from = intv[1];
457       }
458       if (intv[0] > to)
459       {
460         to = intv[0];
461       }
462       if (intv[1] > to)
463       {
464         to = intv[1];
465       }
466     }
467     int tF = 0, tT = 0;
468     int mp[][] = new int[to - from + 2][];
469     for (int i = 0; i < mp.length; i++)
470     {
471       int[] m = shift(i + from, shiftTo, ratio, shiftFrom, toRatio);
472       if (m != null)
473       {
474         if (i == 0)
475         {
476           tF = tT = m[0];
477         }
478         else
479         {
480           if (m[0] < tF)
481           {
482             tF = m[0];
483           }
484           if (m[0] > tT)
485           {
486             tT = m[0];
487           }
488         }
489       }
490       mp[i] = m;
491     }
492     int[][] map = new int[][] { new int[] { from, to, tF, tT },
493         new int[to - from + 2] };
494
495     map[0][2] = tF;
496     map[0][3] = tT;
497
498     for (int i = 0; i < mp.length; i++)
499     {
500       if (mp[i] != null)
501       {
502         map[1][i] = mp[i][0] - tF;
503       }
504       else
505       {
506         map[1][i] = -1; // indicates an out of range mapping
507       }
508     }
509     return map;
510   }
511
512   /**
513    * addShift
514    * 
515    * @param pos
516    *          start position for shift (in original reference frame)
517    * @param shift
518    *          length of shift
519    * 
520    *          public void addShift(int pos, int shift) { int sidx = 0; int[]
521    *          rshift=null; while (sidx<shifts.size() && (rshift=(int[])
522    *          shifts.elementAt(sidx))[0]<pos) sidx++; if (sidx==shifts.size())
523    *          shifts.insertElementAt(new int[] { pos, shift}, sidx); else
524    *          rshift[1]+=shift; }
525    */
526
527   /**
528    * shift from pos to To(pos)
529    * 
530    * @param pos
531    *          int
532    * @return int shifted position in To, frameshift in From, direction of mapped
533    *         symbol in To
534    */
535   public int[] shiftFrom(int pos)
536   {
537     return shift(pos, fromShifts, fromRatio, toShifts, toRatio);
538   }
539
540   /**
541    * inverse of shiftFrom - maps pos in To to a position in From
542    * 
543    * @param pos
544    *          (in To)
545    * @return shifted position in From, frameshift in To, direction of mapped
546    *         symbol in From
547    */
548   public int[] shiftTo(int pos)
549   {
550     return shift(pos, toShifts, toRatio, fromShifts, fromRatio);
551   }
552
553   /**
554    * 
555    * @param shiftTo
556    * @param fromRatio
557    * @param shiftFrom
558    * @param toRatio
559    * @return
560    */
561   protected static int[] shift(int pos, List<int[]> shiftTo, int fromRatio,
562           List<int[]> shiftFrom, int toRatio)
563   {
564     // TODO: javadoc; tests
565     int[] fromCount = countPos(shiftTo, pos);
566     if (fromCount == null)
567     {
568       return null;
569     }
570     int fromRemainder = (fromCount[0] - 1) % fromRatio;
571     int toCount = 1 + (((fromCount[0] - 1) / fromRatio) * toRatio);
572     int[] toPos = traverseToPosition(shiftFrom, toCount);
573     if (toPos == null)
574     {
575       return null; // throw new Error("Bad Mapping!");
576     }
577     // System.out.println(fromCount[0]+" "+fromCount[1]+" "+toCount);
578     return new int[] { toPos[0], fromRemainder, toPos[1] };
579   }
580
581   /**
582    * count how many positions pos is along the series of intervals.
583    * 
584    * @param shiftTo
585    * @param pos
586    * @return number of positions or null if pos is not within intervals
587    */
588   protected static int[] countPos(List<int[]> shiftTo, int pos)
589   {
590     int count = 0, intv[], iv = 0, ivSize = shiftTo.size();
591     while (iv < ivSize)
592     {
593       intv = shiftTo.get(iv++);
594       if (intv[0] <= intv[1])
595       {
596         if (pos >= intv[0] && pos <= intv[1])
597         {
598           return new int[] { count + pos - intv[0] + 1, +1 };
599         }
600         else
601         {
602           count += intv[1] - intv[0] + 1;
603         }
604       }
605       else
606       {
607         if (pos >= intv[1] && pos <= intv[0])
608         {
609           return new int[] { count + intv[0] - pos + 1, -1 };
610         }
611         else
612         {
613           count += intv[0] - intv[1] + 1;
614         }
615       }
616     }
617     return null;
618   }
619
620   /**
621    * Reads through the given intervals until {@code count} positions have been
622    * traversed, and returns an array consisting of two values:
623    * <ul>
624    * <li>the value at the {@code count'th} position</li>
625    * <li>+1 if the last interval read is forwards, -1 if reverse direction</li>
626    * </ul>
627    * Returns null if the ranges include less than {@code count} positions, or if
628    * {@code count < 1}.
629    * 
630    * @param intervals
631    *          a list of [start, end] ranges
632    * @param count
633    *          the number of positions to traverse
634    * @return
635    */
636   protected static int[] traverseToPosition(List<int[]> intervals,
637           final int count)
638   {
639     int traversed = 0;
640     int ivSize = intervals.size();
641     int iv = 0;
642
643     if (count < 1)
644     {
645       return null;
646     }
647
648     while (iv < ivSize)
649     {
650       int[] intv = intervals.get(iv++);
651       int diff = intv[1] - intv[0];
652       if (diff >= 0)
653       {
654         if (count <= traversed + 1 + diff)
655         {
656           return new int[] { intv[0] + (count - traversed - 1), +1 };
657         }
658         else
659         {
660           traversed += 1 + diff;
661         }
662       }
663       else
664       {
665         if (count <= traversed + 1 - diff)
666         {
667           return new int[] { intv[0] - (count - traversed - 1), -1 };
668         }
669         else
670         {
671           traversed += 1 - diff;
672         }
673       }
674     }
675     return null;
676   }
677
678   /**
679    * find series of intervals mapping from start-end in the From map.
680    * 
681    * @param start
682    *          position mapped 'to'
683    * @param end
684    *          position mapped 'to'
685    * @return series of [start, end] ranges in sequence mapped 'from'
686    */
687   public int[] locateInFrom(int start, int end)
688   {
689     // inefficient implementation
690     int fromStart[] = shiftTo(start);
691     // needs to be inclusive of end of symbol position
692     int fromEnd[] = shiftTo(end);
693
694     return getIntervals(fromShifts, fromStart, fromEnd, fromRatio);
695   }
696
697   /**
698    * find series of intervals mapping from start-end in the to map.
699    * 
700    * @param start
701    *          position mapped 'from'
702    * @param end
703    *          position mapped 'from'
704    * @return series of [start, end] ranges in sequence mapped 'to'
705    */
706   public int[] locateInTo(int start, int end)
707   {
708     int toStart[] = shiftFrom(start);
709     int toEnd[] = shiftFrom(end);
710     return getIntervals(toShifts, toStart, toEnd, toRatio);
711   }
712
713   /**
714    * like shift - except returns the intervals in the given vector of shifts
715    * which were spanned in traversing fromStart to fromEnd
716    * 
717    * @param shiftFrom
718    * @param fromStart
719    * @param fromEnd
720    * @param fromRatio2
721    * @return series of from,to intervals from from first position of starting
722    *         region to final position of ending region inclusive
723    */
724   protected static int[] getIntervals(List<int[]> shiftFrom,
725           int[] fromStart, int[] fromEnd, int fromRatio2)
726   {
727     if (fromStart == null || fromEnd == null)
728     {
729       return null;
730     }
731     int startpos, endpos;
732     startpos = fromStart[0]; // first position in fromStart
733     endpos = fromEnd[0]; // last position in fromEnd
734     int endindx = (fromRatio2 - 1); // additional positions to get to last
735     // position from endpos
736     int intv = 0, intvSize = shiftFrom.size();
737     int iv[], i = 0, fs = -1, fe_s = -1, fe = -1; // containing intervals
738     // search intervals to locate ones containing startpos and count endindx
739     // positions on from endpos
740     while (intv < intvSize && (fs == -1 || fe == -1))
741     {
742       iv = shiftFrom.get(intv++);
743       if (fe_s > -1)
744       {
745         endpos = iv[0]; // start counting from beginning of interval
746         endindx--; // inclusive of endpos
747       }
748       if (iv[0] <= iv[1])
749       {
750         if (fs == -1 && startpos >= iv[0] && startpos <= iv[1])
751         {
752           fs = i;
753         }
754         if (endpos >= iv[0] && endpos <= iv[1])
755         {
756           if (fe_s == -1)
757           {
758             fe_s = i;
759           }
760           if (fe_s != -1)
761           {
762             if (endpos + endindx <= iv[1])
763             {
764               fe = i;
765               endpos = endpos + endindx; // end of end token is within this
766               // interval
767             }
768             else
769             {
770               endindx -= iv[1] - endpos; // skip all this interval too
771             }
772           }
773         }
774       }
775       else
776       {
777         if (fs == -1 && startpos <= iv[0] && startpos >= iv[1])
778         {
779           fs = i;
780         }
781         if (endpos <= iv[0] && endpos >= iv[1])
782         {
783           if (fe_s == -1)
784           {
785             fe_s = i;
786           }
787           if (fe_s != -1)
788           {
789             if (endpos - endindx >= iv[1])
790             {
791               fe = i;
792               endpos = endpos - endindx; // end of end token is within this
793               // interval
794             }
795             else
796             {
797               endindx -= endpos - iv[1]; // skip all this interval too
798             }
799           }
800         }
801       }
802       i++;
803     }
804     if (fs == fe && fe == -1)
805     {
806       return null;
807     }
808     List<int[]> ranges = new ArrayList<>();
809     if (fs <= fe)
810     {
811       intv = fs;
812       i = fs;
813       // truncate initial interval
814       iv = shiftFrom.get(intv++);
815       iv = new int[] { iv[0], iv[1] };// clone
816       if (i == fs)
817       {
818         iv[0] = startpos;
819       }
820       while (i != fe)
821       {
822         ranges.add(iv); // add initial range
823         iv = shiftFrom.get(intv++); // get next interval
824         iv = new int[] { iv[0], iv[1] };// clone
825         i++;
826       }
827       if (i == fe)
828       {
829         iv[1] = endpos;
830       }
831       ranges.add(iv); // add only - or final range
832     }
833     else
834     {
835       // walk from end of interval.
836       i = shiftFrom.size() - 1;
837       while (i > fs)
838       {
839         i--;
840       }
841       iv = shiftFrom.get(i);
842       iv = new int[] { iv[1], iv[0] };// reverse and clone
843       // truncate initial interval
844       if (i == fs)
845       {
846         iv[0] = startpos;
847       }
848       while (--i != fe)
849       { // fix apparent logic bug when fe==-1
850         ranges.add(iv); // add (truncated) reversed interval
851         iv = shiftFrom.get(i);
852         iv = new int[] { iv[1], iv[0] }; // reverse and clone
853       }
854       if (i == fe)
855       {
856         // interval is already reversed
857         iv[1] = endpos;
858       }
859       ranges.add(iv); // add only - or final range
860     }
861     // create array of start end intervals.
862     int[] range = null;
863     if (ranges != null && ranges.size() > 0)
864     {
865       range = new int[ranges.size() * 2];
866       intv = 0;
867       intvSize = ranges.size();
868       i = 0;
869       while (intv < intvSize)
870       {
871         iv = ranges.get(intv);
872         range[i++] = iv[0];
873         range[i++] = iv[1];
874         ranges.set(intv++, null); // remove
875       }
876     }
877     return range;
878   }
879
880   /**
881    * get the 'initial' position of mpos in To
882    * 
883    * @param mpos
884    *          position in from
885    * @return position of first word in to reference frame
886    */
887   public int getToPosition(int mpos)
888   {
889     // TODO not used - remove??
890     int[] mp = shiftTo(mpos);
891     if (mp != null)
892     {
893       return mp[0];
894     }
895     return mpos;
896   }
897
898   /**
899    * get range of positions in To frame for the mpos word in From
900    * 
901    * @param mpos
902    *          position in From
903    * @return null or int[] first position in To for mpos, last position in to
904    *         for Mpos
905    */
906   public int[] getToWord(int mpos)
907   {
908     int[] mp = shiftTo(mpos);
909     if (mp != null)
910     {
911       return new int[] { mp[0], mp[0] + mp[2] * (getFromRatio() - 1) };
912     }
913     return null;
914   }
915
916   /**
917    * get From position in the associated reference frame for position pos in the
918    * associated sequence.
919    * 
920    * @param pos
921    * @return
922    */
923   public int getMappedPosition(int pos)
924   {
925     // TODO not used - remove??
926     int[] mp = shiftFrom(pos);
927     if (mp != null)
928     {
929       return mp[0];
930     }
931     return pos;
932   }
933
934   public int[] getMappedWord(int pos)
935   {
936     // TODO not used - remove??
937     int[] mp = shiftFrom(pos);
938     if (mp != null)
939     {
940       return new int[] { mp[0], mp[0] + mp[2] * (getToRatio() - 1) };
941     }
942     return null;
943   }
944
945   /**
946    * 
947    * @return a MapList whose From range is this maplist's To Range, and vice
948    *         versa
949    */
950   public MapList getInverse()
951   {
952     return new MapList(getToRanges(), getFromRanges(), getToRatio(),
953             getFromRatio());
954   }
955
956   /**
957    * test for containment rather than equivalence to another mapping
958    * 
959    * @param map
960    *          to be tested for containment
961    * @return true if local or mapped range map contains or is contained by this
962    *         mapping
963    */
964   public boolean containsEither(boolean local, MapList map)
965   {
966     // TODO not used - remove?
967     if (local)
968     {
969       return ((getFromLowest() >= map.getFromLowest()
970               && getFromHighest() <= map.getFromHighest())
971               || (getFromLowest() <= map.getFromLowest()
972                       && getFromHighest() >= map.getFromHighest()));
973     }
974     else
975     {
976       return ((getToLowest() >= map.getToLowest()
977               && getToHighest() <= map.getToHighest())
978               || (getToLowest() <= map.getToLowest()
979                       && getToHighest() >= map.getToHighest()));
980     }
981   }
982
983   /**
984    * String representation - for debugging, not guaranteed not to change
985    */
986   @Override
987   public String toString()
988   {
989     StringBuilder sb = new StringBuilder(64);
990     sb.append("[");
991     for (int[] shift : fromShifts)
992     {
993       sb.append(" ").append(Arrays.toString(shift));
994     }
995     sb.append(" ] ");
996     sb.append(fromRatio).append(":").append(toRatio);
997     sb.append(" to [");
998     for (int[] shift : toShifts)
999     {
1000       sb.append(" ").append(Arrays.toString(shift));
1001     }
1002     sb.append(" ]");
1003     return sb.toString();
1004   }
1005
1006   /**
1007    * Extend this map list by adding the given map's ranges. There is no
1008    * validation check that the ranges do not overlap existing ranges (or each
1009    * other), but contiguous ranges are merged.
1010    * 
1011    * @param map
1012    */
1013   public void addMapList(MapList map)
1014   {
1015     if (this.equals(map))
1016     {
1017       return;
1018     }
1019     this.fromLowest = Math.min(fromLowest, map.fromLowest);
1020     this.toLowest = Math.min(toLowest, map.toLowest);
1021     this.fromHighest = Math.max(fromHighest, map.fromHighest);
1022     this.toHighest = Math.max(toHighest, map.toHighest);
1023
1024     for (int[] range : map.getFromRanges())
1025     {
1026       addRange(range, fromShifts);
1027     }
1028     for (int[] range : map.getToRanges())
1029     {
1030       addRange(range, toShifts);
1031     }
1032   }
1033
1034   /**
1035    * Adds the given range to a list of ranges. If the new range just extends
1036    * existing ranges, the current endpoint is updated instead.
1037    * 
1038    * @param range
1039    * @param addTo
1040    */
1041   static void addRange(int[] range, List<int[]> addTo)
1042   {
1043     /*
1044      * list is empty - add to it!
1045      */
1046     if (addTo.size() == 0)
1047     {
1048       addTo.add(range);
1049       return;
1050     }
1051
1052     int[] last = addTo.get(addTo.size() - 1);
1053     boolean lastForward = last[1] >= last[0];
1054     boolean newForward = range[1] >= range[0];
1055
1056     /*
1057      * contiguous range in the same direction - just update endpoint
1058      */
1059     if (lastForward == newForward && last[1] == range[0])
1060     {
1061       last[1] = range[1];
1062       return;
1063     }
1064
1065     /*
1066      * next range starts at +1 in forward sense - update endpoint
1067      */
1068     if (lastForward && newForward && range[0] == last[1] + 1)
1069     {
1070       last[1] = range[1];
1071       return;
1072     }
1073
1074     /*
1075      * next range starts at -1 in reverse sense - update endpoint
1076      */
1077     if (!lastForward && !newForward && range[0] == last[1] - 1)
1078     {
1079       last[1] = range[1];
1080       return;
1081     }
1082
1083     /*
1084      * just add the new range
1085      */
1086     addTo.add(range);
1087   }
1088
1089   /**
1090    * Returns true if mapping is from forward strand, false if from reverse
1091    * strand. Result is just based on the first 'from' range that is not a single
1092    * position. Default is true unless proven to be false. Behaviour is not well
1093    * defined if the mapping has a mixture of forward and reverse ranges.
1094    * 
1095    * @return
1096    */
1097   public boolean isFromForwardStrand()
1098   {
1099     return isForwardStrand(getFromRanges());
1100   }
1101
1102   /**
1103    * Returns true if mapping is to forward strand, false if to reverse strand.
1104    * Result is just based on the first 'to' range that is not a single position.
1105    * Default is true unless proven to be false. Behaviour is not well defined if
1106    * the mapping has a mixture of forward and reverse ranges.
1107    * 
1108    * @return
1109    */
1110   public boolean isToForwardStrand()
1111   {
1112     return isForwardStrand(getToRanges());
1113   }
1114
1115   /**
1116    * A helper method that returns true unless at least one range has start >
1117    * end. Behaviour is undefined for a mixture of forward and reverse ranges.
1118    * 
1119    * @param ranges
1120    * @return
1121    */
1122   private boolean isForwardStrand(List<int[]> ranges)
1123   {
1124     boolean forwardStrand = true;
1125     for (int[] range : ranges)
1126     {
1127       if (range[1] > range[0])
1128       {
1129         break; // forward strand confirmed
1130       }
1131       else if (range[1] < range[0])
1132       {
1133         forwardStrand = false;
1134         break; // reverse strand confirmed
1135       }
1136     }
1137     return forwardStrand;
1138   }
1139
1140   /**
1141    * 
1142    * @return true if from, or to is a three to 1 mapping
1143    */
1144   public boolean isTripletMap()
1145   {
1146     return (toRatio == 3 && fromRatio == 1)
1147             || (fromRatio == 3 && toRatio == 1);
1148   }
1149
1150   /**
1151    * Returns a map which is the composite of this one and the input map. That
1152    * is, the output map has the fromRanges of this map, and its toRanges are the
1153    * toRanges of this map as transformed by the input map.
1154    * <p>
1155    * Returns null if the mappings cannot be traversed (not all toRanges of this
1156    * map correspond to fromRanges of the input), or if this.toRatio does not
1157    * match map.fromRatio.
1158    * 
1159    * <pre>
1160    * Example 1:
1161    *    this:   from [1-100] to [501-600]
1162    *    input:  from [10-40] to [60-90]
1163    *    output: from [10-40] to [560-590]
1164    * Example 2 ('reverse strand exons'):
1165    *    this:   from [1-100] to [2000-1951], [1000-951] // transcript to loci
1166    *    input:  from [1-50]  to [41-90] // CDS to transcript
1167    *    output: from [10-40] to [1960-1951], [1000-971] // CDS to gene loci
1168    * </pre>
1169    * 
1170    * @param map
1171    * @return
1172    */
1173   public MapList traverse(MapList map)
1174   {
1175     if (map == null)
1176     {
1177       return null;
1178     }
1179
1180     /*
1181      * compound the ratios by this rule:
1182      * A:B with M:N gives A*M:B*N
1183      * reduced by greatest common divisor
1184      * so 1:3 with 3:3 is 3:9 or 1:3
1185      * 1:3 with 3:1 is 3:3 or 1:1
1186      * 1:3 with 1:3 is 1:9
1187      * 2:5 with 3:7 is 6:35
1188      */
1189     int outFromRatio = getFromRatio() * map.getFromRatio();
1190     int outToRatio = getToRatio() * map.getToRatio();
1191     int gcd = MathUtils.gcd(outFromRatio, outToRatio);
1192     outFromRatio /= gcd;
1193     outToRatio /= gcd;
1194
1195     List<int[]> toRanges = new ArrayList<>();
1196     for (int[] range : getToRanges())
1197     {
1198       int[] transferred = map.locateInTo(range[0], range[1]);
1199       if (transferred == null || transferred.length % 2 != 0)
1200       {
1201         return null;
1202       }
1203
1204       /*
1205        *  convert [start1, end1, start2, end2, ...] 
1206        *  to [[start1, end1], [start2, end2], ...]
1207        */
1208       for (int i = 0; i < transferred.length;)
1209       {
1210         toRanges.add(new int[] { transferred[i], transferred[i + 1] });
1211         i += 2;
1212       }
1213     }
1214
1215     return new MapList(getFromRanges(), toRanges, outFromRatio, outToRatio);
1216   }
1217
1218   /**
1219    * Answers true if the mapping is from one contiguous range to another, else
1220    * false
1221    * 
1222    * @return
1223    */
1224   public boolean isContiguous()
1225   {
1226     return fromShifts.size() == 1 && toShifts.size() == 1;
1227   }
1228 }