Merge branch 'develop' into Jalview-JS/develop
[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.DBRefEntry;
25 import jalview.datamodel.DBRefSource;
26 import jalview.datamodel.GeneLociI;
27 import jalview.datamodel.MappedFeatures;
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 int MAX_DESCRIPTION_LENGTH = 40;
50
51   private static final String COMMA = ",";
52
53   private static final String ELLIPSIS = "...";
54
55   private static final int MAX_REFS_PER_SOURCE = 4;
56
57   private static final int MAX_SOURCES = 40;
58
59  // public static final String[][] PRIMARY_SOURCES  moved to DBRefSource.java
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<DBRefEntry>()
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 = DBRefSource.isPrimarySource(s1);
84       boolean s2Primary = DBRefSource.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 : DBRefSource.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 Returns number of
128    * features left if maxlength limit is (or would have been) reached
129    * 
130    * @param sb
131    * @param residuePos
132    * @param features
133    * @param minmax
134    * @param maxlength
135    */
136   public int appendFeaturesLengthLimit(final StringBuilder sb,
137           int residuePos, List<SequenceFeature> features,
138           FeatureRendererModel fr, int maxlength)
139   {
140     for (int i = 0; i < features.size(); i++)
141     {
142       SequenceFeature feature = features.get(i);
143       if (appendFeature(sb, residuePos, fr, feature, null, maxlength))
144       {
145         return features.size() - i;
146       }
147     }
148     return 0;
149   }
150
151   public void appendFeatures(final StringBuilder sb, int residuePos,
152           List<SequenceFeature> features, FeatureRendererModel fr)
153   {
154     appendFeaturesLengthLimit(sb, residuePos, features, fr, 0);
155   }
156
157   /**
158    * Appends text for mapped features (e.g. CDS feature for peptide or vice versa)
159    * Returns number of features left if maxlength limit is (or would have been)
160    * reached
161    * 
162    * @param sb
163    * @param residuePos
164    * @param mf
165    * @param fr
166    * @param maxlength
167    */
168   public int appendFeaturesLengthLimit(StringBuilder sb, int residuePos,
169           MappedFeatures mf, FeatureRendererModel fr, int maxlength)
170   {
171     for (int i = 0; i < mf.features.size(); i++)
172     {
173       SequenceFeature feature = mf.features.get(i);
174       if (appendFeature(sb, residuePos, fr, feature, mf, maxlength))
175       {
176         return mf.features.size() - i;
177       }
178     }
179     return 0;
180   }
181
182   public void appendFeatures(StringBuilder sb, int residuePos,
183           MappedFeatures mf, FeatureRendererModel fr)
184   {
185     appendFeaturesLengthLimit(sb, residuePos, mf, fr, 0);
186   }
187
188   /**
189    * Appends the feature at rpos to the given buffer
190    * 
191    * @param sb
192    * @param rpos
193    * @param minmax
194    * @param feature
195    */
196   boolean appendFeature(final StringBuilder sb0, int rpos,
197           FeatureRendererModel fr, SequenceFeature feature,
198           MappedFeatures mf, int maxlength)
199   {
200     StringBuilder sb = new StringBuilder();
201     if (feature.isContactFeature())
202     {
203       if (feature.getBegin() == rpos || feature.getEnd() == rpos)
204       {
205         if (sb0.length() > 6)
206         {
207           sb.append("<br/>");
208         }
209         sb.append(feature.getType()).append(" ").append(feature.getBegin())
210                 .append(":").append(feature.getEnd());
211       }
212       return appendTextMaxLengthReached(sb0, sb, maxlength);
213     }
214
215     if (sb0.length() > 6)
216     {
217       sb.append("<br/>");
218     }
219     // TODO: remove this hack to display link only features
220     boolean linkOnly = feature.getValue("linkonly") != null;
221     if (!linkOnly)
222     {
223       sb.append(feature.getType()).append(" ");
224       if (rpos != 0)
225       {
226         // we are marking a positional feature
227         sb.append(feature.begin);
228       }
229       if (feature.begin != feature.end)
230       {
231         sb.append(" ").append(feature.end);
232       }
233
234       String description = feature.getDescription();
235       if (description != null && !description.equals(feature.getType()))
236       {
237         description = StringUtils.stripHtmlTags(description);
238
239         /*
240          * truncate overlong descriptions unless they contain an href
241          * before the truncation point (as truncation could leave corrupted html)
242          */
243         int linkindex = description.toLowerCase().indexOf("<a ");
244         boolean hasLink = linkindex > -1
245                 && linkindex < MAX_DESCRIPTION_LENGTH;
246         if (description.length() > MAX_DESCRIPTION_LENGTH && !hasLink)
247         {
248           description = description.substring(0, MAX_DESCRIPTION_LENGTH)
249                   + ELLIPSIS;
250         }
251
252         sb.append("; ").append(description);
253       }
254
255       if (showScore(feature, fr))
256       {
257         sb.append(" Score=").append(String.valueOf(feature.getScore()));
258       }
259       String status = (String) feature.getValue("status");
260       if (status != null && status.length() > 0)
261       {
262         sb.append("; (").append(status).append(")");
263       }
264
265       /*
266        * add attribute value if coloured by attribute
267        */
268       if (fr != null)
269       {
270         FeatureColourI fc = fr.getFeatureColours().get(feature.getType());
271         if (fc != null && fc.isColourByAttribute())
272         {
273           String[] attName = fc.getAttributeName();
274           String attVal = feature.getValueAsString(attName);
275           if (attVal != null)
276           {
277             sb.append("; ").append(String.join(":", attName)).append("=")
278                     .append(attVal);
279           }
280         }
281       }
282
283       if (mf != null)
284       {
285         String variants = mf.findProteinVariants(feature);
286         if (!variants.isEmpty())
287         {
288           sb.append(" ").append(variants);
289         }
290       }
291     }
292     return appendTextMaxLengthReached(sb0, sb, maxlength);
293   }
294
295   void appendFeature(final StringBuilder sb, int rpos,
296           FeatureRendererModel fr, SequenceFeature feature,
297           MappedFeatures mf)
298   {
299     appendFeature(sb, rpos, fr, feature, mf, 0);
300   }
301
302   private static boolean appendTextMaxLengthReached(StringBuilder sb0,
303           StringBuilder sb, int maxlength)
304   {
305     boolean ret = false;
306     if (maxlength == 0 || sb0.length() + sb.length() < maxlength)
307     {
308       sb0.append(sb);
309       return false;
310     } else {
311       return true;
312     }
313   }
314
315   /**
316    * Answers true if score should be shown, else false. Score is shown if it is
317    * not NaN, and the feature type has a non-trivial min-max score range
318    */
319   boolean showScore(SequenceFeature feature, FeatureRendererModel fr)
320   {
321     if (Float.isNaN(feature.getScore()))
322     {
323       return false;
324     }
325     if (fr == null)
326     {
327       return true;
328     }
329     float[][] minMax = fr.getMinMax().get(feature.getType());
330
331     /*
332      * minMax[0] is the [min, max] score range for positional features
333      */
334     if (minMax == null || minMax[0] == null || minMax[0][0] == minMax[0][1])
335     {
336       return false;
337     }
338     return true;
339   }
340
341   /**
342    * Format and appends any hyperlinks for the sequence feature to the string
343    * buffer
344    * 
345    * @param sb
346    * @param feature
347    */
348   void appendLinks(final StringBuffer sb, SequenceFeature feature)
349   {
350     if (feature.links != null)
351     {
352       if (linkImageURL != null)
353       {
354         sb.append(" <img src=\"" + linkImageURL + "\">");
355       }
356       else
357       {
358         for (String urlstring : feature.links)
359         {
360           try
361           {
362             for (List<String> urllink : createLinksFrom(null, urlstring))
363             {
364               sb.append("<br/> <a href=\""
365                       + urllink.get(3)
366                       + "\" target=\""
367                       + urllink.get(0)
368                       + "\">"
369                       + (urllink.get(0).toLowerCase()
370                               .equals(urllink.get(1).toLowerCase()) ? urllink
371                               .get(0) : (urllink.get(0) + ":" + urllink
372                                               .get(1)))
373                       + "</a><br/>");
374             }
375           } catch (Exception x)
376           {
377             System.err.println("problem when creating links from "
378                     + urlstring);
379             x.printStackTrace();
380           }
381         }
382       }
383
384     }
385   }
386
387   /**
388    * 
389    * @param seq
390    * @param link
391    * @return Collection< List<String> > { List<String> { link target, link
392    *         label, dynamic component inserted (if any), url }}
393    */
394   Collection<List<String>> createLinksFrom(SequenceI seq, String link)
395   {
396     Map<String, List<String>> urlSets = new LinkedHashMap<>();
397     UrlLink urlLink = new UrlLink(link);
398     if (!urlLink.isValid())
399     {
400       System.err.println(urlLink.getInvalidMessage());
401       return null;
402     }
403
404     urlLink.createLinksFromSeq(seq, urlSets);
405
406     return urlSets.values();
407   }
408
409   public void createSequenceAnnotationReport(final StringBuilder tip,
410           SequenceI sequence, boolean showDbRefs, boolean showNpFeats,
411           FeatureRendererModel fr)
412   {
413     createSequenceAnnotationReport(tip, sequence, showDbRefs, showNpFeats,
414             fr, false);
415   }
416
417   /**
418    * Builds an html formatted report of sequence details and appends it to the
419    * provided buffer.
420    * 
421    * @param sb
422    *          buffer to append report to
423    * @param sequence
424    *          the sequence the report is for
425    * @param showDbRefs
426    *          whether to include database references for the sequence
427    * @param showNpFeats
428    *          whether to include non-positional sequence features
429    * @param fr
430    * @param summary
431    * @return
432    */
433   int createSequenceAnnotationReport(final StringBuilder sb,
434           SequenceI sequence, boolean showDbRefs, boolean showNpFeats,
435           FeatureRendererModel fr, boolean summary)
436   {
437     String tmp;
438     sb.append("<i>");
439
440     int maxWidth = 0;
441     if (sequence.getDescription() != null)
442     {
443       tmp = sequence.getDescription();
444       sb.append(tmp);
445       maxWidth = Math.max(maxWidth, tmp.length());
446     }
447     SequenceI ds = sequence;
448     while (ds.getDatasetSequence() != null)
449     {
450       ds = ds.getDatasetSequence();
451     }
452
453     if (showDbRefs)
454     {
455       maxWidth = Math.max(maxWidth, appendDbRefs(sb, ds, summary));
456     }
457
458     /*
459      * add non-positional features if wanted
460      */
461     if (showNpFeats)
462     {
463       for (SequenceFeature sf : sequence.getFeatures()
464               .getNonPositionalFeatures())
465       {
466         int sz = -sb.length();
467         appendFeature(sb, 0, fr, sf, null);
468         sz += sb.length();
469         maxWidth = Math.max(maxWidth, sz);
470       }
471     }
472     sb.append("</i>");
473     return maxWidth;
474   }
475
476   /**
477    * A helper method that appends any DBRefs, returning the maximum line length
478    * added
479    * 
480    * @param sb
481    * @param ds
482    * @param summary
483    * @return
484    */
485   protected int appendDbRefs(final StringBuilder sb, SequenceI ds,
486           boolean summary)
487   {
488     List<DBRefEntry> dbrefs = ds.getDBRefs();
489     if (dbrefs == null)
490     {
491       return 0;
492     }
493
494     // note this sorts the refs held on the sequence!
495     dbrefs.sort(comparator);
496     boolean ellipsis = false;
497     String source = null;
498     String lastSource = null;
499     int countForSource = 0;
500     int sourceCount = 0;
501     boolean moreSources = false;
502     int maxLineLength = 0;
503     int lineLength = 0;
504
505     for (DBRefEntry ref : dbrefs)
506     {
507       source = ref.getSource();
508       if (source == null)
509       {
510         // shouldn't happen
511         continue;
512       }
513       boolean sourceChanged = !source.equals(lastSource);
514       if (sourceChanged)
515       {
516         lineLength = 0;
517         countForSource = 0;
518         sourceCount++;
519       }
520       if (sourceCount > MAX_SOURCES && summary)
521       {
522         ellipsis = true;
523         moreSources = true;
524         break;
525       }
526       lastSource = source;
527       countForSource++;
528       if (countForSource == 1 || !summary)
529       {
530         sb.append("<br/>");
531       }
532       if (countForSource <= MAX_REFS_PER_SOURCE || !summary)
533       {
534         String accessionId = ref.getAccessionId();
535         lineLength += accessionId.length() + 1;
536         if (countForSource > 1 && summary)
537         {
538           sb.append(", ").append(accessionId);
539           lineLength++;
540         }
541         else
542         {
543           sb.append(source).append(" ").append(accessionId);
544           lineLength += source.length();
545         }
546         maxLineLength = Math.max(maxLineLength, lineLength);
547       }
548       if (countForSource == MAX_REFS_PER_SOURCE && summary)
549       {
550         sb.append(COMMA).append(ELLIPSIS);
551         ellipsis = true;
552       }
553     }
554     if (moreSources)
555     {
556       sb.append("<br/>").append(source).append(COMMA).append(ELLIPSIS);
557     }
558     if (ellipsis)
559     {
560       sb.append("<br/>(");
561       sb.append(MessageManager.getString("label.output_seq_details"));
562       sb.append(")");
563     }
564
565     return maxLineLength;
566   }
567
568   public void createTooltipAnnotationReport(final StringBuilder tip,
569           SequenceI sequence, boolean showDbRefs, boolean showNpFeats,
570           FeatureRendererModel fr)
571   {
572     int maxWidth = createSequenceAnnotationReport(tip, sequence,
573             showDbRefs, showNpFeats, fr, true);
574
575     if (maxWidth > 60)
576     {
577       // ? not sure this serves any useful purpose
578       // tip.insert(0, "<table width=350 border=0><tr><td>");
579       // tip.append("</td></tr></table>");
580     }
581   }
582 }