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