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