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