Clover icon

Coverage Report

  1. Project Clover database Thu Dec 4 2025 16:11:35 GMT
  2. Package jalview.io

File SequenceAnnotationReport.java

 

Coverage histogram

../../img/srcFileCovDistChart8.png
20% of files have more coverage

Code metrics

128
215
13
1
689
463
100
0.47
16.54
13
7.69

Classes

Class Line # Actions
SequenceAnnotationReport 50 215 100
0.7359550673.6%
 

Contributing tests

This file is covered by 202 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.io;
22   
23    import java.util.ArrayList;
24   
25    import java.util.Collection;
26    import java.util.Comparator;
27    import java.util.LinkedHashMap;
28    import java.util.List;
29    import java.util.Locale;
30    import java.util.Map;
31   
32    import jalview.api.FeatureColourI;
33    import jalview.datamodel.AlignmentAnnotation;
34    import jalview.datamodel.DBRefEntry;
35    import jalview.datamodel.DBRefSource;
36    import jalview.datamodel.GeneLociI;
37    import jalview.datamodel.MappedFeatures;
38    import jalview.datamodel.SequenceFeature;
39    import jalview.datamodel.SequenceI;
40    import jalview.util.MessageManager;
41    import jalview.util.StringUtils;
42    import jalview.util.UrlLink;
43    import jalview.viewmodel.seqfeatures.FeatureRendererModel;
44   
45    /**
46    * generate HTML reports for a sequence
47    *
48    * @author jimp
49    */
 
