Clover icon

Coverage Report

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

File SequenceAnnotationReport.java

 

Coverage histogram

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

Code metrics

118
195
13
1
649
428
91
0.47
15
13
7

Classes

Class Line # Actions
SequenceAnnotationReport 48 195 91
0.7883435578.8%
 

Contributing tests

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