JAL-653 MapList constructor merges any contiguous ranges
[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<int[]>();
81     toShifts = new ArrayList<int[]>();
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     // TODO should also override hashCode to ensure equal objects have equal
92     // hashcodes
93     if (o == null || !(o instanceof MapList))
94     {
95       return false;
96     }
97
98     MapList obj = (MapList) o;
99     if (obj == this)
100     {
101       return true;
102     }
103     if (obj.fromRatio != fromRatio || obj.toRatio != toRatio
104             || obj.fromShifts == null || obj.toShifts == null)
105     {
106       return false;
107     }
108     return Arrays
109             .deepEquals(fromShifts.toArray(), obj.fromShifts.toArray())
110             && Arrays
111                     .deepEquals(toShifts.toArray(), obj.toShifts.toArray());
112   }
113
114   /**
115    * Returns the 'from' ranges as {[start1, end1], [start2, end2], ...}
116    * 
117    * @return
118    */
119   public List<int[]> getFromRanges()
120   {
121     return fromShifts;
122   }
123
124   /**
125    * Returns the 'to' ranges as {[start1, end1], [start2, end2], ...}
126    * 
127    * @return
128    */
129   public List<int[]> getToRanges()
130   {
131     return toShifts;
132   }
133
134   /**
135    * Flattens a list of [start, end] into a single [start1, end1, start2,
136    * end2,...] array.
137    * 
138    * @param shifts
139    * @return
140    */
141   protected static int[] getRanges(List<int[]> shifts)
142   {
143     int[] rnges = new int[2 * shifts.size()];
144     int i = 0;
145     for (int[] r : shifts)
146     {
147       rnges[i++] = r[0];
148       rnges[i++] = r[1];
149     }
150     return rnges;
151   }
152
153   /**
154    * 
155    * @return length of mapped phrase in from
156    */
157   public int getFromRatio()
158   {
159     return fromRatio;
160   }
161
162   /**
163    * 
164    * @return length of mapped phrase in to
165    */
166   public int getToRatio()
167   {
168     return toRatio;
169   }
170
171   public int getFromLowest()
172   {
173     return fromLowest;
174   }
175
176   public int getFromHighest()
177   {
178     return fromHighest;
179   }
180
181   public int getToLowest()
182   {
183     return toLowest;
184   }
185
186   public int getToHighest()
187   {
188     return toHighest;
189   }
190
191   /**
192    * Constructor given from and to ranges as [start1, end1, start2, end2,...].
193    * If any end is equal to the next start, the ranges will be merged.
194    * 
195    * @param from
196    *          contiguous regions as [start1, end1, start2, end2, ...]
197    * @param to
198    *          same format as 'from'
199    * @param fromRatio
200    *          phrase length in 'from' (e.g. 3 for dna)
201    * @param toRatio
202    *          phrase length in 'to' (e.g. 1 for protein)
203    */
204   public MapList(int from[], int to[], int fromRatio, int toRatio)
205   {
206     this();
207     this.fromRatio = fromRatio;
208     this.toRatio = toRatio;
209     fromLowest = from[0];
210     fromHighest = from[1];
211     int added = 0;
212
213     for (int i = 0; i < from.length; i += 2)
214     {
215       fromLowest = Math.min(fromLowest, from[i]);
216       fromHighest = Math.max(fromHighest, from[i + 1]);
217       if (added > 0 && from[i] == fromShifts.get(added - 1)[1])
218       {
219         /*
220          * this range starts where the last ended - just extend it
221          */
222         fromShifts.get(added - 1)[1] = from[i + 1];
223       }
224       else
225       {
226         fromShifts.add(new int[] { from[i], from[i + 1] });
227         added++;
228       }
229     }
230
231     toLowest = to[0];
232     toHighest = to[1];
233     added = 0;
234     for (int i = 0; i < to.length; i += 2)
235     {
236       toLowest = Math.min(toLowest, to[i]);
237       toHighest = Math.max(toHighest, to[i + 1]);
238       if (added > 0 && to[i] == toShifts.get(added - 1)[1])
239       {
240         toShifts.get(added - 1)[1] = to[i + 1];
241       }
242       else
243       {
244         toShifts.add(new int[] { to[i], to[i + 1] });
245         added++;
246       }
247     }
248   }
249
250   /**
251    * Copy constructor. Creates an identical mapping.
252    * 
253    * @param map
254    */
255   public MapList(MapList map)
256   {
257     this();
258     // TODO not used - remove?
259     this.fromLowest = map.fromLowest;
260     this.fromHighest = map.fromHighest;
261     this.toLowest = map.toLowest;
262     this.toHighest = map.toHighest;
263
264     this.fromRatio = map.fromRatio;
265     this.toRatio = map.toRatio;
266     if (map.fromShifts != null)
267     {
268       for (int[] r : map.fromShifts)
269       {
270         fromShifts.add(new int[] { r[0], r[1] });
271       }
272     }
273     if (map.toShifts != null)
274     {
275       for (int[] r : map.toShifts)
276       {
277         toShifts.add(new int[] { r[0], r[1] });
278       }
279     }
280   }
281
282   /**
283    * Constructor given ranges as lists of [start, end] positions
284    * 
285    * @param fromRange
286    * @param toRange
287    * @param fromRatio
288    * @param toRatio
289    */
290   public MapList(List<int[]> fromRange, List<int[]> toRange, int fromRatio,
291           int toRatio)
292   {
293     this();
294     this.fromShifts = fromRange;
295     this.toShifts = toRange;
296     this.fromRatio = fromRatio;
297     this.toRatio = toRatio;
298
299     fromLowest = Integer.MAX_VALUE;
300     fromHighest = 0;
301     for (int[] range : fromRange)
302     {
303       fromLowest = Math.min(fromLowest, range[0]);
304       fromHighest = Math.max(fromHighest, range[1]);
305     }
306
307     toLowest = Integer.MAX_VALUE;
308     toHighest = 0;
309     for (int[] range : toRange)
310     {
311       toLowest = Math.min(toLowest, range[0]);
312       toHighest = Math.max(toHighest, range[1]);
313     }
314   }
315
316   /**
317    * get all mapped positions from 'from' to 'to'
318    * 
319    * @return int[][] { int[] { fromStart, fromFinish, toStart, toFinish }, int
320    *         [fromFinish-fromStart+2] { toStart..toFinish mappings}}
321    */
322   protected int[][] makeFromMap()
323   {
324     // TODO not used - remove??
325     return posMap(fromShifts, fromRatio, toShifts, toRatio);
326   }
327
328   /**
329    * get all mapped positions from 'to' to 'from'
330    * 
331    * @return int[to position]=position mapped in from
332    */
333   protected int[][] makeToMap()
334   {
335     // TODO not used - remove??
336     return posMap(toShifts, toRatio, fromShifts, fromRatio);
337   }
338
339   /**
340    * construct an int map for intervals in intVals
341    * 
342    * @param shiftTo
343    * @return int[] { from, to pos in range }, int[range.to-range.from+1]
344    *         returning mapped position
345    */
346   private int[][] posMap(List<int[]> shiftTo, int ratio,
347           List<int[]> shiftFrom, int toRatio)
348   {
349     // TODO not used - remove??
350     int iv = 0, ivSize = shiftTo.size();
351     if (iv >= ivSize)
352     {
353       return null;
354     }
355     int[] intv = shiftTo.get(iv++);
356     int from = intv[0], to = intv[1];
357     if (from > to)
358     {
359       from = intv[1];
360       to = intv[0];
361     }
362     while (iv < ivSize)
363     {
364       intv = shiftTo.get(iv++);
365       if (intv[0] < from)
366       {
367         from = intv[0];
368       }
369       if (intv[1] < from)
370       {
371         from = intv[1];
372       }
373       if (intv[0] > to)
374       {
375         to = intv[0];
376       }
377       if (intv[1] > to)
378       {
379         to = intv[1];
380       }
381     }
382     int tF = 0, tT = 0;
383     int mp[][] = new int[to - from + 2][];
384     for (int i = 0; i < mp.length; i++)
385     {
386       int[] m = shift(i + from, shiftTo, ratio, shiftFrom, toRatio);
387       if (m != null)
388       {
389         if (i == 0)
390         {
391           tF = tT = m[0];
392         }
393         else
394         {
395           if (m[0] < tF)
396           {
397             tF = m[0];
398           }
399           if (m[0] > tT)
400           {
401             tT = m[0];
402           }
403         }
404       }
405       mp[i] = m;
406     }
407     int[][] map = new int[][] { new int[] { from, to, tF, tT },
408         new int[to - from + 2] };
409
410     map[0][2] = tF;
411     map[0][3] = tT;
412
413     for (int i = 0; i < mp.length; i++)
414     {
415       if (mp[i] != null)
416       {
417         map[1][i] = mp[i][0] - tF;
418       }
419       else
420       {
421         map[1][i] = -1; // indicates an out of range mapping
422       }
423     }
424     return map;
425   }
426
427   /**
428    * addShift
429    * 
430    * @param pos
431    *          start position for shift (in original reference frame)
432    * @param shift
433    *          length of shift
434    * 
435    *          public void addShift(int pos, int shift) { int sidx = 0; int[]
436    *          rshift=null; while (sidx<shifts.size() && (rshift=(int[])
437    *          shifts.elementAt(sidx))[0]<pos) sidx++; if (sidx==shifts.size())
438    *          shifts.insertElementAt(new int[] { pos, shift}, sidx); else
439    *          rshift[1]+=shift; }
440    */
441
442   /**
443    * shift from pos to To(pos)
444    * 
445    * @param pos
446    *          int
447    * @return int shifted position in To, frameshift in From, direction of mapped
448    *         symbol in To
449    */
450   public int[] shiftFrom(int pos)
451   {
452     return shift(pos, fromShifts, fromRatio, toShifts, toRatio);
453   }
454
455   /**
456    * inverse of shiftFrom - maps pos in To to a position in From
457    * 
458    * @param pos
459    *          (in To)
460    * @return shifted position in From, frameshift in To, direction of mapped
461    *         symbol in From
462    */
463   public int[] shiftTo(int pos)
464   {
465     return shift(pos, toShifts, toRatio, fromShifts, fromRatio);
466   }
467
468   /**
469    * 
470    * @param shiftTo
471    * @param fromRatio
472    * @param shiftFrom
473    * @param toRatio
474    * @return
475    */
476   protected static int[] shift(int pos, List<int[]> shiftTo, int fromRatio,
477           List<int[]> shiftFrom, int toRatio)
478   {
479     // TODO: javadoc; tests
480     int[] fromCount = countPos(shiftTo, pos);
481     if (fromCount == null)
482     {
483       return null;
484     }
485     int fromRemainder = (fromCount[0] - 1) % fromRatio;
486     int toCount = 1 + (((fromCount[0] - 1) / fromRatio) * toRatio);
487     int[] toPos = countToPos(shiftFrom, toCount);
488     if (toPos == null)
489     {
490       return null; // throw new Error("Bad Mapping!");
491     }
492     // System.out.println(fromCount[0]+" "+fromCount[1]+" "+toCount);
493     return new int[] { toPos[0], fromRemainder, toPos[1] };
494   }
495
496   /**
497    * count how many positions pos is along the series of intervals.
498    * 
499    * @param shiftTo
500    * @param pos
501    * @return number of positions or null if pos is not within intervals
502    */
503   protected static int[] countPos(List<int[]> shiftTo, int pos)
504   {
505     int count = 0, intv[], iv = 0, ivSize = shiftTo.size();
506     while (iv < ivSize)
507     {
508       intv = shiftTo.get(iv++);
509       if (intv[0] <= intv[1])
510       {
511         if (pos >= intv[0] && pos <= intv[1])
512         {
513           return new int[] { count + pos - intv[0] + 1, +1 };
514         }
515         else
516         {
517           count += intv[1] - intv[0] + 1;
518         }
519       }
520       else
521       {
522         if (pos >= intv[1] && pos <= intv[0])
523         {
524           return new int[] { count + intv[0] - pos + 1, -1 };
525         }
526         else
527         {
528           count += intv[0] - intv[1] + 1;
529         }
530       }
531     }
532     return null;
533   }
534
535   /**
536    * count out pos positions into a series of intervals and return the position
537    * 
538    * @param shiftFrom
539    * @param pos
540    * @return position pos in interval set
541    */
542   protected static int[] countToPos(List<int[]> shiftFrom, int pos)
543   {
544     int count = 0, diff = 0, iv = 0, ivSize = shiftFrom.size();
545     int[] intv = { 0, 0 };
546     while (iv < ivSize)
547     {
548       intv = shiftFrom.get(iv++);
549       diff = intv[1] - intv[0];
550       if (diff >= 0)
551       {
552         if (pos <= count + 1 + diff)
553         {
554           return new int[] { pos - count - 1 + intv[0], +1 };
555         }
556         else
557         {
558           count += 1 + diff;
559         }
560       }
561       else
562       {
563         if (pos <= count + 1 - diff)
564         {
565           return new int[] { intv[0] - (pos - count - 1), -1 };
566         }
567         else
568         {
569           count += 1 - diff;
570         }
571       }
572     }
573     return null;// (diff<0) ? (intv[1]-1) : (intv[0]+1);
574   }
575
576   /**
577    * find series of intervals mapping from start-end in the From map.
578    * 
579    * @param start
580    *          position mapped 'to'
581    * @param end
582    *          position mapped 'to'
583    * @return series of [start, end] ranges in sequence mapped 'from'
584    */
585   public int[] locateInFrom(int start, int end)
586   {
587     // inefficient implementation
588     int fromStart[] = shiftTo(start);
589     // needs to be inclusive of end of symbol position
590     int fromEnd[] = shiftTo(end);
591
592     return getIntervals(fromShifts, fromStart, fromEnd, fromRatio);
593   }
594
595   /**
596    * find series of intervals mapping from start-end in the to map.
597    * 
598    * @param start
599    *          position mapped 'from'
600    * @param end
601    *          position mapped 'from'
602    * @return series of [start, end] ranges in sequence mapped 'to'
603    */
604   public int[] locateInTo(int start, int end)
605   {
606     int toStart[] = shiftFrom(start);
607     int toEnd[] = shiftFrom(end);
608     return getIntervals(toShifts, toStart, toEnd, toRatio);
609   }
610
611   /**
612    * like shift - except returns the intervals in the given vector of shifts
613    * which were spanned in traversing fromStart to fromEnd
614    * 
615    * @param shiftFrom
616    * @param fromStart
617    * @param fromEnd
618    * @param fromRatio2
619    * @return series of from,to intervals from from first position of starting
620    *         region to final position of ending region inclusive
621    */
622   protected static int[] getIntervals(List<int[]> shiftFrom,
623           int[] fromStart, int[] fromEnd, int fromRatio2)
624   {
625     if (fromStart == null || fromEnd == null)
626     {
627       return null;
628     }
629     int startpos, endpos;
630     startpos = fromStart[0]; // first position in fromStart
631     endpos = fromEnd[0]; // last position in fromEnd
632     int endindx = (fromRatio2 - 1); // additional positions to get to last
633     // position from endpos
634     int intv = 0, intvSize = shiftFrom.size();
635     int iv[], i = 0, fs = -1, fe_s = -1, fe = -1; // containing intervals
636     // search intervals to locate ones containing startpos and count endindx
637     // positions on from endpos
638     while (intv < intvSize && (fs == -1 || fe == -1))
639     {
640       iv = shiftFrom.get(intv++);
641       if (fe_s > -1)
642       {
643         endpos = iv[0]; // start counting from beginning of interval
644         endindx--; // inclusive of endpos
645       }
646       if (iv[0] <= iv[1])
647       {
648         if (fs == -1 && startpos >= iv[0] && startpos <= iv[1])
649         {
650           fs = i;
651         }
652         if (endpos >= iv[0] && endpos <= iv[1])
653         {
654           if (fe_s == -1)
655           {
656             fe_s = i;
657           }
658           if (fe_s != -1)
659           {
660             if (endpos + endindx <= iv[1])
661             {
662               fe = i;
663               endpos = endpos + endindx; // end of end token is within this
664               // interval
665             }
666             else
667             {
668               endindx -= iv[1] - endpos; // skip all this interval too
669             }
670           }
671         }
672       }
673       else
674       {
675         if (fs == -1 && startpos <= iv[0] && startpos >= iv[1])
676         {
677           fs = i;
678         }
679         if (endpos <= iv[0] && endpos >= iv[1])
680         {
681           if (fe_s == -1)
682           {
683             fe_s = i;
684           }
685           if (fe_s != -1)
686           {
687             if (endpos - endindx >= iv[1])
688             {
689               fe = i;
690               endpos = endpos - endindx; // end of end token is within this
691               // interval
692             }
693             else
694             {
695               endindx -= endpos - iv[1]; // skip all this interval too
696             }
697           }
698         }
699       }
700       i++;
701     }
702     if (fs == fe && fe == -1)
703     {
704       return null;
705     }
706     List<int[]> ranges = new ArrayList<int[]>();
707     if (fs <= fe)
708     {
709       intv = fs;
710       i = fs;
711       // truncate initial interval
712       iv = shiftFrom.get(intv++);
713       iv = new int[] { iv[0], iv[1] };// clone
714       if (i == fs)
715       {
716         iv[0] = startpos;
717       }
718       while (i != fe)
719       {
720         ranges.add(iv); // add initial range
721         iv = shiftFrom.get(intv++); // get next interval
722         iv = new int[] { iv[0], iv[1] };// clone
723         i++;
724       }
725       if (i == fe)
726       {
727         iv[1] = endpos;
728       }
729       ranges.add(iv); // add only - or final range
730     }
731     else
732     {
733       // walk from end of interval.
734       i = shiftFrom.size() - 1;
735       while (i > fs)
736       {
737         i--;
738       }
739       iv = shiftFrom.get(i);
740       iv = new int[] { iv[1], iv[0] };// reverse and clone
741       // truncate initial interval
742       if (i == fs)
743       {
744         iv[0] = startpos;
745       }
746       while (--i != fe)
747       { // fix apparent logic bug when fe==-1
748         ranges.add(iv); // add (truncated) reversed interval
749         iv = shiftFrom.get(i);
750         iv = new int[] { iv[1], iv[0] }; // reverse and clone
751       }
752       if (i == fe)
753       {
754         // interval is already reversed
755         iv[1] = endpos;
756       }
757       ranges.add(iv); // add only - or final range
758     }
759     // create array of start end intervals.
760     int[] range = null;
761     if (ranges != null && ranges.size() > 0)
762     {
763       range = new int[ranges.size() * 2];
764       intv = 0;
765       intvSize = ranges.size();
766       i = 0;
767       while (intv < intvSize)
768       {
769         iv = ranges.get(intv);
770         range[i++] = iv[0];
771         range[i++] = iv[1];
772         ranges.set(intv++, null); // remove
773       }
774     }
775     return range;
776   }
777
778   /**
779    * get the 'initial' position of mpos in To
780    * 
781    * @param mpos
782    *          position in from
783    * @return position of first word in to reference frame
784    */
785   public int getToPosition(int mpos)
786   {
787     // TODO not used - remove??
788     int[] mp = shiftTo(mpos);
789     if (mp != null)
790     {
791       return mp[0];
792     }
793     return mpos;
794   }
795
796   /**
797    * get range of positions in To frame for the mpos word in From
798    * 
799    * @param mpos
800    *          position in From
801    * @return null or int[] first position in To for mpos, last position in to
802    *         for Mpos
803    */
804   public int[] getToWord(int mpos)
805   {
806     int[] mp = shiftTo(mpos);
807     if (mp != null)
808     {
809       return new int[] { mp[0], mp[0] + mp[2] * (getFromRatio() - 1) };
810     }
811     return null;
812   }
813
814   /**
815    * get From position in the associated reference frame for position pos in the
816    * associated sequence.
817    * 
818    * @param pos
819    * @return
820    */
821   public int getMappedPosition(int pos)
822   {
823     // TODO not used - remove??
824     int[] mp = shiftFrom(pos);
825     if (mp != null)
826     {
827       return mp[0];
828     }
829     return pos;
830   }
831
832   public int[] getMappedWord(int pos)
833   {
834     // TODO not used - remove??
835     int[] mp = shiftFrom(pos);
836     if (mp != null)
837     {
838       return new int[] { mp[0], mp[0] + mp[2] * (getToRatio() - 1) };
839     }
840     return null;
841   }
842
843   /**
844    * 
845    * @return a MapList whose From range is this maplist's To Range, and vice
846    *         versa
847    */
848   public MapList getInverse()
849   {
850     return new MapList(getToRanges(), getFromRanges(), getToRatio(),
851             getFromRatio());
852   }
853
854   /**
855    * test for containment rather than equivalence to another mapping
856    * 
857    * @param map
858    *          to be tested for containment
859    * @return true if local or mapped range map contains or is contained by this
860    *         mapping
861    */
862   public boolean containsEither(boolean local, MapList map)
863   {
864     // TODO not used - remove?
865     if (local)
866     {
867       return ((getFromLowest() >= map.getFromLowest() && getFromHighest() <= map
868               .getFromHighest()) || (getFromLowest() <= map.getFromLowest() && getFromHighest() >= map
869               .getFromHighest()));
870     }
871     else
872     {
873       return ((getToLowest() >= map.getToLowest() && getToHighest() <= map
874               .getToHighest()) || (getToLowest() <= map.getToLowest() && getToHighest() >= map
875               .getToHighest()));
876     }
877   }
878
879   /**
880    * String representation - for debugging, not guaranteed not to change
881    */
882   @Override
883   public String toString()
884   {
885     StringBuilder sb = new StringBuilder(64);
886     sb.append("From (").append(fromRatio).append(":").append(toRatio)
887             .append(") [");
888     for (int[] shift : fromShifts)
889     {
890       sb.append(" ").append(Arrays.toString(shift));
891     }
892     sb.append(" ] To [");
893     for (int[] shift : toShifts)
894     {
895       sb.append(" ").append(Arrays.toString(shift));
896     }
897     sb.append(" ]");
898     return sb.toString();
899   }
900 }