50    public class SequenceAnnotationReport
51    {
52    private static final int MAX_DESCRIPTION_LENGTH = 40;
53   
54    private static final String COMMA = ",";
55   
56    private static final String ELLIPSIS = "...";
57   
58    private static final int MAX_REFS_PER_SOURCE = 4;
59   
60    private static final int MAX_SOURCES = 5;
61   
62    private static String linkImageURL;
63   
64    // public static final String[][] PRIMARY_SOURCES moved to DBRefSource.java
65   
66    /*
67    * Comparator to order DBRefEntry by Source + accession id (case-insensitive),
68    * with 'Primary' sources placed before others, and 'chromosome' first of all
69    */
70    private static Comparator<DBRefEntry> comparator = new Comparator<DBRefEntry>()
71    {
72   
 
73  24 toggle @Override
74    public int compare(DBRefEntry ref1, DBRefEntry ref2)
75    {
76  24 if (ref1 instanceof GeneLociI)
77    {
78  0 return -1;
79    }
80  24 if (ref2 instanceof GeneLociI)
81    {
82  0 return 1;
83    }
84   
85  24 String s1 = ref1.getSource();
86  24 String s2 = ref2.getSource();
87  24 boolean s1Primary = DBRefSource.isPrimarySource(s1);
88  24 boolean s2Primary = DBRefSource.isPrimarySource(s2);
89  24 if (ref1.isCanonical() && !ref2.isCanonical())
90    {
91  0 return -1;
92    }
93  24 if (!ref1.isCanonical() && ref2.isCanonical())
94    {
95  0 return 1;
96    }
97  24 if (s1Primary && !s2Primary)
98    {
99  14 return -1;
100    }
101  10 if (!s1Primary && s2Primary)
102    {
103  0 return 1;
104    }
105  10 int comp = s1 == null ? -1 : (s2 == null ? 1 : s1
106    .compareToIgnoreCase(s2));
107  10 if (comp == 0)
108    {
109  5 String a1 = ref1.getAccessionId();
110  5 String a2 = ref2.getAccessionId();
111  5 comp = a1 == null ? -1 : (a2 == null ? 1 : a1
112    .compareToIgnoreCase(a2));
113    }
114  10 return comp;
115    }
116   
117    // private boolean isPrimarySource(String source)
118    // {
119    // for (String[] primary : DBRefSource.PRIMARY_SOURCES)
120    // {
121    // for (String s : primary)
122    // {
123    // if (source.equals(s))
124    // {
125    // return true;
126    // }
127    // }
128    // }
129    // return false;
130    // }
131    };
132   
133    private boolean forTooltip;
134   
135    /**
136    * Constructor given a flag which affects behaviour
137    * <ul>
138    * <li>if true, generates feature details suitable to show in a tooltip</li>
139    * <li>if false, generates feature details in a form suitable for the sequence
140    * details report</li>
141    * </ul>
142    *
143    * @param isForTooltip
144    */
 
145  1038 toggle public SequenceAnnotationReport(boolean isForTooltip)
146    {
147  1038 this.forTooltip = isForTooltip;
148  1038 if (linkImageURL == null)
149    {
150  54 linkImageURL = getClass().getResource("/images/link.gif").toString();
151    }
152    }
153   
154    /**
155    * Append text for the list of features to the tooltip. Returns the number of
156    * features not added if maxlength limit is (or would have been) reached.
157    *
158    * @param sb
159    * @param residuePos
160    * @param features
161    * @param minmax
162    * @param maxlength
163    */
 
164  1 toggle public int appendFeatures(final StringBuilder sb,
165    int residuePos, List<SequenceFeature> features,
166    FeatureRendererModel fr, int maxlength)
167    {
168  4 for (int i = 0; i < features.size(); i++)
169    {
170  4 SequenceFeature feature = features.get(i);
171  4 if (appendFeature(sb, residuePos, fr, feature, null, maxlength))
172    {
173  1 return features.size() - i;
174    }
175    }
176  0 return 0;
177    }
178   
179    /**
180    * Appends text for mapped features (e.g. CDS feature for peptide or vice
181    * versa) Returns number of features left if maxlength limit is (or would have
182    * been) reached.
183    *
184    * @param sb
185    * @param residuePos
186    * @param mf
187    * @param fr
188    * @param maxlength
189    */
 
190  0 toggle public int appendFeatures(StringBuilder sb, int residuePos,
191    MappedFeatures mf, FeatureRendererModel fr, int maxlength)
192    {
193  0 for (int i = 0; i < mf.features.size(); i++)
194    {
195  0 SequenceFeature feature = mf.features.get(i);
196  0 if (appendFeature(sb, residuePos, fr, feature, mf, maxlength))
197    {
198  0 return mf.features.size() - i;
199    }
200    }
201  0 return 0;
202    }
203   
204    /**
205    * Appends the feature at rpos to the given buffer
206    *
207    * @param sb
208    * @param rpos
209    * @param minmax
210    * @param feature
211    */
 
212  41 toggle boolean appendFeature(final StringBuilder sb0, int rpos,
213    FeatureRendererModel fr, SequenceFeature feature,
214    MappedFeatures mf, int maxlength)
215    {
216  41 int begin = feature.getBegin();
217  41 int end = feature.getEnd();
218   
219    /*
220    * if this is a virtual features, convert begin/end to the
221    * coordinates of the sequence it is mapped to
222    */
223  41 int[] beginRange = null; // feature start in local coordinates
224  41 int[] endRange = null; // feature end in local coordinates
225  41 if (mf != null)
226    {
227  3 if (feature.isContactFeature())
228    {
229    /*
230    * map start and end points individually
231    */
232  0 beginRange = mf.getMappedPositions(begin, begin);
233  0 endRange = begin == end ? beginRange
234    : mf.getMappedPositions(end, end);
235    }
236    else
237    {
238    /*
239    * map the feature extent
240    */
241  3 beginRange = mf.getMappedPositions(begin, end);
242  3 endRange = beginRange;
243    }
244  3 if (beginRange == null || endRange == null)
245    {
246    // something went wrong
247  0 return false;
248    }
249  3 begin = beginRange[0];
250  3 end = endRange[endRange.length - 1];
251    }
252   
253  41 StringBuilder sb = new StringBuilder();
254  41 if (feature.isContactFeature())
255    {
256    /*
257    * include if rpos is at start or end position of [mapped] feature
258    */
259  3 boolean showContact = (mf == null) && (rpos == begin || rpos == end);
260  3 boolean showMappedContact = (mf != null) && ((rpos >= beginRange[0]
261    && rpos <= beginRange[beginRange.length - 1])
262    || (rpos >= endRange[0]
263    && rpos <= endRange[endRange.length - 1]));
264  3 if (showContact || showMappedContact)
265    {
266  2 if (sb0.length() > 6)
267    {
268  1 sb.append("<br>");
269    }
270  2 sb.append(feature.getType()).append(" ").append(begin).append(":")
271    .append(end);
272    }
273  3 return appendText(sb0, sb, maxlength);
274    }
275   
276  38 if (sb0.length() > 6)
277    {
278  21 sb.append("<br>");
279    }
280    // TODO: remove this hack to display link only features
281  38 boolean linkOnly = feature.getValue("linkonly") != null;
282  38 if (!linkOnly)
283    {
284  38 sb.append(feature.getType()).append(" ");
285  38 if (rpos != 0)
286    {
287    // we are marking a positional feature
288  21 sb.append(begin);
289  21 if (begin != end)
290    {
291  19 sb.append(" ").append(end);
292    }
293    }
294   
295  38 String description = feature.getDescription();
296  38 if (description != null && !description.equals(feature.getType()))
297    {
298  37 description = StringUtils.stripHtmlTags(description);
299   
300    /*
301    * truncate overlong descriptions unless they contain an href
302    * before the truncation point (as truncation could leave corrupted html)
303    */
304  37 int linkindex = description.toLowerCase(Locale.ROOT).indexOf("<a ");
305  37 boolean hasLink = linkindex > -1
306    && linkindex < MAX_DESCRIPTION_LENGTH;
307  37 if (
308    // BH suggestion maxlength == 0 &&
309  37 description.length() > MAX_DESCRIPTION_LENGTH && !hasLink)
310    {
311  6 description = description.substring(0, MAX_DESCRIPTION_LENGTH)
312    + ELLIPSIS;
313    }
314   
315  37 sb.append("; ").append(description);
316    }
317   
318  38 if (showScore(feature, fr))
319    {
320  5 sb.append(" Score=").append(String.valueOf(feature.getScore()));
321    }
322  38 String status = (String) feature.getValue("status");
323  38 if (status != null && status.length() > 0)
324    {
325  2 sb.append("; (").append(status).append(")");
326    }
327   
328    /*
329    * add attribute value if coloured by attribute
330    */
331  38 if (fr != null)
332    {
333  27 FeatureColourI fc = fr.getFeatureColours().get(feature.getType());
334  27 if (fc != null && fc.isColourByAttribute())
335    {
336  5 String[] attName = fc.getAttributeName();
337  5 String attVal = feature.getValueAsString(attName);
338  5 if (attVal != null)
339    {
340  4 sb.append("; ").append(String.join(":", attName)).append("=")
341    .append(attVal);
342    }
343    }
344    }
345   
346  38 if (mf != null)
347    {
348  3 String variants = mf.findProteinVariants(feature);
349  3 if (!variants.isEmpty())
350    {
351  1 sb.append(" ").append(variants);
352    }
353    }
354    }
355  38 return appendText(sb0, sb, maxlength);
356    }
357   
358    /**
359    * Appends sb to sb0, and returns false, unless maxlength is not zero and
360    * appending would make the result longer than or equal to maxlength, in which
361    * case the append is not done and returns true
362    *
363    * @param sb0
364    * @param sb
365    * @param maxlength
366    * @return
367    */
 
368  41 toggle private static boolean appendText(StringBuilder sb0, StringBuilder sb,
369    int maxlength)
370    {
371  41 if (maxlength == 0 || sb0.length() + sb.length() < maxlength)
372    {
373  40 sb0.append(sb);
374  40 return false;
375    }
376  1 return true;
377    }
378   
379    /**
380    * Answers true if score should be shown, else false. Score is shown if it is
381    * not NaN, and the feature type has a non-trivial min-max score range
382    */
 
383  38 toggle boolean showScore(SequenceFeature feature, FeatureRendererModel fr)
384    {
385  38 if (Float.isNaN(feature.getScore()))
386    {
387  11 return false;
388    }
389  27 if (fr == null)
390    {
391  3 return true;
392    }
393  24 float[][] minMax = fr.getMinMax().get(feature.getType());
394   
395    /*
396    * minMax[0] is the [min, max] score range for positional features
397    */
398  24 if (minMax == null || minMax[0] == null || minMax[0][0] == minMax[0][1])
399    {
400  22 return false;
401    }
402  2 return true;
403    }
404   
405    /**
406    * Format and appends any hyperlinks for the sequence feature to the string
407    * buffer
408    *
409    * @param sb
410    * @param feature
411    */
 
412  0 toggle void appendLinks(final StringBuffer sb, SequenceFeature feature)
413    {
414  0 if (feature.links != null)
415    {
416  0 if (linkImageURL != null)
417    {
418  0 sb.append(" <img src=\"" + linkImageURL + "\">");
419    }
420    else
421    {
422  0 for (String urlstring : feature.links)
423    {
424  0 try
425    {
426  0 for (List<String> urllink : createLinksFrom(null, urlstring))
427    {
428  0 sb.append("<br> <a href=\""
429    + urllink.get(3)
430    + "\" target=\""
431    + urllink.get(0)
432    + "\">"
433  0 + (urllink.get(0).toLowerCase(Locale.ROOT)
434    .equals(urllink.get(1).toLowerCase(Locale.ROOT)) ? urllink
435    .get(0) : (urllink.get(0) + ":" + urllink
436    .get(1)))
437    + "</a><br>");
438    }
439    } catch (Exception x)
440    {
441  0 jalview.bin.Console.errPrintln("problem when creating links from "
442    + urlstring);
443  0 x.printStackTrace();
444    }
445    }
446    }
447   
448    }
449    }
450   
451    /**
452    *
453    * @param seq
454    * @param link
455    * @return Collection< List<String> > { List<String> { link target, link
456    * label, dynamic component inserted (if any), url }}
457    */
 
458  0 toggle Collection<List<String>> createLinksFrom(SequenceI seq, String link)
459    {
460  0 Map<String, List<String>> urlSets = new LinkedHashMap<>();
461  0 UrlLink urlLink = new UrlLink(link);
462  0 if (!urlLink.isValid())
463    {
464  0 jalview.bin.Console.errPrintln(urlLink.getInvalidMessage());
465  0 return null;
466    }
467   
468  0 urlLink.createLinksFromSeq(seq, urlSets);
469   
470  0 return urlSets.values();
471    }
472   
 
473  10 toggle public void createSequenceAnnotationReport(final StringBuilder tip,
474    SequenceI sequence, boolean showDbRefs, boolean showNpFeats,
475    FeatureRendererModel fr)
476    {
477  10 createSequenceAnnotationReport(tip, sequence, showDbRefs, showNpFeats,
478    fr, false);
479    }
480   
481    /**
482    * Builds an html formatted report of sequence details and appends it to the
483    * provided buffer.
484    *
485    * @param sb
486    * buffer to append report to
487    * @param sequence
488    * the sequence the report is for
489    * @param showDbRefs
490    * whether to include database references for the sequence
491    * @param showNpFeats
492    * whether to include non-positional sequence features
493    * @param fr
494    * @param summary
495    * @return
496    */
 
497  11 toggle int createSequenceAnnotationReport(final StringBuilder sb,
498    SequenceI sequence, boolean showDbRefs, boolean showNpFeats,
499    FeatureRendererModel fr, boolean summary)
500    {
501  11 String tmp;
502  11 sb.append("<i>");
503   
504  11 int maxWidth = 0;
505  11 if (sequence.getDescription() != null)
506    {
507  10 tmp = sequence.getDescription();
508  10 sb.append(tmp);
509  10 maxWidth = Math.max(maxWidth, tmp.length());
510    }
511  11 sb.append("\n");
512  11 SequenceI ds = sequence;
513  11 while (ds.getDatasetSequence() != null)
514    {
515  0 ds = ds.getDatasetSequence();
516    }
517   
518   
519    /*
520    * add any annotation scores
521    */
522  11 AlignmentAnnotation[] anns = ds.getAnnotation();
523  11 if (anns!=null && anns.length>0) {
524  0 boolean first=true;
525  0 for (int i = 0; anns != null && i < anns.length; i++)
526    {
527  0 AlignmentAnnotation aa = anns[i];
528  0 if (aa != null && aa.hasScore() && aa.sequenceRef != null)
529    {
530  0 if (first) {
531  0 sb.append("<br>").append("Annotation Scores<br>");
532  0 first=false;
533    }
534  0 sb.append("<br>").append(aa.label).append(": ")
535    .append(aa.getScore());
536    }
537    }
538    }
539  11 if (showDbRefs)
540    {
541  9 maxWidth = Math.max(maxWidth, appendDbRefs(sb, ds, summary));
542    }
543  11 sb.append("\n");
544   
545    /*
546    * add non-positional features if wanted
547    */
548  11 if (showNpFeats)
549    {
550  9 for (SequenceFeature sf : sequence.getFeatures()
551    .getNonPositionalFeatures())
552    {
553  17 int sz = -sb.length();
554  17 appendFeature(sb, 0, fr, sf, null, 0);
555  17 sz += sb.length();
556  17 maxWidth = Math.max(maxWidth, sz);
557    }
558    }
559  11 if (sequence.getAnnotation("Search Scores") != null)
560    {
561  0 sb.append("<br>");
562  0 String eValue = " E-Value: "
563    + sequence.getAnnotation("Search Scores")[0].getEValue();
564  0 String bitScore = " Bit Score: "
565    + sequence.getAnnotation("Search Scores")[0].getBitScore();
566  0 sb.append(eValue);
567  0 sb.append("<br>");
568  0 sb.append(bitScore);
569  0 maxWidth = Math.max(maxWidth, eValue.length());
570  0 maxWidth = Math.max(maxWidth, bitScore.length());
571  0 sb.append("<br>");
572    }
573  11 sb.append("</i>");
574  11 return maxWidth;
575    }
576   
577    /**
578    * A helper method that appends any DBRefs, returning the maximum line length
579    * added
580    *
581    * @param sb
582    * @param ds
583    * @param summary
584    * @return
585    */
 
586  9 toggle protected int appendDbRefs(final StringBuilder sb, SequenceI ds,
587    boolean summary)
588    {
589  9 List<DBRefEntry> dbrefs, dbrefset = ds.getDBRefs();
590   
591  9 if (dbrefset == null)
592    {
593  6 return 0;
594    }
595   
596    // PATCH for JAL-3980 defensive copy
597   
598  3 dbrefs = new ArrayList<DBRefEntry>();
599   
600  3 dbrefs.addAll(dbrefset);
601    // note this sorts the refs held on the sequence!
602  3 dbrefs.sort(comparator);
603  3 boolean ellipsis = false;
604  3 String source = null;
605  3 String lastSource = null;
606  3 int countForSource = 0;
607  3 int sourceCount = 0;
608  3 boolean moreSources = false;
609  3 int maxLineLength = 0;
610  3 int lineLength = 0;
611   
612  3 for (DBRefEntry ref : dbrefs)
613    {
614  14 source = ref.getSource();
615  14 if (source == null)
616    {
617    // shouldn't happen
618  0 continue;
619    }
620  14 boolean sourceChanged = !source.equals(lastSource);
621  14 if (sourceChanged)
622    {
623  10 lineLength = 0;
624  10 countForSource = 0;
625  10 sourceCount++;
626    }
627  14 if (sourceCount > MAX_SOURCES && summary)
628    {
629  1 ellipsis = true;
630  1 moreSources = true;
631  1 break;
632    }
633  13 lastSource = source;
634  13 countForSource++;
635  13 if (countForSource == 1 || !summary)
636    {
637  9 sb.append("<br>\n");
638    }
639  13 if (countForSource <= MAX_REFS_PER_SOURCE || !summary)
640    {
641  12 String accessionId = ref.getAccessionId();
642  12 lineLength += accessionId.length() + 1;
643  12 if (countForSource > 1 && summary)
644    {
645  3 sb.append(",\n ").append(accessionId);
646  3 lineLength++;
647    }
648    else
649    {
650  9 sb.append(source).append(" ").append(accessionId);
651  9 lineLength += source.length();
652    }
653  12 maxLineLength = Math.max(maxLineLength, lineLength);
654    }
655  13 if (countForSource == MAX_REFS_PER_SOURCE && summary)
656    {
657  1 sb.append(COMMA).append(ELLIPSIS);
658  1 ellipsis = true;
659    }
660    }
661  3 if (moreSources)
662    {
663  1 sb.append("<br>\n").append(source).append(COMMA).append(ELLIPSIS);
664    }
665  3 if (ellipsis)
666    {
667  1 sb.append("<br>\n(");
668  1 sb.append(MessageManager.getString("label.output_seq_details"));
669  1 sb.append(")");
670    }
671   
672  3 return maxLineLength;
673    }
674   
 
675  0 toggle public void createTooltipAnnotationReport(final StringBuilder tip,
676    SequenceI sequence, boolean showDbRefs, boolean showNpFeats,
677    FeatureRendererModel fr)
678    {
679  0 int maxWidth = createSequenceAnnotationReport(tip, sequence,
680    showDbRefs, showNpFeats, fr, true);
681   
682  0 if (maxWidth > 60)
683    {
684    // ? not sure this serves any useful purpose
685    // tip.insert(0, "<table width=350 border=0><tr><td>");
686    // tip.append("</td></tr></table>");
687    }
688    }
689    }