Merge branch 'Jalview-JS/JAL-3253-applet' of https://source.jalview.org/git/jalview...
[jalview.git] / src / jalview / datamodel / SequenceFeature.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;
22
23 import jalview.datamodel.features.FeatureAttributeType;
24 import jalview.datamodel.features.FeatureAttributes;
25 import jalview.datamodel.features.FeatureLocationI;
26 import jalview.datamodel.features.FeatureSourceI;
27 import jalview.datamodel.features.FeatureSources;
28 import jalview.util.StringUtils;
29
30 import java.util.Comparator;
31 import java.util.HashMap;
32 import java.util.Map;
33 import java.util.Map.Entry;
34 import java.util.SortedMap;
35 import java.util.TreeMap;
36 import java.util.Vector;
37
38 /**
39  * A class that models a single contiguous feature on a sequence. If flag
40  * 'contactFeature' is true, the start and end positions are interpreted instead
41  * as two contact points.
42  */
43 public class SequenceFeature implements FeatureLocationI
44 {
45   /*
46    * score value if none is set; preferably Float.Nan, but see
47    * JAL-2060 and JAL-2554 for a couple of blockers to that
48    */
49   private static final float NO_SCORE = 0f;
50
51   private static final String STATUS = "status";
52
53   private static final String STRAND = "STRAND";
54
55   // private key for Phase designed not to conflict with real GFF data
56   private static final String PHASE = "!Phase";
57
58   // private key for ENA location designed not to conflict with real GFF data
59   private static final String LOCATION = "!Location";
60
61   private static final String ROW_DATA = "<tr><td>%s</td><td>%s</td><td>%s</td></tr>";
62
63   /*
64    * ATTRIBUTES is reserved for the GFF 'column 9' data, formatted as
65    * name1=value1;name2=value2,value3;...etc
66    */
67   private static final String ATTRIBUTES = "ATTRIBUTES";
68
69   /*
70    * type, begin, end, featureGroup, score and contactFeature are final 
71    * to ensure that the integrity of SequenceFeatures data store 
72    * can't be broken by direct update of these fields
73    */
74   public final String type;
75
76   public final int begin;
77
78   public final int end;
79
80   public final String featureGroup;
81
82   public final float score;
83
84   private final boolean contactFeature;
85
86   public String description;
87
88   /*
89    * a map of key-value pairs; may be populated from GFF 'column 9' data,
90    * other data sources (e.g. GenBank file), or programmatically
91    */
92   public Map<String, Object> otherDetails;
93
94   public Vector<String> links;
95
96   /*
97    * the identifier (if known) for the FeatureSource held in FeatureSources,
98    * as a provider of metadata about feature attributes 
99    */
100   private String source;
101
102   /**
103    * Constructs a duplicate feature. Note: Uses makes a shallow copy of the
104    * otherDetails map, so the new and original SequenceFeature may reference the
105    * same objects in the map.
106    * 
107    * @param cpy
108    */
109   public SequenceFeature(SequenceFeature cpy)
110   {
111     this(cpy, cpy.getBegin(), cpy.getEnd(), cpy.getFeatureGroup(), cpy
112             .getScore());
113   }
114
115   /**
116    * Constructor
117    * 
118    * @param theType
119    * @param theDesc
120    * @param theBegin
121    * @param theEnd
122    * @param group
123    */
124   public SequenceFeature(String theType, String theDesc, int theBegin,
125           int theEnd, String group)
126   {
127     this(theType, theDesc, theBegin, theEnd, NO_SCORE, group);
128   }
129
130   /**
131    * Constructor including a score value
132    * 
133    * @param theType
134    * @param theDesc
135    * @param theBegin
136    * @param theEnd
137    * @param theScore
138    * @param group
139    */
140   public SequenceFeature(String theType, String theDesc, int theBegin,
141           int theEnd, float theScore, String group)
142   {
143     this.type = theType;
144     this.description = theDesc;
145     this.begin = theBegin;
146     this.end = theEnd;
147     this.featureGroup = group;
148     this.score = theScore;
149
150     /*
151      * for now, only "Disulfide/disulphide bond" is treated as a contact feature
152      */
153     this.contactFeature = "disulfide bond".equalsIgnoreCase(type)
154             || "disulphide bond".equalsIgnoreCase(type);
155   }
156
157   /**
158    * A copy constructor that allows the value of final fields to be 'modified'
159    * 
160    * @param sf
161    * @param newType
162    * @param newBegin
163    * @param newEnd
164    * @param newGroup
165    * @param newScore
166    */
167   public SequenceFeature(SequenceFeature sf, String newType, int newBegin,
168           int newEnd, String newGroup, float newScore)
169   {
170     this(newType, sf.getDescription(), newBegin, newEnd, newScore,
171             newGroup);
172
173     this.source = sf.source;
174
175     if (sf.otherDetails != null)
176     {
177       otherDetails = new HashMap<>();
178       for (Entry<String, Object> entry : sf.otherDetails.entrySet())
179       {
180         otherDetails.put(entry.getKey(), entry.getValue());
181       }
182     }
183     if (sf.links != null && sf.links.size() > 0)
184     {
185       links = new Vector<>();
186       for (int i = 0, iSize = sf.links.size(); i < iSize; i++)
187       {
188         links.addElement(sf.links.elementAt(i));
189       }
190     }
191   }
192
193   /**
194    * A copy constructor that allows the value of final fields to be 'modified'
195    * 
196    * @param sf
197    * @param newBegin
198    * @param newEnd
199    * @param newGroup
200    * @param newScore
201    */
202   public SequenceFeature(SequenceFeature sf, int newBegin, int newEnd,
203           String newGroup, float newScore)
204   {
205     this(sf, sf.getType(), newBegin, newEnd, newGroup, newScore);
206   }
207
208   /**
209    * Two features are considered equal if they have the same type, group,
210    * description, start, end, phase, strand, and (if present) 'Name', ID' and
211    * 'Parent' attributes.
212    * 
213    * Note we need to check Parent to distinguish the same exon occurring in
214    * different transcripts (in Ensembl GFF). This allows assembly of transcript
215    * sequences from their component exon regions.
216    */
217   @Override
218   public boolean equals(Object o)
219   {
220     return (o instanceof SequenceFeature
221             && equals((SequenceFeature) o, false));
222   }
223
224   /**
225    * Overloaded method allows the equality test to optionally ignore the
226    * 'Parent' attribute of a feature. This supports avoiding adding many
227    * superficially duplicate 'exon' or CDS features to genomic or protein
228    * sequence.
229    * 
230    * @param o
231    * @param ignoreParent
232    * @return
233    */
234   public boolean equals(SequenceFeature sf, boolean ignoreParent)
235   {
236     return (begin == sf.begin && end == sf.end
237             && getStrand() == sf.getStrand()
238             && (Float.isNaN(score) ? Float.isNaN(sf.score)
239                     : score == sf.score)
240             && (type + description + featureGroup + getPhase())
241                     .equals(sf.type + sf.description + sf.featureGroup
242                             + sf.getPhase())
243             && equalAttribute(getValue("ID"), sf.getValue("ID"))
244             && equalAttribute(getValue("Name"), sf.getValue("Name"))
245             && (ignoreParent || equalAttribute(getValue("Parent"),
246                     sf.getValue("Parent"))));
247   }
248
249   /**
250    * Returns true if both values are null, are both non-null and equal
251    * 
252    * @param att1
253    * @param att2
254    * @return
255    */
256   protected static boolean equalAttribute(Object att1, Object att2)
257   {
258     if (att1 == null && att2 == null)
259     {
260       return true;
261     }
262     if (att1 != null)
263     {
264       return att1.equals(att2);
265     }
266     return att2.equals(att1);
267   }
268
269   /**
270    * DOCUMENT ME!
271    * 
272    * @return DOCUMENT ME!
273    */
274   @Override
275   public int getBegin()
276   {
277     return begin;
278   }
279
280   /**
281    * DOCUMENT ME!
282    * 
283    * @return DOCUMENT ME!
284    */
285   @Override
286   public int getEnd()
287   {
288     return end;
289   }
290
291   /**
292    * DOCUMENT ME!
293    * 
294    * @return DOCUMENT ME!
295    */
296   public String getType()
297   {
298     return type;
299   }
300
301   /**
302    * DOCUMENT ME!
303    * 
304    * @return DOCUMENT ME!
305    */
306   public String getDescription()
307   {
308     return description;
309   }
310
311   public void setDescription(String desc)
312   {
313     description = desc;
314   }
315
316   public String getFeatureGroup()
317   {
318     return featureGroup;
319   }
320
321   public void addLink(String labelLink)
322   {
323     if (links == null)
324     {
325       links = new Vector<>();
326     }
327
328     if (!links.contains(labelLink))
329     {
330       links.insertElementAt(labelLink, 0);
331     }
332   }
333
334   public float getScore()
335   {
336     return score;
337   }
338
339   /**
340    * Used for getting values which are not in the basic set. eg STRAND, PHASE
341    * for GFF file
342    * 
343    * @param key
344    *          String
345    */
346   public Object getValue(String key)
347   {
348     if (otherDetails == null)
349     {
350       return null;
351     }
352     else
353     {
354       return otherDetails.get(key);
355     }
356   }
357
358   /**
359    * Answers the value of the specified attribute as string, or null if no such
360    * value. If more than one attribute name is provided, tries to resolve as keys
361    * to nested maps. For example, if attribute "CSQ" holds a map of key-value
362    * pairs, then getValueAsString("CSQ", "Allele") returns the value of "Allele"
363    * in that map.
364    * 
365    * @param key
366    * @return
367    */
368   public String getValueAsString(String... key)
369   {
370     if (otherDetails == null)
371     {
372       return null;
373     }
374     Object value = otherDetails.get(key[0]);
375     if (key.length > 1 && value instanceof Map<?, ?>)
376     {
377       value = ((Map) value).get(key[1]);
378     }
379     return value == null ? null : value.toString();
380   }
381
382   /**
383    * Returns a property value for the given key if known, else the specified
384    * default value
385    * 
386    * @param key
387    * @param defaultValue
388    * @return
389    */
390   public Object getValue(String key, Object defaultValue)
391   {
392     Object value = getValue(key);
393     return value == null ? defaultValue : value;
394   }
395
396   /**
397    * Used for setting values which are not in the basic set. eg STRAND, FRAME
398    * for GFF file
399    * 
400    * @param key
401    *          eg STRAND
402    * @param value
403    *          eg +
404    */
405   public void setValue(String key, Object value)
406   {
407     if (value != null)
408     {
409       if (otherDetails == null)
410       {
411         otherDetails = new HashMap<>();
412       }
413
414       otherDetails.put(key, value);
415       recordAttribute(key, value);
416     }
417   }
418
419   /**
420    * Notifies the addition of a feature attribute. This lets us keep track of
421    * which attributes are present on each feature type, and also the range of
422    * numerical-valued attributes.
423    * 
424    * @param key
425    * @param value
426    */
427   protected void recordAttribute(String key, Object value)
428   {
429     String attDesc = null;
430     if (source != null)
431     {
432       attDesc = FeatureSources.getInstance().getSource(source)
433               .getAttributeName(key);
434     }
435
436     FeatureAttributes.getInstance().addAttribute(this.type, attDesc, value,
437             key);
438   }
439
440   /*
441    * The following methods are added to maintain the castor Uniprot mapping file
442    * for the moment.
443    */
444   public void setStatus(String status)
445   {
446     setValue(STATUS, status);
447   }
448
449   public String getStatus()
450   {
451     return (String) getValue(STATUS);
452   }
453
454   public void setAttributes(String attr)
455   {
456     setValue(ATTRIBUTES, attr);
457   }
458
459   public String getAttributes()
460   {
461     return (String) getValue(ATTRIBUTES);
462   }
463
464   /**
465    * Return 1 for forward strand ('+' in GFF), -1 for reverse strand ('-' in
466    * GFF), and 0 for unknown or not (validly) specified
467    * 
468    * @return
469    */
470   public int getStrand()
471   {
472     int strand = 0;
473     if (otherDetails != null)
474     {
475       Object str = otherDetails.get(STRAND);
476       if ("-".equals(str))
477       {
478         strand = -1;
479       }
480       else if ("+".equals(str))
481       {
482         strand = 1;
483       }
484     }
485     return strand;
486   }
487
488   /**
489    * Set the value of strand
490    * 
491    * @param strand
492    *          should be "+" for forward, or "-" for reverse
493    */
494   public void setStrand(String strand)
495   {
496     setValue(STRAND, strand);
497   }
498
499   public void setPhase(String phase)
500   {
501     setValue(PHASE, phase);
502   }
503
504   public String getPhase()
505   {
506     return (String) getValue(PHASE);
507   }
508
509   /**
510    * Sets the 'raw' ENA format location specifier e.g. join(12..45,89..121)
511    * 
512    * @param loc
513    */
514   public void setEnaLocation(String loc)
515   {
516     setValue(LOCATION, loc);
517   }
518
519   /**
520    * Gets the 'raw' ENA format location specifier e.g. join(12..45,89..121)
521    * 
522    * @param loc
523    */
524   public String getEnaLocation()
525   {
526     return (String) getValue(LOCATION);
527   }
528
529   /**
530    * Readable representation, for debug only, not guaranteed not to change
531    * between versions
532    */
533   @Override
534   public String toString()
535   {
536     return String.format("%d %d %s %s", getBegin(), getEnd(), getType(),
537             getDescription());
538   }
539
540   /**
541    * Overridden to ensure that whenever two objects are equal, they have the
542    * same hashCode
543    */
544   @Override
545   public int hashCode()
546   {
547     String s = getType() + getDescription() + getFeatureGroup()
548             + getValue("ID") + getValue("Name") + getValue("Parent")
549             + getPhase();
550     return s.hashCode() + getBegin() + getEnd() + (int) getScore()
551             + getStrand();
552   }
553
554   /**
555    * Answers true if the feature's start/end values represent two related
556    * positions, rather than ends of a range. Such features may be visualised or
557    * reported differently to features on a range.
558    */
559   @Override
560   public boolean isContactFeature()
561   {
562     return contactFeature;
563   }
564
565   /**
566    * Answers true if the sequence has zero start and end position
567    * 
568    * @return
569    */
570   public boolean isNonPositional()
571   {
572     return begin == 0 && end == 0;
573   }
574
575   /**
576    * Answers an html-formatted report of feature details
577    * 
578    * @return
579    */
580   public String getDetailsReport()
581   {
582     FeatureSourceI metadata = FeatureSources.getInstance()
583             .getSource(source);
584
585     StringBuilder sb = new StringBuilder(128);
586     sb.append("<br>");
587     sb.append("<table>");
588     sb.append(String.format(ROW_DATA, "Type", type, ""));
589     sb.append(String.format(ROW_DATA, "Start/end", begin == end ? begin
590             : begin + (isContactFeature() ? ":" : "-") + end, ""));
591     String desc = StringUtils.stripHtmlTags(description);
592     sb.append(String.format(ROW_DATA, "Description", desc, ""));
593     if (!Float.isNaN(score) && score != 0f)
594     {
595       sb.append(String.format(ROW_DATA, "Score", score, ""));
596     }
597     if (featureGroup != null)
598     {
599       sb.append(String.format(ROW_DATA, "Group", featureGroup, ""));
600     }
601
602     if (otherDetails != null)
603     {
604       TreeMap<String, Object> ordered = new TreeMap<>(
605               String.CASE_INSENSITIVE_ORDER);
606       ordered.putAll(otherDetails);
607
608       for (Entry<String, Object> entry : ordered.entrySet())
609       {
610         String key = entry.getKey();
611         if (ATTRIBUTES.equals(key))
612         {
613           continue; // to avoid double reporting
614         }
615
616         Object value = entry.getValue();
617         if (value instanceof Map<?, ?>)
618         {
619           /*
620            * expand values in a Map attribute across separate lines
621            * copy to a TreeMap for alphabetical ordering
622            */
623           Map<String, Object> values = (Map<String, Object>) value;
624           SortedMap<String, Object> sm = new TreeMap<>(
625                   String.CASE_INSENSITIVE_ORDER);
626           sm.putAll(values);
627           for (Entry<?, ?> e : sm.entrySet())
628           {
629             sb.append(String.format(ROW_DATA, key, e.getKey().toString(), e
630                     .getValue().toString()));
631           }
632         }
633         else
634         {
635           // tried <td title="key"> but it failed to provide a tooltip :-(
636           String attDesc = null;
637           if (metadata != null)
638           {
639             attDesc = metadata.getAttributeName(key);
640           }
641           String s = entry.getValue().toString();
642           if (isValueInteresting(key, s, metadata))
643           {
644             sb.append(String.format(ROW_DATA, key, attDesc == null ? ""
645                     : attDesc, s));
646           }
647         }
648       }
649     }
650     sb.append("</table>");
651
652     String text = sb.toString();
653     return text;
654   }
655
656   /**
657    * Answers true if we judge the value is worth displaying, by some heuristic
658    * rules, else false
659    * 
660    * @param key
661    * @param value
662    * @param metadata
663    * @return
664    */
665   boolean isValueInteresting(String key, String value,
666           FeatureSourceI metadata)
667   {
668     /*
669      * currently suppressing zero values as well as null or empty
670      */
671     if (value == null || "".equals(value) || ".".equals(value)
672             || "0".equals(value))
673     {
674       return false;
675     }
676
677     if (metadata == null)
678     {
679       return true;
680     }
681
682     FeatureAttributeType attType = metadata.getAttributeType(key);
683     if (attType != null
684             && (attType == FeatureAttributeType.Float || attType
685                     .equals(FeatureAttributeType.Integer)))
686     {
687       try
688       {
689         float fval = Float.valueOf(value);
690         if (fval == 0f)
691         {
692           return false;
693         }
694       } catch (NumberFormatException e)
695       {
696         // ignore
697       }
698     }
699
700     return true; // default to interesting
701   }
702
703   /**
704    * Sets the feature source identifier
705    * 
706    * @param theSource
707    */
708   public void setSource(String theSource)
709   {
710     source = theSource;
711   }
712
713  
714 }
715
716 class SFSortByEnd implements Comparator<SequenceFeature>
717 {
718   @Override
719   public int compare(SequenceFeature a, SequenceFeature b)
720   {
721     return a.getEnd() - b.getEnd();
722   }
723 }
724
725 class SFSortByBegin implements Comparator<SequenceFeature>
726 {
727   @Override
728   public int compare(SequenceFeature a, SequenceFeature b)
729   {
730     return a.getBegin() - b.getBegin();
731   }
732 }