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