JAL-2792 capture feature metadata and render in Feature Details
[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 width=\"10%%\">%s</td><td width=\"50%%\">%s</td><td width=\"40%%\">%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     if (sf.otherDetails != null)
183     {
184       otherDetails = new HashMap<String, Object>();
185       for (Entry<String, Object> entry : sf.otherDetails.entrySet())
186       {
187         otherDetails.put(entry.getKey(), entry.getValue());
188       }
189     }
190     if (sf.links != null && sf.links.size() > 0)
191     {
192       links = new Vector<String>();
193       for (int i = 0, iSize = sf.links.size(); i < iSize; i++)
194       {
195         links.addElement(sf.links.elementAt(i));
196       }
197     }
198   }
199
200   /**
201    * A copy constructor that allows the value of final fields to be 'modified'
202    * 
203    * @param sf
204    * @param newBegin
205    * @param newEnd
206    * @param newGroup
207    * @param newScore
208    */
209   public SequenceFeature(SequenceFeature sf, int newBegin, int newEnd,
210           String newGroup, float newScore)
211   {
212     this(sf, sf.getType(), newBegin, newEnd, newGroup, newScore);
213   }
214
215   /**
216    * Two features are considered equal if they have the same type, group,
217    * description, start, end, phase, strand, and (if present) 'Name', ID' and
218    * 'Parent' attributes.
219    * 
220    * Note we need to check Parent to distinguish the same exon occurring in
221    * different transcripts (in Ensembl GFF). This allows assembly of transcript
222    * sequences from their component exon regions.
223    */
224   @Override
225   public boolean equals(Object o)
226   {
227     return equals(o, false);
228   }
229
230   /**
231    * Overloaded method allows the equality test to optionally ignore the
232    * 'Parent' attribute of a feature. This supports avoiding adding many
233    * superficially duplicate 'exon' or CDS features to genomic or protein
234    * sequence.
235    * 
236    * @param o
237    * @param ignoreParent
238    * @return
239    */
240   public boolean equals(Object o, boolean ignoreParent)
241   {
242     if (o == null || !(o instanceof SequenceFeature))
243     {
244       return false;
245     }
246
247     SequenceFeature sf = (SequenceFeature) o;
248     boolean sameScore = Float.isNaN(score) ? Float.isNaN(sf.score)
249             : score == sf.score;
250     if (begin != sf.begin || end != sf.end || !sameScore)
251     {
252       return false;
253     }
254
255     if (getStrand() != sf.getStrand())
256     {
257       return false;
258     }
259
260     if (!(type + description + featureGroup + getPhase()).equals(
261             sf.type + sf.description + sf.featureGroup + sf.getPhase()))
262     {
263       return false;
264     }
265     if (!equalAttribute(getValue("ID"), sf.getValue("ID")))
266     {
267       return false;
268     }
269     if (!equalAttribute(getValue("Name"), sf.getValue("Name")))
270     {
271       return false;
272     }
273     if (!ignoreParent)
274     {
275       if (!equalAttribute(getValue("Parent"), sf.getValue("Parent")))
276       {
277         return false;
278       }
279     }
280     return true;
281   }
282
283   /**
284    * Returns true if both values are null, are both non-null and equal
285    * 
286    * @param att1
287    * @param att2
288    * @return
289    */
290   protected static boolean equalAttribute(Object att1, Object att2)
291   {
292     if (att1 == null && att2 == null)
293     {
294       return true;
295     }
296     if (att1 != null)
297     {
298       return att1.equals(att2);
299     }
300     return att2.equals(att1);
301   }
302
303   /**
304    * DOCUMENT ME!
305    * 
306    * @return DOCUMENT ME!
307    */
308   @Override
309   public int getBegin()
310   {
311     return begin;
312   }
313
314   /**
315    * DOCUMENT ME!
316    * 
317    * @return DOCUMENT ME!
318    */
319   @Override
320   public int getEnd()
321   {
322     return end;
323   }
324
325   /**
326    * DOCUMENT ME!
327    * 
328    * @return DOCUMENT ME!
329    */
330   public String getType()
331   {
332     return type;
333   }
334
335   /**
336    * DOCUMENT ME!
337    * 
338    * @return DOCUMENT ME!
339    */
340   public String getDescription()
341   {
342     return description;
343   }
344
345   public void setDescription(String desc)
346   {
347     description = desc;
348   }
349
350   public String getFeatureGroup()
351   {
352     return featureGroup;
353   }
354
355   public void addLink(String labelLink)
356   {
357     if (links == null)
358     {
359       links = new Vector<String>();
360     }
361
362     if (!links.contains(labelLink))
363     {
364       links.insertElementAt(labelLink, 0);
365     }
366   }
367
368   public float getScore()
369   {
370     return score;
371   }
372
373   /**
374    * Used for getting values which are not in the basic set. eg STRAND, PHASE
375    * for GFF file
376    * 
377    * @param key
378    *          String
379    */
380   public Object getValue(String key)
381   {
382     if (otherDetails == null)
383     {
384       return null;
385     }
386     else
387     {
388       return otherDetails.get(key);
389     }
390   }
391
392   /**
393    * Returns a property value for the given key if known, else the specified
394    * default value
395    * 
396    * @param key
397    * @param defaultValue
398    * @return
399    */
400   public Object getValue(String key, Object defaultValue)
401   {
402     Object value = getValue(key);
403     return value == null ? defaultValue : value;
404   }
405
406   /**
407    * Used for setting values which are not in the basic set. eg STRAND, FRAME
408    * for GFF file
409    * 
410    * @param key
411    *          eg STRAND
412    * @param value
413    *          eg +
414    */
415   public void setValue(String key, Object value)
416   {
417     if (value != null)
418     {
419       if (otherDetails == null)
420       {
421         otherDetails = new HashMap<String, Object>();
422       }
423
424       otherDetails.put(key, value);
425     }
426   }
427
428   /*
429    * The following methods are added to maintain the castor Uniprot mapping file
430    * for the moment.
431    */
432   public void setStatus(String status)
433   {
434     setValue(STATUS, status);
435   }
436
437   public String getStatus()
438   {
439     return (String) getValue(STATUS);
440   }
441
442   public void setAttributes(String attr)
443   {
444     setValue(ATTRIBUTES, attr);
445   }
446
447   public String getAttributes()
448   {
449     return (String) getValue(ATTRIBUTES);
450   }
451
452   /**
453    * Return 1 for forward strand ('+' in GFF), -1 for reverse strand ('-' in
454    * GFF), and 0 for unknown or not (validly) specified
455    * 
456    * @return
457    */
458   public int getStrand()
459   {
460     int strand = 0;
461     if (otherDetails != null)
462     {
463       Object str = otherDetails.get(STRAND);
464       if ("-".equals(str))
465       {
466         strand = -1;
467       }
468       else if ("+".equals(str))
469       {
470         strand = 1;
471       }
472     }
473     return strand;
474   }
475
476   /**
477    * Set the value of strand
478    * 
479    * @param strand
480    *          should be "+" for forward, or "-" for reverse
481    */
482   public void setStrand(String strand)
483   {
484     setValue(STRAND, strand);
485   }
486
487   public void setPhase(String phase)
488   {
489     setValue(PHASE, phase);
490   }
491
492   public String getPhase()
493   {
494     return (String) getValue(PHASE);
495   }
496
497   /**
498    * Sets the 'raw' ENA format location specifier e.g. join(12..45,89..121)
499    * 
500    * @param loc
501    */
502   public void setEnaLocation(String loc)
503   {
504     setValue(LOCATION, loc);
505   }
506
507   /**
508    * Gets the 'raw' ENA format location specifier e.g. join(12..45,89..121)
509    * 
510    * @param loc
511    */
512   public String getEnaLocation()
513   {
514     return (String) getValue(LOCATION);
515   }
516
517   /**
518    * Readable representation, for debug only, not guaranteed not to change
519    * between versions
520    */
521   @Override
522   public String toString()
523   {
524     return String.format("%d %d %s %s", getBegin(), getEnd(), getType(),
525             getDescription());
526   }
527
528   /**
529    * Overridden to ensure that whenever two objects are equal, they have the
530    * same hashCode
531    */
532   @Override
533   public int hashCode()
534   {
535     String s = getType() + getDescription() + getFeatureGroup()
536             + getValue("ID") + getValue("Name") + getValue("Parent")
537             + getPhase();
538     return s.hashCode() + getBegin() + getEnd() + (int) getScore()
539             + getStrand();
540   }
541
542   /**
543    * Answers true if the feature's start/end values represent two related
544    * positions, rather than ends of a range. Such features may be visualised or
545    * reported differently to features on a range.
546    */
547   @Override
548   public boolean isContactFeature()
549   {
550     return contactFeature;
551   }
552
553   /**
554    * Answers true if the sequence has zero start and end position
555    * 
556    * @return
557    */
558   public boolean isNonPositional()
559   {
560     return begin == 0 && end == 0;
561   }
562
563   /**
564    * Answers an html-formatted report of feature details
565    * 
566    * @return
567    */
568   public String getDetailsReport()
569   {
570     FeatureSourceI metadata = FeatureSources.getInstance()
571             .getSource(source);
572
573     StringBuilder sb = new StringBuilder(128);
574     sb.append("<br>");
575     sb.append("<table>");
576     sb.append(String.format(ROW_DATA, "Type", type, ""));
577     sb.append(String.format(ROW_DATA, "Start/end", begin == end ? begin
578             : begin + (isContactFeature() ? ":" : "-") + end, ""));
579     String desc = StringUtils.stripHtmlTags(description);
580     sb.append(String.format(ROW_DATA, "Description", desc, ""));
581     if (!Float.isNaN(score) && score != 0f)
582     {
583       sb.append(String.format(ROW_DATA, "Score", score, ""));
584     }
585     if (featureGroup != null)
586     {
587       sb.append(String.format(ROW_DATA, "Group", featureGroup, ""));
588     }
589
590     if (otherDetails != null)
591     {
592       TreeMap<String, Object> ordered = new TreeMap<>(
593               String.CASE_INSENSITIVE_ORDER);
594       ordered.putAll(otherDetails);
595
596       for (Entry<String, Object> entry : ordered.entrySet())
597       {
598         String key = entry.getKey();
599         if (ATTRIBUTES.equals(key))
600         {
601           continue; // to avoid double reporting
602         }
603         if (INFO_KEYS.containsKey(key))
604         {
605           /*
606            * split selected INFO data by delimiter over multiple lines
607            */
608           String delimiter = INFO_KEYS.get(key);
609           String[] values = entry.getValue().toString().split(delimiter);
610           for (String value : values)
611           {
612             sb.append(String.format(ROW_DATA, key, "", value));
613           }
614         }
615         else
616         { // tried <td title="key"> but it failed to provide a tooltip :-(
617           String attDesc = null;
618           if (metadata != null)
619           {
620             attDesc = metadata.getAttributeName(key);
621           }
622           String value = entry.getValue().toString();
623           if (isValueInteresting(key, value, metadata))
624           {
625             sb.append(String.format(ROW_DATA, key, attDesc == null ? ""
626                     : attDesc, value));
627           }
628         }
629       }
630     }
631     sb.append("</table>");
632
633     String text = sb.toString();
634     return text;
635   }
636
637   /**
638    * Answers true if we judge the value is worth displaying, by some heuristic
639    * rules, else false
640    * 
641    * @param key
642    * @param value
643    * @param metadata
644    * @return
645    */
646   boolean isValueInteresting(String key, String value,
647           FeatureSourceI metadata)
648   {
649     /*
650      * currently suppressing zero values as well as null or empty
651      */
652     if (value == null || "".equals(value) || ".".equals(value)
653             || "0".equals(value))
654     {
655       return false;
656     }
657
658     if (metadata == null)
659     {
660       return true;
661     }
662
663     FeatureAttributeType attributeType = metadata.getAttributeType(key);
664     if (attributeType == FeatureAttributeType.Float
665             || attributeType.equals(FeatureAttributeType.Integer))
666     {
667       try
668       {
669         float fval = Float.valueOf(value);
670         if (fval == 0f)
671         {
672           return false;
673         }
674       } catch (NumberFormatException e)
675       {
676         // ignore
677       }
678     }
679
680     return true; // default to interesting
681   }
682
683   /**
684    * Sets the feature source identifier
685    * 
686    * @param theSource
687    */
688   public void setSource(String theSource)
689   {
690     source = theSource;
691   }
692 }