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