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