Clover icon

Coverage Report

  1. Project Clover database Mon Sep 2 2024 17:57:51 BST
  2. Package jalview.datamodel

File SequenceFeature.java

 

Coverage histogram

../../img/srcFileCovDistChart9.png
13% of files have more coverage

Code metrics

92
152
38
3
766
425
97
0.64
4
12.67
2.55

Classes

Class Line # Actions
SequenceFeature 43 150 95
0.877697887.8%
SFSortByEnd 750 1 1
1.0100%
SFSortByBegin 759 1 1
0.00%
 

Contributing tests

This file is covered by 260 tests. .

Source view

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