JAL-3253-applet JAL-3397
[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             && equalsWithParent((SequenceFeature) o));
222   }
223
224   /**
225    * BH 2019.09.22 required due to subclassing by CrossRef and used by
226    * intervalstore.nonc.IntervalStoreImpl
227    * 
228    * @param sf
229    * @return
230    */
231   public boolean equalsWithParent(SequenceFeature sf)
232   {
233     return equals(sf, false);
234   }
235
236   /**
237    * Overloaded method allows the equality test to optionally ignore the
238    * 'Parent' attribute of a feature. This supports avoiding adding many
239    * superficially duplicate 'exon' or CDS features to genomic or protein
240    * sequence.
241    * 
242    * @param o
243    * @param ignoreParent
244    * @return
245    */
246   public boolean equals(SequenceFeature sf, boolean ignoreParent)
247   {
248     return (begin == sf.begin && end == sf.end
249             && getStrand() == sf.getStrand()
250             && (Float.isNaN(score) ? Float.isNaN(sf.score)
251                     : score == sf.score)
252             && (type + description + featureGroup + getPhase())
253                     .equals(sf.type + sf.description + sf.featureGroup
254                             + sf.getPhase())
255             && equalAttribute(getValue("ID"), sf.getValue("ID"))
256             && equalAttribute(getValue("Name"), sf.getValue("Name"))
257             && (ignoreParent || equalAttribute(getValue("Parent"),
258                     sf.getValue("Parent"))));
259   }
260
261   /**
262    * Returns true if both values are null, are both non-null and equal
263    * 
264    * @param att1
265    * @param att2
266    * @return
267    */
268   protected static boolean equalAttribute(Object att1, Object att2)
269   {
270     if (att1 == null && att2 == null)
271     {
272       return true;
273     }
274     if (att1 != null)
275     {
276       return att1.equals(att2);
277     }
278     return att2.equals(att1);
279   }
280
281   /**
282    * DOCUMENT ME!
283    * 
284    * @return DOCUMENT ME!
285    */
286   @Override
287   public int getBegin()
288   {
289     return begin;
290   }
291
292   /**
293    * DOCUMENT ME!
294    * 
295    * @return DOCUMENT ME!
296    */
297   @Override
298   public int getEnd()
299   {
300     return end;
301   }
302
303   /**
304    * DOCUMENT ME!
305    * 
306    * @return DOCUMENT ME!
307    */
308   public String getType()
309   {
310     return type;
311   }
312
313   /**
314    * DOCUMENT ME!
315    * 
316    * @return DOCUMENT ME!
317    */
318   public String getDescription()
319   {
320     return description;
321   }
322
323   public void setDescription(String desc)
324   {
325     description = desc;
326   }
327
328   public String getFeatureGroup()
329   {
330     return featureGroup;
331   }
332
333   public void addLink(String labelLink)
334   {
335     if (links == null)
336     {
337       links = new Vector<>();
338     }
339
340     if (!links.contains(labelLink))
341     {
342       links.insertElementAt(labelLink, 0);
343     }
344   }
345
346   public float getScore()
347   {
348     return score;
349   }
350
351   /**
352    * Used for getting values which are not in the basic set. eg STRAND, PHASE
353    * for GFF file
354    * 
355    * @param key
356    *          String
357    */
358   public Object getValue(String key)
359   {
360     if (otherDetails == null)
361     {
362       return null;
363     }
364     else
365     {
366       return otherDetails.get(key);
367     }
368   }
369
370   /**
371    * Answers the value of the specified attribute as string, or null if no such
372    * value. If more than one attribute name is provided, tries to resolve as keys
373    * to nested maps. For example, if attribute "CSQ" holds a map of key-value
374    * pairs, then getValueAsString("CSQ", "Allele") returns the value of "Allele"
375    * in that map.
376    * 
377    * @param key
378    * @return
379    */
380   public String getValueAsString(String... key)
381   {
382     if (otherDetails == null)
383     {
384       return null;
385     }
386     Object value = otherDetails.get(key[0]);
387     if (key.length > 1 && value instanceof Map<?, ?>)
388     {
389       value = ((Map) value).get(key[1]);
390     }
391     return value == null ? null : value.toString();
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<>();
424       }
425
426       otherDetails.put(key, value);
427       recordAttribute(key, value);
428     }
429   }
430
431   /**
432    * Notifies the addition of a feature attribute. This lets us keep track of
433    * which attributes are present on each feature type, and also the range of
434    * numerical-valued attributes.
435    * 
436    * @param key
437    * @param value
438    */
439   protected void recordAttribute(String key, Object value)
440   {
441     String attDesc = null;
442     if (source != null)
443     {
444       attDesc = FeatureSources.getInstance().getSource(source)
445               .getAttributeName(key);
446     }
447
448     FeatureAttributes.getInstance().addAttribute(this.type, attDesc, value,
449             key);
450   }
451
452   /*
453    * The following methods are added to maintain the castor Uniprot mapping file
454    * for the moment.
455    */
456   public void setStatus(String status)
457   {
458     setValue(STATUS, status);
459   }
460
461   public String getStatus()
462   {
463     return (String) getValue(STATUS);
464   }
465
466   public void setAttributes(String attr)
467   {
468     setValue(ATTRIBUTES, attr);
469   }
470
471   public String getAttributes()
472   {
473     return (String) getValue(ATTRIBUTES);
474   }
475
476   /**
477    * Return 1 for forward strand ('+' in GFF), -1 for reverse strand ('-' in
478    * GFF), and 0 for unknown or not (validly) specified
479    * 
480    * @return
481    */
482   public int getStrand()
483   {
484     int strand = 0;
485     if (otherDetails != null)
486     {
487       Object str = otherDetails.get(STRAND);
488       if ("-".equals(str))
489       {
490         strand = -1;
491       }
492       else if ("+".equals(str))
493       {
494         strand = 1;
495       }
496     }
497     return strand;
498   }
499
500   /**
501    * Set the value of strand
502    * 
503    * @param strand
504    *          should be "+" for forward, or "-" for reverse
505    */
506   public void setStrand(String strand)
507   {
508     setValue(STRAND, strand);
509   }
510
511   public void setPhase(String phase)
512   {
513     setValue(PHASE, phase);
514   }
515
516   public String getPhase()
517   {
518     return (String) getValue(PHASE);
519   }
520
521   /**
522    * Sets the 'raw' ENA format location specifier e.g. join(12..45,89..121)
523    * 
524    * @param loc
525    */
526   public void setEnaLocation(String loc)
527   {
528     setValue(LOCATION, loc);
529   }
530
531   /**
532    * Gets the 'raw' ENA format location specifier e.g. join(12..45,89..121)
533    * 
534    * @param loc
535    */
536   public String getEnaLocation()
537   {
538     return (String) getValue(LOCATION);
539   }
540
541   /**
542    * Readable representation, for debug only, not guaranteed not to change
543    * between versions
544    */
545   @Override
546   public String toString()
547   {
548     return String.format("%d %d %s %s", getBegin(), getEnd(), getType(),
549             getDescription());
550   }
551
552   /**
553    * Overridden to ensure that whenever two objects are equal, they have the
554    * same hashCode
555    */
556   @Override
557   public int hashCode()
558   {
559     String s = getType() + getDescription() + getFeatureGroup()
560             + getValue("ID") + getValue("Name") + getValue("Parent")
561             + getPhase();
562     return s.hashCode() + getBegin() + getEnd() + (int) getScore()
563             + getStrand();
564   }
565
566   /**
567    * Answers true if the feature's start/end values represent two related
568    * positions, rather than ends of a range. Such features may be visualised or
569    * reported differently to features on a range.
570    */
571   @Override
572   public boolean isContactFeature()
573   {
574     return contactFeature;
575   }
576
577   /**
578    * Answers true if the sequence has zero start and end position
579    * 
580    * @return
581    */
582   public boolean isNonPositional()
583   {
584     return begin == 0 && end == 0;
585   }
586
587   /**
588    * Answers an html-formatted report of feature details
589    * 
590    * @return
591    */
592   public String getDetailsReport()
593   {
594     FeatureSourceI metadata = FeatureSources.getInstance()
595             .getSource(source);
596
597     StringBuilder sb = new StringBuilder(128);
598     sb.append("<br>");
599     sb.append("<table>");
600     sb.append(String.format(ROW_DATA, "Type", type, ""));
601     sb.append(String.format(ROW_DATA, "Start/end", begin == end ? begin
602             : begin + (isContactFeature() ? ":" : "-") + end, ""));
603     String desc = StringUtils.stripHtmlTags(description);
604     sb.append(String.format(ROW_DATA, "Description", desc, ""));
605     if (!Float.isNaN(score) && score != 0f)
606     {
607       sb.append(String.format(ROW_DATA, "Score", score, ""));
608     }
609     if (featureGroup != null)
610     {
611       sb.append(String.format(ROW_DATA, "Group", featureGroup, ""));
612     }
613
614     if (otherDetails != null)
615     {
616       TreeMap<String, Object> ordered = new TreeMap<>(
617               String.CASE_INSENSITIVE_ORDER);
618       ordered.putAll(otherDetails);
619
620       for (Entry<String, Object> entry : ordered.entrySet())
621       {
622         String key = entry.getKey();
623         if (ATTRIBUTES.equals(key))
624         {
625           continue; // to avoid double reporting
626         }
627
628         Object value = entry.getValue();
629         if (value instanceof Map<?, ?>)
630         {
631           /*
632            * expand values in a Map attribute across separate lines
633            * copy to a TreeMap for alphabetical ordering
634            */
635           Map<String, Object> values = (Map<String, Object>) value;
636           SortedMap<String, Object> sm = new TreeMap<>(
637                   String.CASE_INSENSITIVE_ORDER);
638           sm.putAll(values);
639           for (Entry<?, ?> e : sm.entrySet())
640           {
641             sb.append(String.format(ROW_DATA, key, e.getKey().toString(), e
642                     .getValue().toString()));
643           }
644         }
645         else
646         {
647           // tried <td title="key"> but it failed to provide a tooltip :-(
648           String attDesc = null;
649           if (metadata != null)
650           {
651             attDesc = metadata.getAttributeName(key);
652           }
653           String s = entry.getValue().toString();
654           if (isValueInteresting(key, s, metadata))
655           {
656             sb.append(String.format(ROW_DATA, key, attDesc == null ? ""
657                     : attDesc, s));
658           }
659         }
660       }
661     }
662     sb.append("</table>");
663
664     String text = sb.toString();
665     return text;
666   }
667
668   /**
669    * Answers true if we judge the value is worth displaying, by some heuristic
670    * rules, else false
671    * 
672    * @param key
673    * @param value
674    * @param metadata
675    * @return
676    */
677   boolean isValueInteresting(String key, String value,
678           FeatureSourceI metadata)
679   {
680     /*
681      * currently suppressing zero values as well as null or empty
682      */
683     if (value == null || "".equals(value) || ".".equals(value)
684             || "0".equals(value))
685     {
686       return false;
687     }
688
689     if (metadata == null)
690     {
691       return true;
692     }
693
694     FeatureAttributeType attType = metadata.getAttributeType(key);
695     if (attType != null
696             && (attType == FeatureAttributeType.Float || attType
697                     .equals(FeatureAttributeType.Integer)))
698     {
699       try
700       {
701         float fval = Float.valueOf(value);
702         if (fval == 0f)
703         {
704           return false;
705         }
706       } catch (NumberFormatException e)
707       {
708         // ignore
709       }
710     }
711
712     return true; // default to interesting
713   }
714
715   /**
716    * Sets the feature source identifier
717    * 
718    * @param theSource
719    */
720   public void setSource(String theSource)
721   {
722     source = theSource;
723   }
724
725  
726 }
727
728 class SFSortByEnd implements Comparator<SequenceFeature>
729 {
730   @Override
731   public int compare(SequenceFeature a, SequenceFeature b)
732   {
733     return a.getEnd() - b.getEnd();
734   }
735 }
736
737 class SFSortByBegin implements Comparator<SequenceFeature>
738 {
739   @Override
740   public int compare(SequenceFeature a, SequenceFeature b)
741   {
742     return a.getBegin() - b.getBegin();
743   }
744 }