bd5042002d803d78ce667b9253835d98ff9e3e3c
[jalview.git] / src / jalview / datamodel / features / FeatureStoreJS.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.datamodel.features;
22
23 import jalview.datamodel.SequenceFeature;
24
25 import java.util.ArrayList;
26 import java.util.List;
27
28 /**
29  * An adaption of FeatureStore that is efficient and lightweight, accelerating
30  * processing speed in JavaScript.
31  * 
32  * @author gmcarstairs
33  * @author Bob Hanson 2019.08.03
34  *
35  */
36 public class FeatureStoreJS extends FeatureStore
37 {
38   boolean contactsTainted = true;
39
40   /**
41    * internal reference to features as an ArrayList
42    */
43   private ArrayList<SequenceFeature> featureList;
44
45   /**
46    * Constructor
47    */
48   public FeatureStoreJS()
49   {
50     features = featureList = new ArrayList<>();
51   }
52
53   /**
54    * Add a contact feature to the lists that hold them ordered by start (first
55    * contact) and by end (second contact) position, ensuring the lists remain
56    * ordered, and returns true. This method allows duplicate features to be
57    * added, so test before calling to avoid this.
58    * 
59    * @param feature
60    * @return
61    */
62   @Override
63   protected synchronized boolean addContactFeature(SequenceFeature feature)
64   {
65     if (contactFeatureStarts == null)
66     {
67       contactFeatureStarts = new ArrayList<>();
68       contactFeatureEnds = new ArrayList<>();
69     }
70     contactFeatureStarts.add(
71             findFirstBegin(contactFeatureStarts, feature.begin), feature);
72     contactFeatureEnds.add(findFirstEnd(contactFeatureEnds, feature.end),
73             feature);
74     return true;
75   }
76
77   /**
78    * Adds one feature to the IntervalStore that can manage nested features
79    * (creating the IntervalStore if necessary)
80    */
81   @Override
82   protected synchronized void addNestedFeature(SequenceFeature feature)
83   {
84     featureList.add(findFirstBegin(featureList, feature.begin), feature);
85   }
86
87   /**
88    * Returns a (possibly empty) list of features whose extent overlaps the given
89    * range. The returned list is not ordered. Contact features are included if
90    * either of the contact points lies within the range.
91    * 
92    * @param start
93    *          start position of overlap range (inclusive)
94    * @param end
95    *          end position of overlap range (inclusive)
96    * @return
97    */
98
99   @Override
100   public List<SequenceFeature> findOverlappingFeatures(long start, long end,
101           List<SequenceFeature> result)
102   {
103     if (result == null)
104     {
105       result = new ArrayList<>();
106     }
107     if (contactFeatureStarts != null)
108     {
109       if (start == end)
110       {
111         findContactPoints(contactFeatureStarts, start, result, true);
112         findContactPoints(contactFeatureEnds, start, result, false);
113       }
114       else
115       {
116         findContactFeatures(start, end, result);
117       }
118     }
119     if (featureList.size() > 0)
120     {
121       findOverlaps(start, end, result);
122     }
123     return result;
124   }
125
126   // The following methods use a linked list of containment in SequenceFeature
127   // rather than IntervalStore.
128   //
129   // There are two parts --- initialization, and overlap searching.
130   //
131   // Initialization involves two steps:
132   //
133   // (1) sorting of features by start position using a standard Array.sort with
134   // Comparator.
135   // (2) linking of features, effectively nesting them.
136   //
137   // Searching involves three steps:
138   //
139   // (1) binary search for a starting point within the sorted features array.
140   // (2) traverse the linked lists with an end check to read out the
141   // overlapped features at this position.
142   // (3) For an interval, find the last feature that starts in this interval,
143   // and add all features up through that feature.
144   //
145   // All of this is done with very simple standard methods.
146
147   // Initialization
148
149   /*
150    * contact features ordered by first contact position
151    */
152   private SequenceFeature[] orderedFeatureStarts;
153
154   private void rebuildArrays(int n)
155   {
156     // Arrays.sort(orderedFeatureStarts, startComp);
157     orderedFeatureStarts = featureList
158             .toArray(new SequenceFeature[featureList.size()]);
159     linkFeatures(orderedFeatureStarts);
160   }
161
162   // /**
163   // * just a standard Comparator
164   // */
165   // private static StartComparator startComp;
166   //
167   // class StartComparator implements Comparator<SequenceFeature>
168   // {
169   //
170   // @Override
171   // public int compare(SequenceFeature o1, SequenceFeature o2)
172   // {
173   // int p1 = o1.begin;
174   // int p2 = o2.begin;
175   // return (p1 < p2 ? -1 : p1 > p2 ? 1 : 0);
176   // }
177   //
178   // }
179
180   /**
181    * Run through the sorted sequence array once, building the containedBy linked
182    * list references. Does a check first to make sure there is actually
183    * something out there that is overlapping. A null for sf.containedBy means
184    * there are no overlaps for this feature.
185    * 
186    * @param intervals
187    */
188   private void linkFeatures(SequenceFeature[] intervals)
189   {
190     if (intervals.length < 2)
191     {
192       return;
193     }
194     int maxEnd = intervals[0].end;
195     for (int i = 1, n = intervals.length; i < n; i++)
196     {
197       SequenceFeature sf = intervals[i];
198       if (sf.begin <= maxEnd)
199       {
200         sf.containedBy = getContainedBy(intervals[i - 1], sf);
201       }
202       if (sf.end > maxEnd)
203       {
204         maxEnd = sf.end;
205       }
206     }
207   }
208
209   /**
210    * Since we are traversing the sorted feature array in a forward direction,
211    * all elements prior to the one we are working on have been fully linked. All
212    * we are doing is following those links until we find the first array feature
213    * with a containedBy element that has an end &gt;= our begin point. It is
214    * generally a very short list -- maybe one or two depths. But it might be
215    * more than that.
216    * 
217    * @param sf
218    * @param sf0
219    * @return
220    */
221   private SequenceFeature getContainedBy(SequenceFeature sf,
222           SequenceFeature sf0)
223   {
224     int begin = sf0.begin;
225     while (sf != null)
226     {
227       if (begin <= sf.end)
228       {
229         // System.out.println("\nFS found " + sf0.index + ":" + sf0
230         // + "\nFS in " + sf.index + ":" + sf);
231         return sf;
232       }
233       sf = sf.containedBy;
234     }
235     return null;
236   }
237
238   // search-stage methods
239
240   /**
241    * Binary search for contact start or end at a given (Overview) position.
242    * 
243    * @param l
244    * @param pos
245    * @param result
246    * @param isStart
247    * 
248    * @author Bob Hanson 2019.07.30
249    */
250   private static void findContactPoints(List<SequenceFeature> l, long pos,
251           List<SequenceFeature> result, boolean isStart)
252   {
253     int low = 0;
254     int high = l.size() - 1;
255     while (low <= high)
256     {
257       int mid = (low + high) >>> 1;
258       SequenceFeature f = l.get(mid);
259       switch (Long.signum((isStart ? f.begin : f.end) - pos))
260       {
261       case -1:
262         low = mid + 1;
263         continue;
264       case 1:
265         high = mid - 1;
266         continue;
267       case 0:
268         int m = mid;
269         result.add(f);
270         // could be "5" in 12345556788 ?
271         while (++mid <= high && (f = l.get(mid)) != null
272                 && (isStart ? f.begin : f.end) == pos)
273         {
274           result.add(f);
275         }
276         while (--m >= low && (f = l.get(m)) != null
277                 && (isStart ? f.begin : f.end) == pos)
278         {
279           result.add(f);
280         }
281         return;
282       }
283     }
284   }
285
286   /**
287    * Find all overlaps; special case when there is only one feature. The
288    * required array of start-sorted SequenceFeature is created lazily.
289    * 
290    * @param start
291    * @param end
292    * @param result
293    */
294   private void findOverlaps(long start, long end,
295           List<SequenceFeature> result)
296   {
297     int n = featureList.size();
298     switch (n)
299     {
300     case 0:
301       return;
302     case 1:
303       checkOne(featureList.get(0), start, end,
304               result);
305       return;
306     default:
307       if (orderedFeatureStarts == null)
308       {
309         rebuildArrays(n);
310       }
311       break;
312     }
313
314     // (1) Find the closest feature to this position.
315
316     int index = findClosestFeature(orderedFeatureStarts, start);
317     SequenceFeature sf = (index < 0 ? null : orderedFeatureStarts[index]);
318
319     // (2) Traverse the containedBy field, checking for overlap.
320
321     while (sf != null)
322     {
323       if (sf.end >= start)
324       {
325         result.add(sf);
326       }
327       sf = sf.containedBy;
328     }
329
330     // (3) For an interval, find the last feature that starts in this interval,
331     // and add all features up through that feature.
332
333     if (end > start)
334     {
335       // fill in with all features that start within this interval, fully
336       // inclusive
337       int index2 = findClosestFeature(orderedFeatureStarts, end);
338       while (++index <= index2)
339       {
340         result.add(orderedFeatureStarts[index]);
341       }
342
343     }
344   }
345
346   /**
347    * Quick check when we only have one feature.
348    * 
349    * @param sf
350    * @param start
351    * @param end
352    * @param result
353    */
354   private void checkOne(SequenceFeature sf, long start, long end,
355           List<SequenceFeature> result)
356   {
357     if (sf.begin <= end && sf.end >= start)
358     {
359       result.add(sf);
360     }
361     return;
362   }
363
364   @Override
365   protected boolean containsFeature(SequenceFeature feature)
366   {
367
368     int pos = findFirstBegin(featureList,
369             feature.begin);
370     int len = featureList.size();
371     while (pos < len)
372     {
373       SequenceFeature sf = featureList.get(pos);
374       if (sf.begin > feature.begin)
375       {
376         return false; // no match found
377       }
378       if (sf.equals(feature))
379       {
380         return true;
381       }
382       pos++;
383     }
384     return false;
385
386   }
387
388   /**
389    * A binary search identical to the one used for contact start/end, but here
390    * we return the feature itself. Unlike Collection.BinarySearch, all we have
391    * to be is close, not exact, and we make sure if there is a string of
392    * identical starts, then we slide to the end so that we can check all of
393    * them.
394    * 
395    * @param l
396    * @param pos
397    * @return
398    */
399   private int findClosestFeature(SequenceFeature[] l, long pos)
400   {
401     int low = 0;
402     int high = l.length - 1;
403     while (low <= high)
404     {
405       int mid = (low + high) >>> 1;
406       SequenceFeature f = l[mid];
407       switch (Long.signum(f.begin - pos))
408       {
409       case -1:
410         low = mid + 1;
411         continue;
412       case 1:
413         high = mid - 1;
414         continue;
415       case 0:
416
417         while (++mid <= high && l[mid].begin == pos)
418         {
419           ;
420         }
421         return --mid;
422       }
423     }
424     return (high < 0 ? -1 : high);
425   }
426
427   /**
428    * Adds contact features to the result list where either the second or the
429    * first contact position lies within the target range
430    * 
431    * @param from
432    * @param to
433    * @param result
434    */
435   @Override
436   protected void findContactFeatures(long from, long to,
437           List<SequenceFeature> result)
438   {
439     findContactStartOverlaps(from, to, result);
440     findContactEndOverlaps(from, to, result);
441   }
442
443   /**
444    * Adds to the result list any contact features whose end (second contact
445    * point), but not start (first contact point), lies in the query from-to
446    * range
447    * 
448    * @param from
449    * @param to
450    * @param result
451    */
452
453   private void findContactEndOverlaps(long from, long to,
454           List<SequenceFeature> result)
455   {
456     // find the first contact feature (if any)
457     // with end point not before the target range
458
459     for (int i = findFirstEnd(contactFeatureEnds,
460             from), n = contactFeatureEnds.size(); i < n; i++)
461     {
462       SequenceFeature sf = contactFeatureEnds.get(i);
463       if (sf.begin >= from && sf.begin <= to)
464       {
465         // this feature's first contact position lies in the search range
466         // so we don't include it in results a second time
467         continue;
468       }
469
470       if (sf.end > to)
471       {
472         // this feature (and all following) has end point after the target range
473         break;
474       }
475
476       // feature has end >= from and end <= to
477       // i.e. contact end point lies within overlap search range
478       result.add(sf);
479     }
480   }
481
482   /**
483    * Adds contact features whose start position lies in the from-to range to the
484    * result list
485    * 
486    * @param from
487    * @param to
488    * @param result
489    */
490
491   private void findContactStartOverlaps(long from, long to,
492           List<SequenceFeature> result)
493   {
494     for (int i = findFirstBegin(contactFeatureStarts,
495             from), n = contactFeatureStarts.size(); i < n; i++)
496     {
497       SequenceFeature sf = contactFeatureStarts.get(i);
498       if (sf.begin > to)
499       {
500         break;
501       }
502       result.add(sf);
503     }
504   }
505
506   @Override
507   protected int findFirstBegin(List<SequenceFeature> list, long pos)
508   {
509     int start = 0;
510     int end = list.size() - 1;
511     int matched = list.size();
512
513     while (start <= end)
514     {
515       int mid = (start + end) / 2;
516       if (list.get(mid).begin >= pos)
517       {
518         matched = mid;
519         end = mid - 1;
520       }
521       else
522       {
523         start = mid + 1;
524       }
525     }
526     return matched;
527   }
528
529   @Override
530   protected int findFirstEnd(List<SequenceFeature> list, long pos)
531   {
532     int start = 0;
533     int end = list.size() - 1;
534     int matched = list.size();
535
536     while (start <= end)
537     {
538       int mid = (start + end) / 2;
539       if (list.get(mid).end >= pos)
540       {
541         matched = mid;
542         end = mid - 1;
543       }
544       else
545       {
546         start = mid + 1;
547       }
548     }
549     return matched;
550   }
551
552
553 }