61b749123c20fd8eb1848628443785d3c4e89f02
[jalview.git] / src / jalview / io / SequenceAnnotationReport.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.io;
22
23 import jalview.api.FeatureColourI;
24 import jalview.datamodel.AlignmentAnnotation;
25 import jalview.datamodel.DBRefEntry;
26 import jalview.datamodel.DBRefSource;
27 import jalview.datamodel.GeneLociI;
28 import jalview.datamodel.MappedFeatures;
29 import jalview.datamodel.SequenceFeature;
30 import jalview.datamodel.SequenceI;
31 import jalview.util.MessageManager;
32 import jalview.util.StringUtils;
33 import jalview.util.UrlLink;
34 import jalview.viewmodel.seqfeatures.FeatureRendererModel;
35
36 import java.util.Arrays;
37 import java.util.Collection;
38 import java.util.Comparator;
39 import java.util.LinkedHashMap;
40 import java.util.List;
41 import java.util.Map;
42
43 /**
44  * generate HTML reports for a sequence
45  * 
46  * @author jimp
47  */
48 public class SequenceAnnotationReport
49 {
50   private static final String COMMA = ",";
51
52   private static final String ELLIPSIS = "...";
53
54   private static final int MAX_REFS_PER_SOURCE = 4;
55
56   private static final int MAX_SOURCES = 40;
57
58   private static final String[][] PRIMARY_SOURCES = new String[][] {
59       DBRefSource.CODINGDBS, DBRefSource.DNACODINGDBS,
60       DBRefSource.PROTEINDBS };
61
62   final String linkImageURL;
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<>()
69   {
70
71     @Override
72     public int compare(DBRefEntry ref1, DBRefEntry ref2)
73     {
74       if (ref1 instanceof GeneLociI)
75       {
76         return -1;
77       }
78       if (ref2 instanceof GeneLociI)
79       {
80         return 1;
81       }
82       String s1 = ref1.getSource();
83       String s2 = ref2.getSource();
84       boolean s1Primary = isPrimarySource(s1);
85       boolean s2Primary = isPrimarySource(s2);
86       if (s1Primary && !s2Primary)
87       {
88         return -1;
89       }
90       if (!s1Primary && s2Primary)
91       {
92         return 1;
93       }
94       int comp = s1 == null ? -1 : (s2 == null ? 1 : s1
95               .compareToIgnoreCase(s2));
96       if (comp == 0)
97       {
98         String a1 = ref1.getAccessionId();
99         String a2 = ref2.getAccessionId();
100         comp = a1 == null ? -1 : (a2 == null ? 1 : a1
101                 .compareToIgnoreCase(a2));
102       }
103       return comp;
104     }
105
106     private boolean isPrimarySource(String source)
107     {
108       for (String[] primary : PRIMARY_SOURCES)
109       {
110         for (String s : primary)
111         {
112           if (source.equals(s))
113           {
114             return true;
115           }
116         }
117       }
118       return false;
119     }
120   };
121
122   public SequenceAnnotationReport(String linkURL)
123   {
124     this.linkImageURL = linkURL;
125   }
126
127   /**
128    * Append text for the list of features to the tooltip
129    * 
130    * @param sb
131    * @param residuePos
132    * @param features
133    * @param minmax
134    */
135   public void appendFeatures(final StringBuilder sb, int residuePos,
136           List<SequenceFeature> features, FeatureRendererModel fr)
137   {
138     for (SequenceFeature feature : features)
139     {
140       appendFeature(sb, residuePos, fr, feature, null);
141     }
142   }
143
144   /**
145    * Appends text for mapped features (e.g. CDS feature for peptide or vice versa)
146    * 
147    * @param sb
148    * @param residuePos
149    * @param mf
150    * @param fr
151    */
152   public void appendFeatures(StringBuilder sb, int residuePos,
153           MappedFeatures mf, FeatureRendererModel fr)
154   {
155     for (SequenceFeature feature : mf.features)
156     {
157       appendFeature(sb, residuePos, fr, feature, mf);
158     }
159   }
160
161   /**
162    * Appends the feature at rpos to the given buffer
163    * 
164    * @param sb
165    * @param rpos
166    * @param minmax
167    * @param feature
168    */
169   void appendFeature(final StringBuilder sb, int rpos,
170           FeatureRendererModel fr, SequenceFeature feature,
171           MappedFeatures mf)
172   {
173     if (feature.isContactFeature())
174     {
175       if (feature.getBegin() == rpos || feature.getEnd() == rpos)
176       {
177         if (sb.length() > 6)
178         {
179           sb.append("<br>");
180         }
181         sb.append(feature.getType()).append(" ").append(feature.getBegin())
182                 .append(":").append(feature.getEnd());
183       }
184       return;
185     }
186
187     if (sb.length() > 6)
188     {
189       sb.append("<br>");
190     }
191     // TODO: remove this hack to display link only features
192     boolean linkOnly = feature.getValue("linkonly") != null;
193     if (!linkOnly)
194     {
195       sb.append(feature.getType()).append(" ");
196       if (rpos != 0)
197       {
198         // we are marking a positional feature
199         sb.append(feature.begin);
200       }
201       if (feature.begin != feature.end)
202       {
203         sb.append(" ").append(feature.end);
204       }
205
206       String description = feature.getDescription();
207       if (description != null && !description.equals(feature.getType()))
208       {
209         description = StringUtils.stripHtmlTags(description);
210         sb.append("; ").append(description);
211       }
212
213       if (showScore(feature, fr))
214       {
215         sb.append(" Score=").append(String.valueOf(feature.getScore()));
216       }
217       String status = (String) feature.getValue("status");
218       if (status != null && status.length() > 0)
219       {
220         sb.append("; (").append(status).append(")");
221       }
222
223       /*
224        * add attribute value if coloured by attribute
225        */
226       if (fr != null)
227       {
228         FeatureColourI fc = fr.getFeatureColours().get(feature.getType());
229         if (fc != null && fc.isColourByAttribute())
230         {
231           String[] attName = fc.getAttributeName();
232           String attVal = feature.getValueAsString(attName);
233           if (attVal != null)
234           {
235             sb.append("; ").append(String.join(":", attName)).append("=")
236                     .append(attVal);
237           }
238         }
239       }
240
241       if (mf != null)
242       {
243         String variants = mf.findProteinVariants(feature);
244         if (!variants.isEmpty())
245         {
246           sb.append(" ").append(variants);
247         }
248       }
249     }
250   }
251
252   /**
253    * Answers true if score should be shown, else false. Score is shown if it is
254    * not NaN, and the feature type has a non-trivial min-max score range
255    */
256   boolean showScore(SequenceFeature feature, FeatureRendererModel fr)
257   {
258     if (Float.isNaN(feature.getScore()))
259     {
260       return false;
261     }
262     if (fr == null)
263     {
264       return true;
265     }
266     float[][] minMax = fr.getMinMax().get(feature.getType());
267
268     /*
269      * minMax[0] is the [min, max] score range for positional features
270      */
271     if (minMax == null || minMax[0] == null || minMax[0][0] == minMax[0][1])
272     {
273       return false;
274     }
275     return true;
276   }
277
278   /**
279    * Format and appends any hyperlinks for the sequence feature to the string
280    * buffer
281    * 
282    * @param sb
283    * @param feature
284    */
285   void appendLinks(final StringBuffer sb, SequenceFeature feature)
286   {
287     if (feature.links != null)
288     {
289       if (linkImageURL != null)
290       {
291         sb.append(" <img src=\"" + linkImageURL + "\">");
292       }
293       else
294       {
295         for (String urlstring : feature.links)
296         {
297           try
298           {
299             for (List<String> urllink : createLinksFrom(null, urlstring))
300             {
301               sb.append("<br/> <a href=\""
302                       + urllink.get(3)
303                       + "\" target=\""
304                       + urllink.get(0)
305                       + "\">"
306                       + (urllink.get(0).toLowerCase()
307                               .equals(urllink.get(1).toLowerCase()) ? urllink
308                               .get(0) : (urllink.get(0) + ":" + urllink
309                               .get(1))) + "</a></br>");
310             }
311           } catch (Exception x)
312           {
313             System.err.println("problem when creating links from "
314                     + urlstring);
315             x.printStackTrace();
316           }
317         }
318       }
319
320     }
321   }
322
323   /**
324    * 
325    * @param seq
326    * @param link
327    * @return Collection< List<String> > { List<String> { link target, link
328    *         label, dynamic component inserted (if any), url }}
329    */
330   Collection<List<String>> createLinksFrom(SequenceI seq, String link)
331   {
332     Map<String, List<String>> urlSets = new LinkedHashMap<>();
333     UrlLink urlLink = new UrlLink(link);
334     if (!urlLink.isValid())
335     {
336       System.err.println(urlLink.getInvalidMessage());
337       return null;
338     }
339
340     urlLink.createLinksFromSeq(seq, urlSets);
341
342     return urlSets.values();
343   }
344
345   public void createSequenceAnnotationReport(final StringBuilder tip,
346           SequenceI sequence, boolean showDbRefs, boolean showNpFeats,
347           FeatureRendererModel fr)
348   {
349     createSequenceAnnotationReport(tip, sequence, showDbRefs, showNpFeats,
350             fr, false);
351   }
352
353   /**
354    * Builds an html formatted report of sequence details and appends it to the
355    * provided buffer.
356    * 
357    * @param sb
358    *          buffer to append report to
359    * @param sequence
360    *          the sequence the report is for
361    * @param showDbRefs
362    *          whether to include database references for the sequence
363    * @param showNpFeats
364    *          whether to include non-positional sequence features
365    * @param fr
366    * @param summary
367    * @return
368    */
369   int createSequenceAnnotationReport(final StringBuilder sb,
370           SequenceI sequence, boolean showDbRefs, boolean showNpFeats,
371           FeatureRendererModel fr, boolean summary)
372   {
373     String tmp;
374     sb.append("<i>");
375
376     int maxWidth = 0;
377     if (sequence.getDescription() != null)
378     {
379       tmp = sequence.getDescription();
380       sb.append("<br>").append(tmp);
381       maxWidth = Math.max(maxWidth, tmp.length());
382     }
383
384     SequenceI ds = sequence;
385     while (ds.getDatasetSequence() != null)
386     {
387       ds = ds.getDatasetSequence();
388     }
389
390     /*
391      * add any annotation scores
392      */
393     AlignmentAnnotation[] anns = ds.getAnnotation();
394     for (int i = 0; anns != null && i < anns.length; i++)
395     {
396       AlignmentAnnotation aa = anns[i];
397       if (aa != null && aa.hasScore() && aa.sequenceRef != null)
398       {
399         sb.append("<br>").append(aa.label).append(": ")
400                 .append(aa.getScore());
401       }
402     }
403
404     if (showDbRefs)
405     {
406       maxWidth = Math.max(maxWidth, appendDbRefs(sb, ds, summary));
407     }
408
409     /*
410      * add non-positional features if wanted
411      */
412     if (showNpFeats)
413     {
414       for (SequenceFeature sf : sequence.getFeatures()
415               .getNonPositionalFeatures())
416       {
417         int sz = -sb.length();
418         appendFeature(sb, 0, fr, sf, null);
419         sz += sb.length();
420         maxWidth = Math.max(maxWidth, sz);
421       }
422     }
423
424
425     if (sequence.getAnnotation("Search Scores") != null)
426     {
427       sb.append("<br>");
428       String eValue = " E-Value: "
429               + sequence.getAnnotation("Search Scores")[0].getEValue();
430       String bitScore = " Bit Score: "
431               + sequence.getAnnotation("Search Scores")[0].getBitScore();
432       sb.append(eValue);
433       sb.append("<br>");
434       sb.append(bitScore);
435       maxWidth = Math.max(maxWidth, eValue.length());
436       maxWidth = Math.max(maxWidth, bitScore.length());
437     }
438     sb.append("<br>");
439     sb.append("</i>");
440
441     return maxWidth;
442   }
443
444   /**
445    * A helper method that appends any DBRefs, returning the maximum line length
446    * added
447    * 
448    * @param sb
449    * @param ds
450    * @param summary
451    * @return
452    */
453   protected int appendDbRefs(final StringBuilder sb, SequenceI ds,
454           boolean summary)
455   {
456     DBRefEntry[] dbrefs = ds.getDBRefs();
457     if (dbrefs == null)
458     {
459       return 0;
460     }
461
462     // note this sorts the refs held on the sequence!
463     Arrays.sort(dbrefs, comparator);
464     boolean ellipsis = false;
465     String source = null;
466     String lastSource = null;
467     int countForSource = 0;
468     int sourceCount = 0;
469     boolean moreSources = false;
470     int maxLineLength = 0;
471     int lineLength = 0;
472
473     for (DBRefEntry ref : dbrefs)
474     {
475       source = ref.getSource();
476       if (source == null)
477       {
478         // shouldn't happen
479         continue;
480       }
481       boolean sourceChanged = !source.equals(lastSource);
482       if (sourceChanged)
483       {
484         lineLength = 0;
485         countForSource = 0;
486         sourceCount++;
487       }
488       if (sourceCount > MAX_SOURCES && summary)
489       {
490         ellipsis = true;
491         moreSources = true;
492         break;
493       }
494       lastSource = source;
495       countForSource++;
496       if (countForSource == 1 || !summary)
497       {
498         sb.append("<br>");
499       }
500       if (countForSource <= MAX_REFS_PER_SOURCE || !summary)
501       {
502         String accessionId = ref.getAccessionId();
503         lineLength += accessionId.length() + 1;
504         if (countForSource > 1 && summary)
505         {
506           sb.append(", ").append(accessionId);
507           lineLength++;
508         }
509         else
510         {
511           sb.append(source).append(" ").append(accessionId);
512           lineLength += source.length();
513         }
514         maxLineLength = Math.max(maxLineLength, lineLength);
515       }
516       if (countForSource == MAX_REFS_PER_SOURCE && summary)
517       {
518         sb.append(COMMA).append(ELLIPSIS);
519         ellipsis = true;
520       }
521     }
522     if (moreSources)
523     {
524       sb.append("<br>").append(source).append(COMMA).append(ELLIPSIS);
525     }
526     if (ellipsis)
527     {
528       sb.append("<br>(");
529       sb.append(MessageManager.getString("label.output_seq_details"));
530       sb.append(")");
531     }
532
533     return maxLineLength;
534   }
535
536   public void createTooltipAnnotationReport(final StringBuilder tip,
537           SequenceI sequence, boolean showDbRefs, boolean showNpFeats,
538           FeatureRendererModel fr)
539   {
540     int maxWidth = createSequenceAnnotationReport(tip, sequence,
541             showDbRefs, showNpFeats, fr, true);
542
543     if (maxWidth > 60)
544     {
545       // ? not sure this serves any useful purpose
546       // tip.insert(0, "<table width=350 border=0><tr><td>");
547       // tip.append("</td></tr></table>");
548     }
549   }
550 }