Merge commit 'alpha/update_2_12_for_2_11_2_series_merge^2' into HEAD
[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.BitSet;
26 import java.util.List;
27
28 /**
29  * A simple way of bijectively mapping a non-contiguous linear range to another
30  * non-contiguous linear range.
31  * 
32  * Use at your own risk!
33  * 
34  * TODO: test/ensure that sense of from and to ratio start position is conserved
35  * (codon start position recovery)
36  */
37 public class MapList
38 {
39
40   /*
41    * Subregions (base 1) described as { [start1, end1], [start2, end2], ...}
42    */
43   private List<int[]> fromShifts;
44
45   /*
46    * Same format as fromShifts, for the 'mapped to' sequence
47    */
48   private List<int[]> toShifts;
49
50   /*
51    * number of steps in fromShifts to one toRatio unit
52    */
53   private int fromRatio;
54
55   /*
56    * number of steps in toShifts to one fromRatio
57    */
58   private int toRatio;
59
60   /*
61    * lowest and highest value in the from Map
62    */
63   private int fromLowest;
64
65   private int fromHighest;
66
67   /*
68    * lowest and highest value in the to Map
69    */
70   private int toLowest;
71
72   private int toHighest;
73
74   /**
75    * Constructor
76    */
77   public MapList()
78   {
79     fromShifts = new ArrayList<>();
80     toShifts = new ArrayList<>();
81   }
82
83   /**
84    * Two MapList objects are equal if they are the same object, or they both
85    * have populated shift ranges and all values are the same.
86    */
87   @Override
88   public boolean equals(Object o)
89   {
90     if (o == null || !(o instanceof MapList))
91     {
92       return false;
93     }
94
95     MapList obj = (MapList) o;
96     if (obj == this)
97     {
98       return true;
99     }
100     if (obj.fromRatio != fromRatio || obj.toRatio != toRatio
101             || obj.fromShifts == null || obj.toShifts == null)
102     {
103       return false;
104     }
105     return Arrays.deepEquals(fromShifts.toArray(), obj.fromShifts.toArray())
106             && Arrays.deepEquals(toShifts.toArray(),
107                     obj.toShifts.toArray());
108   }
109
110   /**
111    * Returns a hashcode made from the fromRatio, toRatio, and from/to ranges
112    */
113   @Override
114   public int hashCode()
115   {
116     int hashCode = 31 * fromRatio;
117     hashCode = 31 * hashCode + toRatio;
118     for (int[] shift : fromShifts)
119     {
120       hashCode = 31 * hashCode + shift[0];
121       hashCode = 31 * hashCode + shift[1];
122     }
123     for (int[] shift : toShifts)
124     {
125       hashCode = 31 * hashCode + shift[0];
126       hashCode = 31 * hashCode + shift[1];
127     }
128
129     return hashCode;
130   }
131
132   /**
133    * Returns the 'from' ranges as {[start1, end1], [start2, end2], ...}
134    * 
135    * @return
136    */
137   public List<int[]> getFromRanges()
138   {
139     return fromShifts;
140   }
141
142   /**
143    * Returns the 'to' ranges as {[start1, end1], [start2, end2], ...}
144    * 
145    * @return
146    */
147   public List<int[]> getToRanges()
148   {
149     return toShifts;
150   }
151
152   /**
153    * Flattens a list of [start, end] into a single [start1, end1, start2,
154    * end2,...] array.
155    * 
156    * @param shifts
157    * @return
158    */
159   protected static int[] getRanges(List<int[]> shifts)
160   {
161     int[] rnges = new int[2 * shifts.size()];
162     int i = 0;
163     for (int[] r : shifts)
164     {
165       rnges[i++] = r[0];
166       rnges[i++] = r[1];
167     }
168     return rnges;
169   }
170
171   /**
172    * 
173    * @return length of mapped phrase in from
174    */
175   public int getFromRatio()
176   {
177     return fromRatio;
178   }
179
180   /**
181    * 
182    * @return length of mapped phrase in to
183    */
184   public int getToRatio()
185   {
186     return toRatio;
187   }
188
189   public int getFromLowest()
190   {
191     return fromLowest;
192   }
193
194   public int getFromHighest()
195   {
196     return fromHighest;
197   }
198
199   public int getToLowest()
200   {
201     return toLowest;
202   }
203
204   public int getToHighest()
205   {
206     return toHighest;
207   }
208
209   /**
210    * Constructor given from and to ranges as [start1, end1, start2, end2,...].
211    * There is no validation check that the ranges do not overlap each other.
212    * 
213    * @param from
214    *          contiguous regions as [start1, end1, start2, end2, ...]
215    * @param to
216    *          same format as 'from'
217    * @param fromRatio
218    *          phrase length in 'from' (e.g. 3 for dna)
219    * @param toRatio
220    *          phrase length in 'to' (e.g. 1 for protein)
221    */
222   public MapList(int from[], int to[], int fromRatio, int toRatio)
223   {
224     this();
225     this.fromRatio = fromRatio;
226     this.toRatio = toRatio;
227     fromLowest = Integer.MAX_VALUE;
228     fromHighest = Integer.MIN_VALUE;
229
230     for (int i = 0; i < from.length; i += 2)
231     {
232       /*
233        * note lowest and highest values - bearing in mind the
234        * direction may be reversed
235        */
236       fromLowest = Math.min(fromLowest, Math.min(from[i], from[i + 1]));
237       fromHighest = Math.max(fromHighest, Math.max(from[i], from[i + 1]));
238       fromShifts.add(new int[] { from[i], from[i + 1] });
239     }
240
241     toLowest = Integer.MAX_VALUE;
242     toHighest = Integer.MIN_VALUE;
243     for (int i = 0; i < to.length; i += 2)
244     {
245       toLowest = Math.min(toLowest, Math.min(to[i], to[i + 1]));
246       toHighest = Math.max(toHighest, Math.max(to[i], to[i + 1]));
247       toShifts.add(new int[] { to[i], to[i + 1] });
248     }
249   }
250
251   /**
252    * Copy constructor. Creates an identical mapping.
253    * 
254    * @param map
255    */
256   public MapList(MapList map)
257   {
258     this();
259     // TODO not used - remove?
260     this.fromLowest = map.fromLowest;
261     this.fromHighest = map.fromHighest;
262     this.toLowest = map.toLowest;
263     this.toHighest = map.toHighest;
264
265     this.fromRatio = map.fromRatio;
266     this.toRatio = map.toRatio;
267     if (map.fromShifts != null)
268     {
269       for (int[] r : map.fromShifts)
270       {
271         fromShifts.add(new int[] { r[0], r[1] });
272       }
273     }
274     if (map.toShifts != null)
275     {
276       for (int[] r : map.toShifts)
277       {
278         toShifts.add(new int[] { r[0], r[1] });
279       }
280     }
281   }
282
283   /**
284    * Constructor given ranges as lists of [start, end] positions. There is no
285    * validation check that the ranges do not overlap each other.
286    * 
287    * @param fromRange
288    * @param toRange
289    * @param fromRatio
290    * @param toRatio
291    */
292   public MapList(List<int[]> fromRange, List<int[]> toRange, int fromRatio,
293           int toRatio)
294   {
295     this();
296     fromRange = coalesceRanges(fromRange);
297     toRange = coalesceRanges(toRange);
298     this.fromShifts = fromRange;
299     this.toShifts = toRange;
300     this.fromRatio = fromRatio;
301     this.toRatio = toRatio;
302
303     fromLowest = Integer.MAX_VALUE;
304     fromHighest = Integer.MIN_VALUE;
305     for (int[] range : fromRange)
306     {
307       if (range.length != 2)
308       {
309         // throw new IllegalArgumentException(range);
310         System.err.println("Invalid format for fromRange "
311                 + Arrays.toString(range) + " may cause errors");
312       }
313       fromLowest = Math.min(fromLowest, Math.min(range[0], range[1]));
314       fromHighest = Math.max(fromHighest, Math.max(range[0], range[1]));
315     }
316
317     toLowest = Integer.MAX_VALUE;
318     toHighest = Integer.MIN_VALUE;
319     for (int[] range : toRange)
320     {
321       if (range.length != 2)
322       {
323         // throw new IllegalArgumentException(range);
324         System.err.println("Invalid format for toRange "
325                 + Arrays.toString(range) + " may cause errors");
326       }
327       toLowest = Math.min(toLowest, Math.min(range[0], range[1]));
328       toHighest = Math.max(toHighest, Math.max(range[0], range[1]));
329     }
330   }
331
332   /**
333    * Consolidates a list of ranges so that any contiguous ranges are merged.
334    * This assumes the ranges are already in start order (does not sort them).
335    * <p>
336    * The main use case for this method is when mapping cDNA sequence to its
337    * protein product, based on CDS feature ranges which derive from spliced
338    * exons, but are contiguous on the cDNA sequence. For example
339    * 
340    * <pre>
341    *   CDS 1-20  // from exon1
342    *   CDS 21-35 // from exon2
343    *   CDS 36-71 // from exon3
344    * 'coalesce' to range 1-71
345    * </pre>
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
374       int direction = range[1] >= range[0] ? 1 : -1;
375
376       /*
377        * if next range is in the same direction as last and contiguous,
378        * just update the end position of the last range
379        */
380       boolean sameDirection = range[1] == range[0]
381               || direction == lastDirection;
382       boolean extending = range[0] == lastRange[1] + lastDirection;
383       if (sameDirection && extending)
384       {
385         lastRange[1] = range[1];
386         changed = true;
387       }
388       else
389       {
390         lastRange = new int[] { range[0], range[1] };
391         merged.add(lastRange);
392         // careful: merging [5, 5] after [7, 6] should keep negative direction
393         lastDirection = (range[1] == range[0]) ? lastDirection : direction;
394       }
395     }
396
397     return changed ? merged : ranges;
398   }
399
400   /**
401    * get all mapped positions from 'from' to 'to'
402    * 
403    * @return int[][] { int[] { fromStart, fromFinish, toStart, toFinish }, int
404    *         [fromFinish-fromStart+2] { toStart..toFinish mappings}}
405    */
406   protected int[][] makeFromMap()
407   {
408     // TODO only used for test - remove??
409     return posMap(fromShifts, fromRatio, toShifts, toRatio);
410   }
411
412   /**
413    * get all mapped positions from 'to' to 'from'
414    * 
415    * @return int[to position]=position mapped in from
416    */
417   protected int[][] makeToMap()
418   {
419     // TODO only used for test - remove??
420     return posMap(toShifts, toRatio, fromShifts, fromRatio);
421   }
422
423   /**
424    * construct an int map for intervals in intVals
425    * 
426    * @param shiftTo
427    * @return int[] { from, to pos in range }, int[range.to-range.from+1]
428    *         returning mapped position
429    */
430   private int[][] posMap(List<int[]> shiftTo, int sourceRatio,
431           List<int[]> shiftFrom, int targetRatio)
432   {
433     // TODO only used for test - remove??
434     int iv = 0, ivSize = shiftTo.size();
435     if (iv >= ivSize)
436     {
437       return null;
438     }
439     int[] intv = shiftTo.get(iv++);
440     int from = intv[0], to = intv[1];
441     if (from > to)
442     {
443       from = intv[1];
444       to = intv[0];
445     }
446     while (iv < ivSize)
447     {
448       intv = shiftTo.get(iv++);
449       if (intv[0] < from)
450       {
451         from = intv[0];
452       }
453       if (intv[1] < from)
454       {
455         from = intv[1];
456       }
457       if (intv[0] > to)
458       {
459         to = intv[0];
460       }
461       if (intv[1] > to)
462       {
463         to = intv[1];
464       }
465     }
466     int tF = 0, tT = 0;
467     int mp[][] = new int[to - from + 2][];
468     for (int i = 0; i < mp.length; i++)
469     {
470       int[] m = shift(i + from, shiftTo, sourceRatio, shiftFrom, targetRatio);
471       if (m != null)
472       {
473         if (i == 0)
474         {
475           tF = tT = m[0];
476         }
477         else
478         {
479           if (m[0] < tF)
480           {
481             tF = m[0];
482           }
483           if (m[0] > tT)
484           {
485             tT = m[0];
486           }
487         }
488       }
489       mp[i] = m;
490     }
491     int[][] map = new int[][] { new int[] { from, to, tF, tT },
492         new int[to - from + 2] };
493
494     map[0][2] = tF;
495     map[0][3] = tT;
496
497     for (int i = 0; i < mp.length; i++)
498     {
499       if (mp[i] != null)
500       {
501         map[1][i] = mp[i][0] - tF;
502       }
503       else
504       {
505         map[1][i] = -1; // indicates an out of range mapping
506       }
507     }
508     return map;
509   }
510
511   /**
512    * addShift
513    * 
514    * @param pos
515    *          start position for shift (in original reference frame)
516    * @param shift
517    *          length of shift
518    * 
519    *          public void addShift(int pos, int shift) { int sidx = 0; int[]
520    *          rshift=null; while (sidx<shifts.size() && (rshift=(int[])
521    *          shifts.elementAt(sidx))[0]<pos) sidx++; if (sidx==shifts.size())
522    *          shifts.insertElementAt(new int[] { pos, shift}, sidx); else
523    *          rshift[1]+=shift; }
524    */
525
526   /**
527    * shift from pos to To(pos)
528    * 
529    * @param pos
530    *          int
531    * @return int shifted position in To, frameshift in From, direction of mapped
532    *         symbol in To
533    */
534   public int[] shiftFrom(int pos)
535   {
536     return shift(pos, fromShifts, fromRatio, toShifts, toRatio);
537   }
538
539   /**
540    * inverse of shiftFrom - maps pos in To to a position in From
541    * 
542    * @param pos
543    *          (in To)
544    * @return shifted position in From, frameshift in To, direction of mapped
545    *         symbol in From
546    */
547   public int[] shiftTo(int pos)
548   {
549     return shift(pos, toShifts, toRatio, fromShifts, fromRatio);
550   }
551
552   /**
553    * 
554    * @param shiftTo
555    * @param fromRatio
556    * @param shiftFrom
557    * @param toRatio
558    * @return
559    */
560   protected static int[] shift(int pos, List<int[]> shiftTo, int fromRatio,
561           List<int[]> shiftFrom, int toRatio)
562   {
563     // TODO: javadoc; tests
564     int[] fromCount = countPositions(shiftTo, pos);
565     if (fromCount == null)
566     {
567       return null;
568     }
569     int fromRemainder = (fromCount[0] - 1) % fromRatio;
570     int toCount = 1 + (((fromCount[0] - 1) / fromRatio) * toRatio);
571     int[] toPos = traverseToPosition(shiftFrom, toCount);
572     if (toPos == null)
573     {
574       return null;
575     }
576     return new int[] { toPos[0], fromRemainder, toPos[1] };
577   }
578
579   /**
580    * Counts how many positions pos is along the series of intervals. Returns an
581    * array of two values:
582    * <ul>
583    * <li>the number of positions traversed (inclusive) to reach {@code pos}</li>
584    * <li>+1 if the last interval traversed is forward, -1 if in a negative
585    * direction</li>
586    * </ul>
587    * Returns null if {@code pos} does not lie in any of the given intervals.
588    * 
589    * @param intervals
590    *          a list of start-end intervals
591    * @param pos
592    *          a position that may lie in one (or more) of the intervals
593    * @return
594    */
595   protected static int[] countPositions(List<int[]> intervals, int pos)
596   {
597     int count = 0;
598     int iv = 0;
599     int ivSize = intervals.size();
600
601     while (iv < ivSize)
602     {
603       int[] intv = intervals.get(iv++);
604       if (intv[0] <= intv[1])
605       {
606         /*
607          * forwards interval
608          */
609         if (pos >= intv[0] && pos <= intv[1])
610         {
611           return new int[] { count + pos - intv[0] + 1, +1 };
612         }
613         else
614         {
615           count += intv[1] - intv[0] + 1;
616         }
617       }
618       else
619       {
620         /*
621          * reverse interval
622          */
623         if (pos >= intv[1] && pos <= intv[0])
624         {
625           return new int[] { count + intv[0] - pos + 1, -1 };
626         }
627         else
628         {
629           count += intv[0] - intv[1] + 1;
630         }
631       }
632     }
633     return null;
634   }
635
636   /**
637    * Reads through the given intervals until {@code count} positions have been
638    * traversed, and returns an array consisting of two values:
639    * <ul>
640    * <li>the value at the {@code count'th} position</li>
641    * <li>+1 if the last interval read is forwards, -1 if reverse direction</li>
642    * </ul>
643    * Returns null if the ranges include less than {@code count} positions, or if
644    * {@code count < 1}.
645    * 
646    * @param intervals
647    *          a list of [start, end] ranges
648    * @param count
649    *          the number of positions to traverse
650    * @return
651    */
652   protected static int[] traverseToPosition(List<int[]> intervals,
653           final int count)
654   {
655     int traversed = 0;
656     int ivSize = intervals.size();
657     int iv = 0;
658
659     if (count < 1)
660     {
661       return null;
662     }
663
664     while (iv < ivSize)
665     {
666       int[] intv = intervals.get(iv++);
667       int diff = intv[1] - intv[0];
668       if (diff >= 0)
669       {
670         if (count <= traversed + 1 + diff)
671         {
672           return new int[] { intv[0] + (count - traversed - 1), +1 };
673         }
674         else
675         {
676           traversed += 1 + diff;
677         }
678       }
679       else
680       {
681         if (count <= traversed + 1 - diff)
682         {
683           return new int[] { intv[0] - (count - traversed - 1), -1 };
684         }
685         else
686         {
687           traversed += 1 - diff;
688         }
689       }
690     }
691     return null;
692   }
693
694   /**
695    * like shift - except returns the intervals in the given vector of shifts
696    * which were spanned in traversing fromStart to fromEnd
697    * 
698    * @param shiftFrom
699    * @param fromStart
700    * @param fromEnd
701    * @param fromRatio2
702    * @return series of from,to intervals from from first position of starting
703    *         region to final position of ending region inclusive
704    */
705   protected static int[] getIntervals(List<int[]> shiftFrom,
706           int[] fromStart, int[] fromEnd, int fromRatio2)
707   {
708     if (fromStart == null || fromEnd == null)
709     {
710       return null;
711     }
712     int startpos, endpos;
713     startpos = fromStart[0]; // first position in fromStart
714     endpos = fromEnd[0]; // last position in fromEnd
715     int endindx = (fromRatio2 - 1); // additional positions to get to last
716     // position from endpos
717     int intv = 0, intvSize = shiftFrom.size();
718     int iv[], i = 0, fs = -1, fe_s = -1, fe = -1; // containing intervals
719     // search intervals to locate ones containing startpos and count endindx
720     // positions on from endpos
721     while (intv < intvSize && (fs == -1 || fe == -1))
722     {
723       iv = shiftFrom.get(intv++);
724       if (fe_s > -1)
725       {
726         endpos = iv[0]; // start counting from beginning of interval
727         endindx--; // inclusive of endpos
728       }
729       if (iv[0] <= iv[1])
730       {
731         if (fs == -1 && startpos >= iv[0] && startpos <= iv[1])
732         {
733           fs = i;
734         }
735         if (endpos >= iv[0] && endpos <= iv[1])
736         {
737           if (fe_s == -1)
738           {
739             fe_s = i;
740           }
741           if (fe_s != -1)
742           {
743             if (endpos + endindx <= iv[1])
744             {
745               fe = i;
746               endpos = endpos + endindx; // end of end token is within this
747               // interval
748             }
749             else
750             {
751               endindx -= iv[1] - endpos; // skip all this interval too
752             }
753           }
754         }
755       }
756       else
757       {
758         if (fs == -1 && startpos <= iv[0] && startpos >= iv[1])
759         {
760           fs = i;
761         }
762         if (endpos <= iv[0] && endpos >= iv[1])
763         {
764           if (fe_s == -1)
765           {
766             fe_s = i;
767           }
768           if (fe_s != -1)
769           {
770             if (endpos - endindx >= iv[1])
771             {
772               fe = i;
773               endpos = endpos - endindx; // end of end token is within this
774               // interval
775             }
776             else
777             {
778               endindx -= endpos - iv[1]; // skip all this interval too
779             }
780           }
781         }
782       }
783       i++;
784     }
785     if (fs == fe && fe == -1)
786     {
787       return null;
788     }
789     List<int[]> ranges = new ArrayList<>();
790     if (fs <= fe)
791     {
792       intv = fs;
793       i = fs;
794       // truncate initial interval
795       iv = shiftFrom.get(intv++);
796       iv = new int[] { iv[0], iv[1] };// clone
797       if (i == fs)
798       {
799         iv[0] = startpos;
800       }
801       while (i != fe)
802       {
803         ranges.add(iv); // add initial range
804         iv = shiftFrom.get(intv++); // get next interval
805         iv = new int[] { iv[0], iv[1] };// clone
806         i++;
807       }
808       if (i == fe)
809       {
810         iv[1] = endpos;
811       }
812       ranges.add(iv); // add only - or final range
813     }
814     else
815     {
816       // walk from end of interval.
817       i = shiftFrom.size() - 1;
818       while (i > fs)
819       {
820         i--;
821       }
822       iv = shiftFrom.get(i);
823       iv = new int[] { iv[1], iv[0] };// reverse and clone
824       // truncate initial interval
825       if (i == fs)
826       {
827         iv[0] = startpos;
828       }
829       while (--i != fe)
830       { // fix apparent logic bug when fe==-1
831         ranges.add(iv); // add (truncated) reversed interval
832         iv = shiftFrom.get(i);
833         iv = new int[] { iv[1], iv[0] }; // reverse and clone
834       }
835       if (i == fe)
836       {
837         // interval is already reversed
838         iv[1] = endpos;
839       }
840       ranges.add(iv); // add only - or final range
841     }
842     // create array of start end intervals.
843     int[] range = null;
844     if (ranges != null && ranges.size() > 0)
845     {
846       range = new int[ranges.size() * 2];
847       intv = 0;
848       intvSize = ranges.size();
849       i = 0;
850       while (intv < intvSize)
851       {
852         iv = ranges.get(intv);
853         range[i++] = iv[0];
854         range[i++] = iv[1];
855         ranges.set(intv++, null); // remove
856       }
857     }
858     return range;
859   }
860
861   /**
862    * get the 'initial' position of mpos in To
863    * 
864    * @param mpos
865    *          position in from
866    * @return position of first word in to reference frame
867    */
868   public int getToPosition(int mpos)
869   {
870     int[] mp = shiftTo(mpos);
871     if (mp != null)
872     {
873       return mp[0];
874     }
875     return mpos;
876   }
877
878   /**
879    * 
880    * @return a MapList whose From range is this maplist's To Range, and vice
881    *         versa
882    */
883   public MapList getInverse()
884   {
885     return new MapList(getToRanges(), getFromRanges(), getToRatio(),
886             getFromRatio());
887   }
888
889   /**
890    * String representation - for debugging, not guaranteed not to change
891    */
892   @Override
893   public String toString()
894   {
895     StringBuilder sb = new StringBuilder(64);
896     sb.append("[");
897     for (int[] shift : fromShifts)
898     {
899       sb.append(" ").append(Arrays.toString(shift));
900     }
901     sb.append(" ] ");
902     sb.append(fromRatio).append(":").append(toRatio);
903     sb.append(" to [");
904     for (int[] shift : toShifts)
905     {
906       sb.append(" ").append(Arrays.toString(shift));
907     }
908     sb.append(" ]");
909     return sb.toString();
910   }
911
912   /**
913    * Extend this map list by adding the given map's ranges. There is no
914    * validation check that the ranges do not overlap existing ranges (or each
915    * other), but contiguous ranges are merged.
916    * 
917    * @param map
918    */
919   public void addMapList(MapList map)
920   {
921     if (this.equals(map))
922     {
923       return;
924     }
925     this.fromLowest = Math.min(fromLowest, map.fromLowest);
926     this.toLowest = Math.min(toLowest, map.toLowest);
927     this.fromHighest = Math.max(fromHighest, map.fromHighest);
928     this.toHighest = Math.max(toHighest, map.toHighest);
929
930     for (int[] range : map.getFromRanges())
931     {
932       MappingUtils.addRange(range, fromShifts);
933     }
934     for (int[] range : map.getToRanges())
935     {
936       MappingUtils.addRange(range, toShifts);
937     }
938   }
939
940   /**
941    * Returns true if mapping is from forward strand, false if from reverse
942    * strand. Result is just based on the first 'from' range that is not a single
943    * position. Default is true unless proven to be false. Behaviour is not well
944    * defined if the mapping has a mixture of forward and reverse ranges.
945    * 
946    * @return
947    */
948   public boolean isFromForwardStrand()
949   {
950     return isForwardStrand(getFromRanges());
951   }
952
953   /**
954    * Returns true if mapping is to forward strand, false if to reverse strand.
955    * Result is just based on the first 'to' range that is not a single position.
956    * Default is true unless proven to be false. Behaviour is not well defined if
957    * the mapping has a mixture of forward and reverse ranges.
958    * 
959    * @return
960    */
961   public boolean isToForwardStrand()
962   {
963     return isForwardStrand(getToRanges());
964   }
965
966   /**
967    * A helper method that returns true unless at least one range has start >
968    * end. Behaviour is undefined for a mixture of forward and reverse ranges.
969    * 
970    * @param ranges
971    * @return
972    */
973   private boolean isForwardStrand(List<int[]> ranges)
974   {
975     boolean forwardStrand = true;
976     for (int[] range : ranges)
977     {
978       if (range[1] > range[0])
979       {
980         break; // forward strand confirmed
981       }
982       else if (range[1] < range[0])
983       {
984         forwardStrand = false;
985         break; // reverse strand confirmed
986       }
987     }
988     return forwardStrand;
989   }
990
991   /**
992    * 
993    * @return true if from, or to is a three to 1 mapping
994    */
995   public boolean isTripletMap()
996   {
997     return (toRatio == 3 && fromRatio == 1)
998             || (fromRatio == 3 && toRatio == 1);
999   }
1000
1001   /**
1002    * Returns a map which is the composite of this one and the input map. That
1003    * is, the output map has the fromRanges of this map, and its toRanges are the
1004    * toRanges of this map as transformed by the input map.
1005    * <p>
1006    * Returns null if the mappings cannot be traversed (not all toRanges of this
1007    * map correspond to fromRanges of the input), or if this.toRatio does not
1008    * match map.fromRatio.
1009    * 
1010    * <pre>
1011    * Example 1:
1012    *    this:   from [1-100] to [501-600]
1013    *    input:  from [10-40] to [60-90]
1014    *    output: from [10-40] to [560-590]
1015    * Example 2 ('reverse strand exons'):
1016    *    this:   from [1-100] to [2000-1951], [1000-951] // transcript to loci
1017    *    input:  from [1-50]  to [41-90] // CDS to transcript
1018    *    output: from [10-40] to [1960-1951], [1000-971] // CDS to gene loci
1019    * </pre>
1020    * 
1021    * @param map
1022    * @return
1023    */
1024   public MapList traverse(MapList map)
1025   {
1026     if (map == null)
1027     {
1028       return null;
1029     }
1030
1031     /*
1032      * compound the ratios by this rule:
1033      * A:B with M:N gives A*M:B*N
1034      * reduced by greatest common divisor
1035      * so 1:3 with 3:3 is 3:9 or 1:3
1036      * 1:3 with 3:1 is 3:3 or 1:1
1037      * 1:3 with 1:3 is 1:9
1038      * 2:5 with 3:7 is 6:35
1039      */
1040     int outFromRatio = getFromRatio() * map.getFromRatio();
1041     int outToRatio = getToRatio() * map.getToRatio();
1042     int gcd = MathUtils.gcd(outFromRatio, outToRatio);
1043     outFromRatio /= gcd;
1044     outToRatio /= gcd;
1045
1046     List<int[]> toRanges = new ArrayList<>();
1047     for (int[] range : getToRanges())
1048     {
1049       int fromLength = Math.abs(range[1] - range[0]) + 1;
1050       int[] transferred = map.locateInTo(range[0], range[1]);
1051       if (transferred == null || transferred.length % 2 != 0)
1052       {
1053         return null;
1054       }
1055
1056       /*
1057        *  convert [start1, end1, start2, end2, ...] 
1058        *  to [[start1, end1], [start2, end2], ...]
1059        */
1060       int toLength = 0;
1061       for (int i = 0; i < transferred.length;)
1062       {
1063         toRanges.add(new int[] { transferred[i], transferred[i + 1] });
1064         toLength += Math.abs(transferred[i + 1] - transferred[i]) + 1;
1065         i += 2;
1066       }
1067
1068       /*
1069        * check we mapped the full range - if not, abort
1070        */
1071       if (fromLength * map.getToRatio() != toLength * map.getFromRatio())
1072       {
1073         return null;
1074       }
1075     }
1076
1077     return new MapList(getFromRanges(), toRanges, outFromRatio, outToRatio);
1078   }
1079
1080   /**
1081    * Answers true if the mapping is from one contiguous range to another, else
1082    * false
1083    * 
1084    * @return
1085    */
1086   public boolean isContiguous()
1087   {
1088     return fromShifts.size() == 1 && toShifts.size() == 1;
1089   }
1090
1091   /**
1092    * Returns the [start1, end1, start2, end2, ...] positions in the 'from' range
1093    * that map to positions between {@code start} and {@code end} in the 'to'
1094    * range. Note that for a reverse strand mapping this will return ranges with
1095    * end < start. Returns null if no mapped positions are found in start-end.
1096    * 
1097    * @param start
1098    * @param end
1099    * @return
1100    */
1101   public int[] locateInFrom(int start, int end)
1102   {
1103     return mapPositions(start, end, toShifts, fromShifts,
1104             toRatio, fromRatio);
1105   }
1106
1107   /**
1108    * Returns the [start1, end1, start2, end2, ...] positions in the 'to' range
1109    * that map to positions between {@code start} and {@code end} in the 'from'
1110    * range. Note that for a reverse strand mapping this will return ranges with
1111    * end < start. Returns null if no mapped positions are found in start-end.
1112    * 
1113    * @param start
1114    * @param end
1115    * @return
1116    */
1117   public int[] locateInTo(int start, int end)
1118   {
1119     return mapPositions(start, end, fromShifts, toShifts,
1120             fromRatio, toRatio);
1121   }
1122
1123   /**
1124    * Helper method that returns the [start1, end1, start2, end2, ...] positions
1125    * in {@code targetRange} that map to positions between {@code start} and
1126    * {@code end} in {@code sourceRange}. Note that for a reverse strand mapping
1127    * this will return ranges with end < start. Returns null if no mapped
1128    * positions are found in start-end.
1129    * 
1130    * @param start
1131    * @param end
1132    * @param sourceRange
1133    * @param targetRange
1134    * @param sourceWordLength
1135    * @param targetWordLength
1136    * @return
1137    */
1138   final static int[] mapPositions(int start, int end,
1139           List<int[]> sourceRange, List<int[]> targetRange,
1140           int sourceWordLength, int targetWordLength)
1141   {
1142     if (end < start)
1143     {
1144       int tmp = end;
1145       end = start;
1146       start = tmp;
1147     }
1148
1149     /*
1150      * traverse sourceRange and mark offsets in targetRange 
1151      * of any positions that lie in [start, end]
1152      */
1153     BitSet offsets = getMappedOffsetsForPositions(start, end, sourceRange,
1154             sourceWordLength, targetWordLength);
1155
1156     /*
1157      * traverse targetRange and collect positions at the marked offsets
1158      */
1159     List<int[]> mapped = getPositionsForOffsets(targetRange, offsets);
1160
1161     // TODO: or just return the List and adjust calling code to match
1162     return mapped.isEmpty() ? null : MappingUtils.rangeListToArray(mapped);
1163   }
1164
1165   /**
1166    * Scans the list of {@code ranges} for any values (positions) that lie
1167    * between start and end (inclusive), and records the <em>offsets</em> from
1168    * the start of the list as a BitSet. The offset positions are converted to
1169    * corresponding words in blocks of {@code wordLength2}.
1170    * 
1171    * <pre>
1172    * For example:
1173    * 1:1 (e.g. gene to CDS):
1174    * ranges { [10-20], [31-40] }, wordLengthFrom = wordLength 2 = 1
1175    *   for start = 1, end = 9, returns a BitSet with no bits set
1176    *   for start = 1, end = 11, returns a BitSet with bits 0-1 set
1177    *   for start = 15, end = 35, returns a BitSet with bits 5-15 set
1178    * 1:3 (peptide to codon):
1179    * ranges { [1-200] }, wordLengthFrom = 1, wordLength 2 = 3
1180    *   for start = 9, end = 9, returns a BitSet with bits 24-26 set
1181    * 3:1 (codon to peptide):
1182    * ranges { [101-150], [171-180] }, wordLengthFrom = 3, wordLength 2 = 1
1183    *   for start = 101, end = 102 (partial first codon), returns a BitSet with bit 0 set
1184    *   for start = 150, end = 171 (partial 17th codon), returns a BitSet with bit 16 set
1185    * 3:1 (circular DNA to peptide):
1186    * ranges { [101-150], [21-30] }, wordLengthFrom = 3, wordLength 2 = 1
1187    *   for start = 24, end = 40 (spans codons 18-20), returns a BitSet with bits 17-19 set
1188    * </pre>
1189    * 
1190    * @param start
1191    * @param end
1192    * @param sourceRange
1193    * @param sourceWordLength
1194    * @param targetWordLength
1195    * @return
1196    */
1197   protected final static BitSet getMappedOffsetsForPositions(int start,
1198           int end, List<int[]> sourceRange, int sourceWordLength, int targetWordLength)
1199   {
1200     BitSet overlaps = new BitSet();
1201     int offset = 0;
1202     final int s1 = sourceRange.size();
1203     for (int i = 0; i < s1; i++)
1204     {
1205       int[] range = sourceRange.get(i);
1206       final int offset1 = offset;
1207       int overlapStartOffset = -1;
1208       int overlapEndOffset = -1;
1209
1210       if (range[1] >= range[0])
1211       {
1212         /*
1213          * forward direction range
1214          */
1215         if (start <= range[1] && end >= range[0])
1216         {
1217           /*
1218            * overlap
1219            */
1220           int overlapStart = Math.max(start, range[0]);
1221           overlapStartOffset = offset1 + overlapStart - range[0];
1222           int overlapEnd = Math.min(end, range[1]);
1223           overlapEndOffset = offset1 + overlapEnd - range[0];
1224         }
1225       }
1226       else
1227       {
1228         /*
1229          * reverse direction range
1230          */
1231         if (start <= range[0] && end >= range[1])
1232         {
1233           /*
1234            * overlap
1235            */
1236           int overlapStart = Math.max(start, range[1]);
1237           int overlapEnd = Math.min(end, range[0]);
1238           overlapStartOffset = offset1 + range[0] - overlapEnd;
1239           overlapEndOffset = offset1 + range[0] - overlapStart;
1240         }
1241       }
1242
1243       if (overlapStartOffset > -1)
1244       {
1245         /*
1246          * found an overlap
1247          */
1248         if (sourceWordLength != targetWordLength)
1249         {
1250           /*
1251            * convert any overlap found to whole words in the target range
1252            * (e.g. treat any partial codon overlap as if the whole codon)
1253            */
1254           overlapStartOffset -= overlapStartOffset % sourceWordLength;
1255           overlapStartOffset = overlapStartOffset / sourceWordLength
1256                   * targetWordLength;
1257
1258           /*
1259            * similar calculation for range end, adding 
1260            * (wordLength2 - 1) for end of mapped word
1261            */
1262           overlapEndOffset -= overlapEndOffset % sourceWordLength;
1263           overlapEndOffset = overlapEndOffset / sourceWordLength
1264                   * targetWordLength;
1265           overlapEndOffset += targetWordLength - 1;
1266         }
1267         overlaps.set(overlapStartOffset, overlapEndOffset + 1);
1268       }
1269       offset += 1 + Math.abs(range[1] - range[0]);
1270     }
1271     return overlaps;
1272   }
1273
1274   /**
1275    * Returns a (possibly empty) list of the [start-end] values (positions) at
1276    * offsets in the {@code targetRange} list that are marked by 'on' bits in the
1277    * {@code offsets} bitset.
1278    * 
1279    * @param targetRange
1280    * @param offsets
1281    * @return
1282    */
1283   protected final static List<int[]> getPositionsForOffsets(
1284           List<int[]> targetRange, BitSet offsets)
1285   {
1286     List<int[]> mapped = new ArrayList<>();
1287     if (offsets.isEmpty())
1288     {
1289       return mapped;
1290     }
1291
1292     /*
1293      * count of positions preceding ranges[i]
1294      */
1295     int traversed = 0;
1296
1297     /*
1298      * for each [from-to] range in ranges:
1299      * - find subranges (if any) at marked offsets
1300      * - add the start-end values at the marked positions
1301      */
1302     final int toAdd = offsets.cardinality();
1303     int added = 0;
1304     final int s2 = targetRange.size();
1305     for (int i = 0; added < toAdd && i < s2; i++)
1306     {
1307       int[] range = targetRange.get(i);
1308       added += addOffsetPositions(mapped, traversed, range, offsets);
1309       traversed += Math.abs(range[1] - range[0]) + 1;
1310     }
1311     return mapped;
1312   }
1313
1314   /**
1315    * Helper method that adds any start-end subranges of {@code range} that are
1316    * at offsets in {@code range} marked by set bits in overlaps.
1317    * {@code mapOffset} is added to {@code range} offset positions. Returns the
1318    * count of positions added.
1319    * 
1320    * @param mapped
1321    * @param mapOffset
1322    * @param range
1323    * @param overlaps
1324    * @return
1325    */
1326   final static int addOffsetPositions(List<int[]> mapped,
1327           final int mapOffset, final int[] range, final BitSet overlaps)
1328   {
1329     final int rangeLength = 1 + Math.abs(range[1] - range[0]);
1330     final int step = range[1] < range[0] ? -1 : 1;
1331     int offsetStart = 0; // offset into range
1332     int added = 0;
1333
1334     while (offsetStart < rangeLength)
1335     {
1336       /*
1337        * find the start of the next marked overlap offset;
1338        * if there is none, or it is beyond range, then finished
1339        */
1340       int overlapStart = overlaps.nextSetBit(mapOffset + offsetStart);
1341       if (overlapStart == -1 || overlapStart - mapOffset >= rangeLength)
1342       {
1343         /*
1344          * no more overlaps, or no more within range[]
1345          */
1346         return added;
1347       }
1348       overlapStart -= mapOffset;
1349
1350       /*
1351        * end of the overlap range is just before the next clear bit;
1352        * restrict it to end of range if necessary;
1353        * note we may add a reverse strand range here (end < start)
1354        */
1355       int overlapEnd = overlaps.nextClearBit(mapOffset + overlapStart + 1);
1356       overlapEnd = (overlapEnd == -1) ? rangeLength - 1
1357               : Math.min(rangeLength - 1, overlapEnd - mapOffset - 1);
1358       int startPosition = range[0] + step * overlapStart;
1359       int endPosition = range[0] + step * overlapEnd;
1360       mapped.add(new int[] { startPosition, endPosition });
1361       offsetStart = overlapEnd + 1;
1362       added += Math.abs(endPosition - startPosition) + 1;
1363     }
1364
1365     return added;
1366   }
1367 }