a911666f37c4d07e82fa8d342db573795ce0deda
[jalview.git] / src / jalview / datamodel / features / NCList.java
1 package jalview.datamodel.features;
2
3 import java.util.ArrayList;
4 import java.util.Collections;
5 import java.util.List;
6
7 /**
8  * An adapted implementation of NCList as described in the paper
9  * 
10  * <pre>
11  * Nested Containment List (NCList): a new algorithm for accelerating
12  * interval query of genome alignment and interval databases
13  * - Alexander V. Alekseyenko, Christopher J. Lee
14  * https://doi.org/10.1093/bioinformatics/btl647
15  * </pre>
16  */
17 public class NCList<T extends ContiguousI>
18 {
19   /*
20    * the number of ranges represented
21    */
22   private int size;
23
24   /*
25    * a list, in start position order, of sublists of ranges ordered so 
26    * that each contains (or is the same as) the one that follows it
27    */
28   private List<NCNode<T>> subranges;
29
30   /**
31    * Constructor given a list of things that are each located on a contiguous
32    * interval. Note that the constructor may reorder the list.
33    * <p>
34    * We assume here that for each range, start &lt;= end. Behaviour for reverse
35    * ordered ranges is undefined.
36    * 
37    * @param ranges
38    */
39   public NCList(List<T> ranges)
40   {
41     this();
42     build(ranges);
43   }
44
45   /**
46    * Sort and group ranges into sublists where each sublist represents a region
47    * and its contained subregions
48    * 
49    * @param ranges
50    */
51   protected void build(List<T> ranges)
52   {
53     /*
54      * sort by start ascending so that contained intervals 
55      * follow their containing interval
56      */
57     Collections.sort(ranges, RangeComparator.BY_START_POSITION);
58
59     List<Range> sublists = buildSubranges(ranges);
60
61     /*
62      * convert each subrange to an NCNode consisting of a range and
63      * (possibly) its contained NCList
64      */
65     for (Range sublist : sublists)
66     {
67       subranges.add(new NCNode<T>(ranges.subList(sublist.start,
68               sublist.end + 1)));
69     }
70
71     size = ranges.size();
72   }
73
74   public NCList(T entry)
75   {
76     this();
77     subranges.add(new NCNode<T>(entry));
78     size = 1;
79   }
80
81   public NCList()
82   {
83     subranges = new ArrayList<NCNode<T>>();
84   }
85
86   /**
87    * Traverses the sorted ranges to identify sublists, within which each
88    * interval contains the one that follows it
89    * 
90    * @param ranges
91    * @return
92    */
93   protected List<Range> buildSubranges(List<T> ranges)
94   {
95     List<Range> sublists = new ArrayList<Range>();
96     
97     if (ranges.isEmpty())
98     {
99       return sublists;
100     }
101
102     int listStartIndex = 0;
103     long lastEndPos = Long.MAX_VALUE;
104
105     for (int i = 0; i < ranges.size(); i++)
106     {
107       ContiguousI nextInterval = ranges.get(i);
108       long nextStart = nextInterval.getBegin();
109       long nextEnd = nextInterval.getEnd();
110       if (nextStart > lastEndPos || nextEnd > lastEndPos)
111       {
112         /*
113          * this interval is not contained in the preceding one 
114          * close off the last sublist
115          */
116         sublists.add(new Range(listStartIndex, i - 1));
117         listStartIndex = i;
118       }
119       lastEndPos = nextEnd;
120     }
121
122     sublists.add(new Range(listStartIndex, ranges.size() - 1));
123     return sublists;
124   }
125
126   /**
127    * Adds one entry to the stored set (with duplicates allowed)
128    * 
129    * @param entry
130    */
131   public void add(T entry)
132   {
133     add(entry, true);
134   }
135
136   /**
137    * Adds one entry to the stored set, and returns true, unless allowDuplicates
138    * is set to false and it is already contained (by object equality test), in
139    * which case it is not added and this method returns false.
140    * 
141    * @param entry
142    * @param allowDuplicates
143    * @return
144    */
145   public synchronized boolean add(T entry, boolean allowDuplicates)
146   {
147     if (!allowDuplicates && contains(entry))
148     {
149       return false;
150     }
151
152     size++;
153     long start = entry.getBegin();
154     long end = entry.getEnd();
155
156     /*
157      * cases:
158      * - precedes all subranges: add as NCNode on front of list
159      * - follows all subranges: add as NCNode on end of list
160      * - enclosed by a subrange - add recursively to subrange
161      * - encloses one or more subranges - push them inside it
162      * - none of the above - add as a new node and resort nodes list (?)
163      */
164
165     /*
166      * find the first subrange whose end does not precede entry's start
167      */
168     int candidateIndex = findFirstOverlap(start);
169     if (candidateIndex == -1)
170     {
171       /*
172        * all subranges precede this one - add it on the end
173        */
174       subranges.add(new NCNode<T>(entry));
175       return true;
176     }
177
178     /*
179      * search for maximal span of subranges i-k that the new entry
180      * encloses; or a subrange that encloses the new entry
181      */
182     boolean enclosing = false;
183     int firstEnclosed = 0;
184     int lastEnclosed = 0;
185     boolean overlapping = false;
186
187     for (int j = candidateIndex; j < subranges.size(); j++)
188     {
189       NCNode<T> subrange = subranges.get(j);
190
191       if (end < subrange.getBegin() && !overlapping && !enclosing)
192       {
193         /*
194          * new entry lies between subranges j-1 j
195          */
196         subranges.add(j, new NCNode<T>(entry));
197         return true;
198       }
199
200       if (subrange.getBegin() <= start && subrange.getEnd() >= end)
201       {
202         /*
203          * push new entry inside this subrange as it encloses it
204          */
205         subrange.add(entry);
206         return true;
207       }
208       
209       if (start <= subrange.getBegin())
210       {
211         if (end >= subrange.getEnd())
212         {
213           /*
214            * new entry encloses this subrange (and possibly preceding ones);
215            * continue to find the maximal list it encloses
216            */
217           if (!enclosing)
218           {
219             firstEnclosed = j;
220           }
221           lastEnclosed = j;
222           enclosing = true;
223           continue;
224         }
225         else
226         {
227           /*
228            * entry spans from before this subrange to inside it
229            */
230           if (enclosing)
231           {
232             /*
233              * entry encloses one or more preceding subranges
234              */
235             addEnclosingRange(entry, firstEnclosed, lastEnclosed);
236             return true;
237           }
238           else
239           {
240             /*
241              * entry spans two subranges but doesn't enclose any
242              * so just add it 
243              */
244             subranges.add(j, new NCNode<T>(entry));
245             return true;
246           }
247         }
248       }
249       else
250       {
251         overlapping = true;
252       }
253     }
254
255     /*
256      * drops through to here if new range encloses all others
257      * or overlaps the last one
258      */
259     if (enclosing)
260     {
261       addEnclosingRange(entry, firstEnclosed, lastEnclosed);
262     }
263     else
264     {
265       subranges.add(new NCNode<T>(entry));
266     }
267
268     return true;
269   }
270   
271   /**
272    * Answers true if this NCList contains the given entry (by object equality
273    * test), else false
274    * 
275    * @param entry
276    * @return
277    */
278   public boolean contains(T entry)
279   {
280     /*
281      * find the first sublist that might overlap, i.e. 
282      * the first whose end position is >= from
283      */
284     int candidateIndex = findFirstOverlap(entry.getBegin());
285
286     if (candidateIndex == -1)
287     {
288       return false;
289     }
290
291     int to = entry.getEnd();
292
293     for (int i = candidateIndex; i < subranges.size(); i++)
294     {
295       NCNode<T> candidate = subranges.get(i);
296       if (candidate.getBegin() > to)
297       {
298         /*
299          * we are past the end of our target range
300          */
301         break;
302       }
303       if (candidate.contains(entry))
304       {
305         return true;
306       }
307     }
308     return false;
309   }
310
311   /**
312    * Update the tree so that the range of the new entry encloses subranges i to
313    * j (inclusive). That is, replace subranges i-j (inclusive) with a new
314    * subrange that contains them.
315    * 
316    * @param entry
317    * @param i
318    * @param j
319    */
320   protected synchronized void addEnclosingRange(T entry, final int i,
321           final int j)
322   {
323     NCList<T> newNCList = new NCList<T>();
324     newNCList.addNodes(subranges.subList(i, j + 1));
325     NCNode<T> newNode = new NCNode<T>(entry, newNCList);
326     for (int k = j; k >= i; k--)
327     {
328       subranges.remove(k);
329     }
330     subranges.add(i, newNode);
331   }
332
333   protected void addNodes(List<NCNode<T>> nodes)
334   {
335     for (NCNode<T> node : nodes)
336     {
337       subranges.add(node);
338       size += node.size();
339     }
340   }
341
342   /**
343    * Returns a (possibly empty) list of items whose extent overlaps the given
344    * range
345    * 
346    * @param from
347    *          start of overlap range (inclusive)
348    * @param to
349    *          end of overlap range (inclusive)
350    * @return
351    */
352   public List<T> findOverlaps(long from, long to)
353   {
354     List<T> result = new ArrayList<T>();
355
356     findOverlaps(from, to, result);
357     
358     return result;
359   }
360
361   /**
362    * Recursively searches the NCList adding any items that overlap the from-to
363    * range to the result list
364    * 
365    * @param from
366    * @param to
367    * @param result
368    */
369   protected void findOverlaps(long from, long to, List<T> result)
370   {
371     /*
372      * find the first sublist that might overlap, i.e. 
373      * the first whose end position is >= from
374      */
375     int candidateIndex = findFirstOverlap(from);
376
377     if (candidateIndex == -1)
378     {
379       return;
380     }
381
382     for (int i = candidateIndex; i < subranges.size(); i++)
383     {
384       NCNode<T> candidate = subranges.get(i);
385       if (candidate.getBegin() > to)
386       {
387         /*
388          * we are past the end of our target range
389          */
390         break;
391       }
392       candidate.findOverlaps(from, to, result);
393     }
394
395   }
396
397   /**
398    * Search subranges for the first one whose end position is not before the
399    * target range's start position, i.e. the first one that may overlap the
400    * target range. Returns the index in the list of the first such range found,
401    * or -1 if none found.
402    * 
403    * @param from
404    * @return
405    */
406   protected int findFirstOverlap(long from)
407   {
408     /*
409      * The NCList paper describes binary search for this step,
410      * but this not implemented here as (a) I haven't understood it yet
411      * and (b) it seems to imply complications for adding to an NCList
412      */
413
414     int i = 0;
415     if (subranges != null)
416     {
417       for (NCNode<T> subrange : subranges)
418       {
419         if (subrange.getEnd() >= from)
420         {
421           return i;
422         }
423         i++;
424       }
425     }
426     return -1;
427   }
428
429   /**
430    * Formats the tree as a bracketed list e.g.
431    * 
432    * <pre>
433    * [1-100 [10-30 [10-20]], 15-30 [20-20]]
434    * </pre>
435    */
436   @Override
437   public String toString()
438   {
439     return subranges.toString();
440   }
441
442   /**
443    * Returns a string representation of the data where containment is shown by
444    * indentation on new lines
445    * 
446    * @return
447    */
448   public String prettyPrint()
449   {
450     StringBuilder sb = new StringBuilder(512);
451     int offset = 0;
452     int indent = 2;
453     prettyPrint(sb, offset, indent);
454     sb.append(System.lineSeparator());
455     return sb.toString();
456   }
457
458   /**
459    * @param sb
460    * @param offset
461    * @param indent
462    */
463   void prettyPrint(StringBuilder sb, int offset, int indent)
464   {
465     boolean first = true;
466     for (NCNode<T> subrange : subranges)
467     {
468       if (!first)
469       {
470         sb.append(System.lineSeparator());
471       }
472       first = false;
473       subrange.prettyPrint(sb, offset, indent);
474     }
475   }
476
477   /**
478    * Answers true if the data held satisfy the rules of construction of an
479    * NCList, else false.
480    * 
481    * @return
482    */
483   public boolean isValid()
484   {
485     return isValid(Integer.MIN_VALUE, Integer.MAX_VALUE);
486   }
487
488   /**
489    * Answers true if the data held satisfy the rules of construction of an
490    * NCList bounded within the given start-end range, else false.
491    * <p>
492    * Each subrange must lie within start-end (inclusive). Subranges must be
493    * ordered by start position ascending.
494    * <p>
495    * 
496    * @param start
497    * @param end
498    * @return
499    */
500   boolean isValid(final int start, final int end)
501   {
502     int lastStart = start;
503     for (NCNode<T> subrange : subranges)
504     {
505       if (subrange.getBegin() < lastStart)
506       {
507         System.err.println("error in NCList: range " + subrange.toString()
508                 + " starts before " + lastStart);
509         return false;
510       }
511       if (subrange.getEnd() > end)
512       {
513         System.err.println("error in NCList: range " + subrange.toString()
514                 + " ends after " + end);
515         return false;
516       }
517       lastStart = subrange.getBegin();
518
519       if (!subrange.isValid())
520       {
521         return false;
522       }
523     }
524     return true;
525   }
526
527   /**
528    * Answers the lowest start position enclosed by the ranges
529    * 
530    * @return
531    */
532   public int getStart()
533   {
534     return subranges.isEmpty() ? 0 : subranges.get(0).getBegin();
535   }
536
537   /**
538    * Returns the number of ranges held (deep count)
539    * 
540    * @return
541    */
542   public int size()
543   {
544     return size;
545   }
546
547   /**
548    * Returns a list of all entries stored
549    * 
550    * @return
551    */
552   public List<T> getEntries()
553   {
554     List<T> result = new ArrayList<T>();
555     getEntries(result);
556     return result;
557   }
558
559   /**
560    * Adds all contained entries to the given list
561    * 
562    * @param result
563    */
564   void getEntries(List<T> result)
565   {
566     for (NCNode<T> subrange : subranges)
567     {
568       subrange.getEntries(result);
569     }
570   }
571
572   /**
573    * Deletes the given entry from the store, returning true if it was found (and
574    * deleted), else false. This method makes no assumption that the entry is in
575    * the 'expected' place in the store, in case it has been modified since it
576    * was added. Only the first 'same object' match is deleted, not 'equal' or
577    * multiple objects.
578    * 
579    * @param entry
580    */
581   public synchronized boolean delete(T entry)
582   {
583     if (entry == null)
584     {
585       return false;
586     }
587     for (int i = 0; i < subranges.size(); i++)
588     {
589       NCNode<T> subrange = subranges.get(i);
590       NCList<T> subRegions = subrange.getSubRegions();
591
592       if (subrange.getRegion() == entry)
593       {
594         /*
595          * if the subrange is rooted on this entry, promote its
596          * subregions (if any) to replace the subrange here;
597          * NB have to resort subranges after doing this since e.g.
598          * [10-30 [12-20 [16-18], 13-19]]
599          * after deleting 12-20, 16-18 is promoted to sibling of 13-19
600          * but should follow it in the list of subranges of 10-30 
601          */
602         subranges.remove(i);
603         if (subRegions != null)
604         {
605           subranges.addAll(subRegions.subranges);
606           Collections.sort(subranges, RangeComparator.BY_START_POSITION);
607         }
608         size--;
609         return true;
610       }
611       else
612       {
613         if (subRegions != null && subRegions.delete(entry))
614         {
615           size--;
616           subrange.deleteSubRegionsIfEmpty();
617           return true;
618         }
619       }
620     }
621     return false;
622   }
623 }