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