JAL-3761 locateInFrom/To revised with tests; unused methods removed
[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 ratio,
431           List<int[]> shiftFrom, int toRatio)
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, ratio, shiftFrom, toRatio);
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       addRange(range, fromShifts);
933     }
934     for (int[] range : map.getToRanges())
935     {
936       addRange(range, toShifts);
937     }
938   }
939
940   /**
941    * Adds the given range to a list of ranges. If the new range just extends
942    * existing ranges, the current endpoint is updated instead.
943    * 
944    * @param range
945    * @param addTo
946    */
947   static void addRange(int[] range, List<int[]> addTo)
948   {
949     /*
950      * list is empty - add to it!
951      */
952     if (addTo.size() == 0)
953     {
954       addTo.add(range);
955       return;
956     }
957
958     int[] last = addTo.get(addTo.size() - 1);
959     boolean lastForward = last[1] >= last[0];
960     boolean newForward = range[1] >= range[0];
961
962     /*
963      * contiguous range in the same direction - just update endpoint
964      */
965     if (lastForward == newForward && last[1] == range[0])
966     {
967       last[1] = range[1];
968       return;
969     }
970
971     /*
972      * next range starts at +1 in forward sense - update endpoint
973      */
974     if (lastForward && newForward && range[0] == last[1] + 1)
975     {
976       last[1] = range[1];
977       return;
978     }
979
980     /*
981      * next range starts at -1 in reverse sense - update endpoint
982      */
983     if (!lastForward && !newForward && range[0] == last[1] - 1)
984     {
985       last[1] = range[1];
986       return;
987     }
988
989     /*
990      * just add the new range
991      */
992     addTo.add(range);
993   }
994
995   /**
996    * Returns true if mapping is from forward strand, false if from reverse
997    * strand. Result is just based on the first 'from' range that is not a single
998    * position. Default is true unless proven to be false. Behaviour is not well
999    * defined if the mapping has a mixture of forward and reverse ranges.
1000    * 
1001    * @return
1002    */
1003   public boolean isFromForwardStrand()
1004   {
1005     return isForwardStrand(getFromRanges());
1006   }
1007
1008   /**
1009    * Returns true if mapping is to forward strand, false if to reverse strand.
1010    * Result is just based on the first 'to' range that is not a single position.
1011    * Default is true unless proven to be false. Behaviour is not well defined if
1012    * the mapping has a mixture of forward and reverse ranges.
1013    * 
1014    * @return
1015    */
1016   public boolean isToForwardStrand()
1017   {
1018     return isForwardStrand(getToRanges());
1019   }
1020
1021   /**
1022    * A helper method that returns true unless at least one range has start >
1023    * end. Behaviour is undefined for a mixture of forward and reverse ranges.
1024    * 
1025    * @param ranges
1026    * @return
1027    */
1028   private boolean isForwardStrand(List<int[]> ranges)
1029   {
1030     boolean forwardStrand = true;
1031     for (int[] range : ranges)
1032     {
1033       if (range[1] > range[0])
1034       {
1035         break; // forward strand confirmed
1036       }
1037       else if (range[1] < range[0])
1038       {
1039         forwardStrand = false;
1040         break; // reverse strand confirmed
1041       }
1042     }
1043     return forwardStrand;
1044   }
1045
1046   /**
1047    * 
1048    * @return true if from, or to is a three to 1 mapping
1049    */
1050   public boolean isTripletMap()
1051   {
1052     return (toRatio == 3 && fromRatio == 1)
1053             || (fromRatio == 3 && toRatio == 1);
1054   }
1055
1056   /**
1057    * Returns a map which is the composite of this one and the input map. That
1058    * is, the output map has the fromRanges of this map, and its toRanges are the
1059    * toRanges of this map as transformed by the input map.
1060    * <p>
1061    * Returns null if the mappings cannot be traversed (not all toRanges of this
1062    * map correspond to fromRanges of the input), or if this.toRatio does not
1063    * match map.fromRatio.
1064    * 
1065    * <pre>
1066    * Example 1:
1067    *    this:   from [1-100] to [501-600]
1068    *    input:  from [10-40] to [60-90]
1069    *    output: from [10-40] to [560-590]
1070    * Example 2 ('reverse strand exons'):
1071    *    this:   from [1-100] to [2000-1951], [1000-951] // transcript to loci
1072    *    input:  from [1-50]  to [41-90] // CDS to transcript
1073    *    output: from [10-40] to [1960-1951], [1000-971] // CDS to gene loci
1074    * </pre>
1075    * 
1076    * @param map
1077    * @return
1078    */
1079   public MapList traverse(MapList map)
1080   {
1081     if (map == null)
1082     {
1083       return null;
1084     }
1085
1086     /*
1087      * compound the ratios by this rule:
1088      * A:B with M:N gives A*M:B*N
1089      * reduced by greatest common divisor
1090      * so 1:3 with 3:3 is 3:9 or 1:3
1091      * 1:3 with 3:1 is 3:3 or 1:1
1092      * 1:3 with 1:3 is 1:9
1093      * 2:5 with 3:7 is 6:35
1094      */
1095     int outFromRatio = getFromRatio() * map.getFromRatio();
1096     int outToRatio = getToRatio() * map.getToRatio();
1097     int gcd = MathUtils.gcd(outFromRatio, outToRatio);
1098     outFromRatio /= gcd;
1099     outToRatio /= gcd;
1100
1101     List<int[]> toRanges = new ArrayList<>();
1102     for (int[] range : getToRanges())
1103     {
1104       int fromLength = Math.abs(range[1] - range[0]) + 1;
1105       int[] transferred = map.locateInTo(range[0], range[1]);
1106       if (transferred == null || transferred.length % 2 != 0)
1107       {
1108         return null;
1109       }
1110
1111       /*
1112        *  convert [start1, end1, start2, end2, ...] 
1113        *  to [[start1, end1], [start2, end2], ...]
1114        */
1115       int toLength = 0;
1116       for (int i = 0; i < transferred.length;)
1117       {
1118         toRanges.add(new int[] { transferred[i], transferred[i + 1] });
1119         toLength += Math.abs(transferred[i + 1] - transferred[i]) + 1;
1120         i += 2;
1121       }
1122
1123       /*
1124        * check we mapped the full range - if not, abort
1125        */
1126       if (fromLength * map.getToRatio() != toLength * map.getFromRatio())
1127       {
1128         return null;
1129       }
1130     }
1131
1132     return new MapList(getFromRanges(), toRanges, outFromRatio, outToRatio);
1133   }
1134
1135   /**
1136    * Answers true if the mapping is from one contiguous range to another, else
1137    * false
1138    * 
1139    * @return
1140    */
1141   public boolean isContiguous()
1142   {
1143     return fromShifts.size() == 1 && toShifts.size() == 1;
1144   }
1145
1146   /**
1147    * Returns the [start1, end1, start2, end2, ...] positions in the 'from' range
1148    * that map to positions between {@code start} and {@code end} in the 'to'
1149    * range. Note that for a reverse strand mapping this will return ranges with
1150    * end < start. Returns null if no mapped positions are found in start-end.
1151    * 
1152    * @param start
1153    * @param end
1154    * @return
1155    */
1156   public int[] locateInFrom(int start, int end)
1157   {
1158     if (end < start)
1159     {
1160       int tmp = end;
1161       end = start;
1162       start = tmp;
1163     }
1164
1165     /*
1166      * traverse toShifts and mark offsets in fromShifts 
1167      * of any positions that lie in [start, end]
1168      */
1169     BitSet offsets = getMappedOffsetsForPositions(start, end, toShifts,
1170             toRatio, fromRatio);
1171
1172     /*
1173      * traverse fromShifts and collect positions at the marked offsets
1174      */
1175     List<int[]> mapped = getPositionsForOffsets(fromShifts, offsets);
1176
1177     // TODO: or just return the List and adjust calling code to match
1178     return mapped.isEmpty() ? null : MappingUtils.rangeListToArray(mapped);
1179   }
1180
1181   /**
1182    * Returns the [start1, end1, start2, end2, ...] positions in the 'to' range
1183    * that map to positions between {@code start} and {@code end} in the 'from'
1184    * range. Note that for a reverse strand mapping this will return ranges with
1185    * end < start. Returns null if no mapped positions are found in start-end.
1186    * 
1187    * @param start
1188    * @param end
1189    * @return
1190    */
1191   public int[] locateInTo(int start, int end)
1192   {
1193     if (end < start)
1194     {
1195       int tmp = end;
1196       end = start;
1197       start = tmp;
1198     }
1199
1200     /*
1201      * traverse fromShifts and mark offsets in toShifts 
1202      * of any positions that lie in [start, end]
1203      */
1204     BitSet offsets = getMappedOffsetsForPositions(start, end, fromShifts,
1205             fromRatio, toRatio);
1206
1207     /*
1208      * traverse toShifts and collect positions at the marked offsets
1209      */
1210     List<int[]> mapped = getPositionsForOffsets(toShifts, offsets);
1211
1212     return mapped.isEmpty() ? null : MappingUtils.rangeListToArray(mapped);
1213   }
1214
1215   /**
1216    * Scans the list of {@code ranges} for any values (positions) that lie
1217    * between start and end (inclusive), and records the <em>offsets</em> from
1218    * the start of the list as a BitSet. The offset positions are converted to
1219    * corresponding words in blocks of {@code wordLength2}.
1220    * 
1221    * <pre>
1222    * For example:
1223    * 1:1 (e.g. gene to CDS):
1224    * ranges { [10-20], [31-40] }, wordLengthFrom = wordLength 2 = 1
1225    *   for start = 1, end = 9, returns a BitSet with no bits set
1226    *   for start = 1, end = 11, returns a BitSet with bits 0-1 set
1227    *   for start = 15, end = 35, returns a BitSet with bits 5-15 set
1228    * 1:3 (peptide to codon):
1229    * ranges { [1-200] }, wordLengthFrom = 1, wordLength 2 = 3
1230    *   for start = 9, end = 9, returns a BitSet with bits 24-26 set
1231    * 3:1 (codon to peptide):
1232    * ranges { [101-150], [171-180] }, wordLengthFrom = 3, wordLength 2 = 1
1233    *   for start = 101, end = 102 (partial first codon), returns a BitSet with bit 0 set
1234    *   for start = 150, end = 171 (partial 17th codon), returns a BitSet with bit 16 set
1235    * 3:1 (circular DNA to peptide):
1236    * ranges { [101-150], [21-30] }, wordLengthFrom = 3, wordLength 2 = 1
1237    *   for start = 24, end = 40 (spans codons 18-20), returns a BitSet with bits 17-19 set
1238    * </pre>
1239    * 
1240    * @param start
1241    * @param end
1242    * @param ranges
1243    * @param wordLengthFrom
1244    * @param wordLengthTo
1245    * @return
1246    */
1247   protected final static BitSet getMappedOffsetsForPositions(int start,
1248           int end, List<int[]> ranges, int wordLengthFrom, int wordLengthTo)
1249   {
1250     BitSet overlaps = new BitSet();
1251     int offset = 0;
1252     final int s1 = ranges.size();
1253     for (int i = 0; i < s1; i++)
1254     {
1255       int[] range = ranges.get(i);
1256       final int offset1 = offset;
1257       int overlapStartOffset = -1;
1258       int overlapEndOffset = -1;
1259
1260       if (range[1] >= range[0])
1261       {
1262         /*
1263          * forward direction range
1264          */
1265         if (start <= range[1] && end >= range[0])
1266         {
1267           /*
1268            * overlap
1269            */
1270           int overlapStart = Math.max(start, range[0]);
1271           overlapStartOffset = offset1 + overlapStart - range[0];
1272           int overlapEnd = Math.min(end, range[1]);
1273           overlapEndOffset = offset1 + overlapEnd - range[0];
1274         }
1275       }
1276       else
1277       {
1278         /*
1279          * reverse direction range
1280          */
1281         if (start <= range[0] && end >= range[1])
1282         {
1283           /*
1284            * overlap
1285            */
1286           int overlapStart = Math.max(start, range[1]);
1287           int overlapEnd = Math.min(end, range[0]);
1288           overlapStartOffset = offset1 + range[0] - overlapEnd;
1289           overlapEndOffset = offset1 + range[0] - overlapStart;
1290         }
1291       }
1292
1293       if (overlapStartOffset > -1)
1294       {
1295         /*
1296          * found an overlap
1297          */
1298         if (wordLengthFrom != wordLengthTo)
1299         {
1300           /*
1301            * convert any overlap found to whole words in the target range
1302            * (e.g. treat any partial codon overlap as if the whole codon)
1303            */
1304           overlapStartOffset -= overlapStartOffset % wordLengthFrom;
1305           overlapStartOffset = overlapStartOffset / wordLengthFrom
1306                   * wordLengthTo;
1307
1308           /*
1309            * similar calculation for range end, adding 
1310            * (wordLength2 - 1) for end of mapped word
1311            */
1312           overlapEndOffset -= overlapEndOffset % wordLengthFrom;
1313           overlapEndOffset = overlapEndOffset / wordLengthFrom
1314                   * wordLengthTo;
1315           overlapEndOffset += wordLengthTo - 1;
1316         }
1317         overlaps.set(overlapStartOffset, overlapEndOffset + 1);
1318       }
1319       offset += 1 + Math.abs(range[1] - range[0]);
1320     }
1321     return overlaps;
1322   }
1323
1324   /**
1325    * Returns a (possibly empty) list of the [start-end] values (positions) at
1326    * offsets in the {@code ranges} list that are marked by 'on' bits in the
1327    * {@code offsets} bitset.
1328    * 
1329    * @param ranges
1330    * @param offsets
1331    * @return
1332    */
1333   protected final static List<int[]> getPositionsForOffsets(
1334           List<int[]> ranges, BitSet offsets)
1335   {
1336     List<int[]> mapped = new ArrayList<>();
1337     if (offsets.isEmpty())
1338     {
1339       return mapped;
1340     }
1341
1342     /*
1343      * count of positions preceding ranges[i]
1344      */
1345     int traversed = 0;
1346
1347     /*
1348      * for each [from-to] range in ranges:
1349      * - find subranges (if any) at marked offsets
1350      * - add the start-end values at the marked positions
1351      */
1352     final int toAdd = offsets.cardinality();
1353     int added = 0;
1354     final int s2 = ranges.size();
1355     for (int i = 0; added < toAdd && i < s2; i++)
1356     {
1357       int[] range = ranges.get(i);
1358       added += addOffsetPositions(mapped, traversed, range, offsets);
1359       traversed += Math.abs(range[1] - range[0]) + 1;
1360     }
1361     return mapped;
1362   }
1363
1364   /**
1365    * Helper method that adds any start-end subranges of {@code range} that are
1366    * at offsets in {@code range} marked by set bits in overlaps.
1367    * {@code mapOffset} is added to {@code range} offset positions. Returns the
1368    * count of positions added.
1369    * 
1370    * @param mapped
1371    * @param mapOffset
1372    * @param range
1373    * @param overlaps
1374    * @return
1375    */
1376   final static int addOffsetPositions(List<int[]> mapped,
1377           final int mapOffset, final int[] range, final BitSet overlaps)
1378   {
1379     final int rangeLength = 1 + Math.abs(range[1] - range[0]);
1380     final int step = range[1] < range[0] ? -1 : 1;
1381     int offsetStart = 0; // offset into range
1382     int added = 0;
1383
1384     while (offsetStart < rangeLength)
1385     {
1386       /*
1387        * find the start of the next marked overlap offset;
1388        * if there is none, or it is beyond range, then finished
1389        */
1390       int overlapStart = overlaps.nextSetBit(mapOffset + offsetStart);
1391       if (overlapStart == -1 || overlapStart - mapOffset >= rangeLength)
1392       {
1393         /*
1394          * no more overlaps, or no more within range[]
1395          */
1396         return added;
1397       }
1398       overlapStart -= mapOffset;
1399
1400       /*
1401        * end of the overlap range is just before the next clear bit;
1402        * restrict it to end of range if necessary;
1403        * note we may add a reverse strand range here (end < start)
1404        */
1405       int overlapEnd = overlaps.nextClearBit(mapOffset + overlapStart + 1);
1406       overlapEnd = (overlapEnd == -1) ? rangeLength - 1
1407               : Math.min(rangeLength - 1, overlapEnd - mapOffset - 1);
1408       int startPosition = range[0] + step * overlapStart;
1409       int endPosition = range[0] + step * overlapEnd;
1410       mapped.add(new int[] { startPosition, endPosition });
1411       offsetStart = overlapEnd + 1;
1412       added += Math.abs(endPosition - startPosition) + 1;
1413     }
1414
1415     return added;
1416   }
1417 }