Merge branch 'develop' into features/JAL-1723_sequenceReport
[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.datamodel.DBRefEntry;
24 import jalview.datamodel.DBRefSource;
25 import jalview.datamodel.SequenceFeature;
26 import jalview.datamodel.SequenceI;
27 import jalview.io.gff.GffConstants;
28 import jalview.util.DBRefUtils;
29 import jalview.util.MessageManager;
30 import jalview.util.UrlLink;
31
32 import java.util.ArrayList;
33 import java.util.Arrays;
34 import java.util.Comparator;
35 import java.util.List;
36 import java.util.Map;
37
38 /**
39  * generate HTML reports for a sequence
40  * 
41  * @author jimp
42  */
43 public class SequenceAnnotationReport
44 {
45   private static final String COMMA = ",";
46
47   private static final String ELLIPSIS = "...";
48
49   private static final int MAX_REFS_PER_SOURCE = 4;
50
51   private static final int MAX_SOURCES = 40;
52
53   private static final String[][] PRIMARY_SOURCES = new String[][] {
54       DBRefSource.CODINGDBS, DBRefSource.DNACODINGDBS,
55       DBRefSource.PROTEINDBS };
56
57   final String linkImageURL;
58
59   /*
60    * Comparator to order DBRefEntry by Source + accession id (case-insensitive)
61    */
62   private static Comparator<DBRefEntry> comparator = new Comparator<DBRefEntry>()
63   {
64
65     @Override
66     public int compare(DBRefEntry ref1, DBRefEntry ref2)
67     {
68       String s1 = ref1.getSource();
69       String s2 = ref2.getSource();
70       boolean s1Primary = isPrimarySource(s1);
71       boolean s2Primary = isPrimarySource(s2);
72       if (s1Primary && !s2Primary)
73       {
74         return -1;
75       }
76       if (!s1Primary && s2Primary)
77       {
78         return 1;
79       }
80       int comp = s1 == null ? -1 : (s2 == null ? 1 : s1
81               .compareToIgnoreCase(s2));
82       if (comp == 0)
83       {
84         String a1 = ref1.getAccessionId();
85         String a2 = ref2.getAccessionId();
86         comp = a1 == null ? -1 : (a2 == null ? 1 : a1
87                 .compareToIgnoreCase(a2));
88       }
89       return comp;
90     }
91
92     private boolean isPrimarySource(String source)
93     {
94       for (String[] primary : PRIMARY_SOURCES)
95       {
96         for (String s : primary)
97         {
98           if (source.equals(s))
99           {
100             return true;
101           }
102         }
103       }
104       return false;
105     }
106   };
107
108   public SequenceAnnotationReport(String linkImageURL)
109   {
110     this.linkImageURL = linkImageURL;
111   }
112
113   /**
114    * Append text for the list of features to the tooltip
115    * 
116    * @param sb
117    * @param rpos
118    * @param features
119    * @param minmax
120    */
121   public void appendFeatures(final StringBuilder sb, int rpos,
122           List<SequenceFeature> features, Map<String, float[][]> minmax)
123   {
124     if (features != null)
125     {
126       for (SequenceFeature feature : features)
127       {
128         appendFeature(sb, rpos, minmax, feature);
129       }
130     }
131   }
132
133   /**
134    * Appends the feature at rpos to the given buffer
135    * 
136    * @param sb
137    * @param rpos
138    * @param minmax
139    * @param feature
140    */
141   void appendFeature(final StringBuilder sb, int rpos,
142           Map<String, float[][]> minmax, SequenceFeature feature)
143   {
144     String tmpString;
145     if (feature.getType().equals("disulfide bond"))
146     {
147       if (feature.getBegin() == rpos || feature.getEnd() == rpos)
148       {
149         if (sb.length() > 6)
150         {
151           sb.append("<br>");
152         }
153         sb.append("disulfide bond ").append(feature.getBegin()).append(":")
154                 .append(feature.getEnd());
155       }
156     }
157     else
158     {
159       if (sb.length() > 6)
160       {
161         sb.append("<br>");
162       }
163       // TODO: remove this hack to display link only features
164       boolean linkOnly = feature.getValue("linkonly") != null;
165       if (!linkOnly)
166       {
167         sb.append(feature.getType()).append(" ");
168         if (rpos != 0)
169         {
170           // we are marking a positional feature
171           sb.append(feature.begin);
172         }
173         if (feature.begin != feature.end)
174         {
175           sb.append(" ").append(feature.end);
176         }
177
178         if (feature.getDescription() != null
179                 && !feature.description.equals(feature.getType()))
180         {
181           tmpString = feature.getDescription();
182           String tmp2up = tmpString.toUpperCase();
183           int startTag = tmp2up.indexOf("<HTML>");
184           if (startTag > -1)
185           {
186             tmpString = tmpString.substring(startTag + 6);
187             tmp2up = tmp2up.substring(startTag + 6);
188           }
189           int endTag = tmp2up.indexOf("</BODY>");
190           if (endTag > -1)
191           {
192             tmpString = tmpString.substring(0, endTag);
193             tmp2up = tmp2up.substring(0, endTag);
194           }
195           endTag = tmp2up.indexOf("</HTML>");
196           if (endTag > -1)
197           {
198             tmpString = tmpString.substring(0, endTag);
199           }
200
201           if (startTag > -1)
202           {
203             sb.append("; ").append(tmpString);
204           }
205           else
206           {
207             if (tmpString.indexOf("<") > -1 || tmpString.indexOf(">") > -1)
208             {
209               // The description does not specify html is to
210               // be used, so we must remove < > symbols
211               tmpString = tmpString.replaceAll("<", "&lt;");
212               tmpString = tmpString.replaceAll(">", "&gt;");
213
214               sb.append("; ");
215               sb.append(tmpString);
216             }
217             else
218             {
219               sb.append("; ").append(tmpString);
220             }
221           }
222         }
223         // check score should be shown
224         if (!Float.isNaN(feature.getScore()))
225         {
226           float[][] rng = (minmax == null) ? null : ((float[][]) minmax
227                   .get(feature.getType()));
228           if (rng != null && rng[0] != null && rng[0][0] != rng[0][1])
229           {
230             sb.append(" Score=" + feature.getScore());
231           }
232         }
233         String status = (String) feature.getValue("status");
234         if (status != null && status.length() > 0)
235         {
236           sb.append("; (").append(status).append(")");
237         }
238         String clinSig = (String) feature
239                 .getValue(GffConstants.CLINICAL_SIGNIFICANCE);
240         if (clinSig != null)
241         {
242           sb.append("; ").append(clinSig);
243         }
244       }
245     }
246   }
247
248   /**
249    * Format and appends any hyperlinks for the sequence feature to the string
250    * buffer
251    * 
252    * @param sb
253    * @param feature
254    */
255   void appendLinks(final StringBuffer sb, SequenceFeature feature)
256   {
257     if (feature.links != null)
258     {
259       if (linkImageURL != null)
260       {
261         sb.append(" <img src=\"" + linkImageURL + "\">");
262       }
263       else
264       {
265         for (String urlstring : feature.links)
266         {
267           try
268           {
269             for (String[] urllink : createLinksFrom(null, urlstring))
270             {
271               sb.append("<br/> <a href=\""
272                       + urllink[3]
273                       + "\" target=\""
274                       + urllink[0]
275                       + "\">"
276                       + (urllink[0].toLowerCase().equals(
277                               urllink[1].toLowerCase()) ? urllink[0]
278                               : (urllink[0] + ":" + urllink[1]))
279                       + "</a></br>");
280             }
281           } catch (Exception x)
282           {
283             System.err.println("problem when creating links from "
284                     + urlstring);
285             x.printStackTrace();
286           }
287         }
288       }
289
290     }
291   }
292
293   /**
294    * 
295    * @param seq
296    * @param link
297    * @return String[][] { String[] { link target, link label, dynamic component
298    *         inserted (if any), url }}
299    */
300   String[][] createLinksFrom(SequenceI seq, String link)
301   {
302     List<String[]> urlSets = new ArrayList<String[]>();
303     List<String> uniques = new ArrayList<String>();
304     UrlLink urlLink = new UrlLink(link);
305     if (!urlLink.isValid())
306     {
307       System.err.println(urlLink.getInvalidMessage());
308       return null;
309     }
310     final String target = urlLink.getTarget(); // link.substring(0,
311     // link.indexOf("|"));
312     final String label = urlLink.getLabel();
313     if (seq != null && urlLink.isDynamic())
314     {
315       urlSets.addAll(createDynamicLinks(seq, urlLink, uniques));
316     }
317     else
318     {
319       String unq = label + "|" + urlLink.getUrl_prefix();
320       if (!uniques.contains(unq))
321       {
322         uniques.add(unq);
323         urlSets.add(new String[] { target, label, null,
324             urlLink.getUrl_prefix() });
325       }
326     }
327
328     return urlSets.toArray(new String[][] {});
329   }
330
331   /**
332    * Formats and returns a list of dynamic href links
333    * 
334    * @param seq
335    * @param urlLink
336    * @param uniques
337    */
338   List<String[]> createDynamicLinks(SequenceI seq, UrlLink urlLink,
339           List<String> uniques)
340   {
341     List<String[]> result = new ArrayList<String[]>();
342     final String target = urlLink.getTarget();
343     final String label = urlLink.getLabel();
344
345     // collect matching db-refs
346     DBRefEntry[] dbr = DBRefUtils.selectRefs(seq.getDBRefs(),
347             new String[] { target });
348     // collect id string too
349     String id = seq.getName();
350     String descr = seq.getDescription();
351     if (descr != null && descr.length() < 1)
352     {
353       descr = null;
354     }
355     if (dbr != null)
356     {
357       for (int r = 0; r < dbr.length; r++)
358       {
359         if (id != null && dbr[r].getAccessionId().equals(id))
360         {
361           // suppress duplicate link creation for the bare sequence ID
362           // string with this link
363           id = null;
364         }
365         // create Bare ID link for this URL
366         String[] urls = urlLink.makeUrls(dbr[r].getAccessionId(), true);
367         if (urls != null)
368         {
369           for (int u = 0; u < urls.length; u += 2)
370           {
371             String unq = urls[u] + "|" + urls[u + 1];
372             if (!uniques.contains(unq))
373             {
374               result.add(new String[] { target, label, urls[u], urls[u + 1] });
375               uniques.add(unq);
376             }
377           }
378         }
379       }
380     }
381     if (id != null)
382     {
383       // create Bare ID link for this URL
384       String[] urls = urlLink.makeUrls(id, true);
385       if (urls != null)
386       {
387         for (int u = 0; u < urls.length; u += 2)
388         {
389           String unq = urls[u] + "|" + urls[u + 1];
390           if (!uniques.contains(unq))
391           {
392             result.add(new String[] { target, label, urls[u], urls[u + 1] });
393             uniques.add(unq);
394           }
395         }
396       }
397     }
398     if (descr != null && urlLink.getRegexReplace() != null)
399     {
400       // create link for this URL from description only if regex matches
401       String[] urls = urlLink.makeUrls(descr, true);
402       if (urls != null)
403       {
404         for (int u = 0; u < urls.length; u += 2)
405         {
406           String unq = urls[u] + "|" + urls[u + 1];
407           if (!uniques.contains(unq))
408           {
409             result.add(new String[] { target, label, urls[u], urls[u + 1] });
410             uniques.add(unq);
411           }
412         }
413       }
414     }
415     return result;
416   }
417
418   public void createSequenceAnnotationReport(final StringBuilder tip,
419           SequenceI sequence, boolean showDbRefs, boolean showNpFeats,
420           Map<String, float[][]> minmax)
421   {
422     createSequenceAnnotationReport(tip, sequence, showDbRefs, showNpFeats,
423             minmax, false);
424   }
425
426   /**
427    * Builds an html formatted report of sequence details and appends it to the
428    * provided buffer.
429    * 
430    * @param sb
431    *          buffer to append report to
432    * @param sequence
433    *          the sequence the report is for
434    * @param showDbRefs
435    *          whether to include database references for the sequence
436    * @param showNpFeats
437    *          whether to include non-positional sequence features
438    * @param minmax
439    * @param summary
440    * @return
441    */
442   int createSequenceAnnotationReport(final StringBuilder sb,
443           SequenceI sequence, boolean showDbRefs, boolean showNpFeats,
444           Map<String, float[][]> minmax, boolean summary)
445   {
446     String tmp;
447     sb.append("<i>");
448
449     int maxWidth = 0;
450     if (sequence.getDescription() != null)
451     {
452       tmp = sequence.getDescription();
453       sb.append("<br>").append(tmp);
454       maxWidth = Math.max(maxWidth, tmp.length());
455     }
456     SequenceI ds = sequence;
457     while (ds.getDatasetSequence() != null)
458     {
459       ds = ds.getDatasetSequence();
460     }
461     DBRefEntry[] dbrefs = ds.getDBRefs();
462     if (showDbRefs && dbrefs != null)
463     {
464       // note this sorts the refs held on the sequence!
465       Arrays.sort(dbrefs, comparator);
466       boolean ellipsis = false;
467       String source = null;
468       String lastSource = null;
469       int countForSource = 0;
470       int sourceCount = 0;
471       boolean moreSources = false;
472       int lineLength = 0;
473
474       for (DBRefEntry ref : dbrefs)
475       {
476         source = ref.getSource();
477         if (source == null)
478         {
479           // shouldn't happen
480           continue;
481         }
482         boolean sourceChanged = !source.equals(lastSource);
483         if (sourceChanged)
484         {
485           lineLength = 0;
486           countForSource = 0;
487           sourceCount++;
488         }
489         if (sourceCount > MAX_SOURCES && summary)
490         {
491           ellipsis = true;
492           moreSources = true;
493           break;
494         }
495         lastSource = source;
496         countForSource++;
497         if (countForSource == 1 || !summary)
498         {
499           sb.append("<br>");
500         }
501         if (countForSource <= MAX_REFS_PER_SOURCE || !summary)
502         {
503           String accessionId = ref.getAccessionId();
504           lineLength += accessionId.length() + 1;
505           if (countForSource > 1 && summary)
506           {
507             sb.append(", ").append(accessionId);
508             lineLength++;
509           }
510           else
511           {
512             sb.append(source).append(" ").append(accessionId);
513             lineLength += source.length();
514           }
515           maxWidth = Math.max(maxWidth, lineLength);
516         }
517         if (countForSource == MAX_REFS_PER_SOURCE && summary)
518         {
519           sb.append(COMMA).append(ELLIPSIS);
520           ellipsis = true;
521         }
522       }
523       if (moreSources)
524       {
525         sb.append("<br>").append(ELLIPSIS).append(COMMA).append(source)
526                 .append(COMMA).append(ELLIPSIS);
527       }
528       if (ellipsis)
529       {
530         sb.append("<br>(");
531         sb.append(MessageManager.getString("label.output_seq_details"));
532         sb.append(")");
533       }
534     }
535
536     /*
537      * add non-positional features if wanted
538      */
539     SequenceFeature[] features = sequence.getSequenceFeatures();
540     if (showNpFeats && features != null)
541     {
542       for (int i = 0; i < features.length; i++)
543       {
544         if (features[i].begin == 0 && features[i].end == 0)
545         {
546           int sz = -sb.length();
547           appendFeature(sb, 0, minmax, features[i]);
548           sz += sb.length();
549           maxWidth = Math.max(maxWidth, sz);
550         }
551       }
552     }
553     sb.append("</i>");
554     return maxWidth;
555   }
556
557   public void createTooltipAnnotationReport(final StringBuilder tip,
558           SequenceI sequence, boolean showDbRefs, boolean showNpFeats,
559           Map<String, float[][]> minmax)
560   {
561     int maxWidth = createSequenceAnnotationReport(tip, sequence,
562             showDbRefs, showNpFeats, minmax, true);
563
564     if (maxWidth > 60)
565     {
566       // ? not sure this serves any useful purpose
567       // tip.insert(0, "<table width=350 border=0><tr><td>");
568       // tip.append("</td></tr></table>");
569     }
570   }
571 }