JAL-2418 source formatting
[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.MessageManager;
29 import jalview.util.UrlLink;
30
31 import java.util.Arrays;
32 import java.util.Collection;
33 import java.util.Comparator;
34 import java.util.LinkedHashMap;
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
81               : (s2 == null ? 1 : s1.compareToIgnoreCase(s2));
82       if (comp == 0)
83       {
84         String a1 = ref1.getAccessionId();
85         String a2 = ref2.getAccessionId();
86         comp = a1 == null ? -1
87                 : (a2 == null ? 1 : a1.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     if (feature.isContactFeature())
145     {
146       if (feature.getBegin() == rpos || feature.getEnd() == rpos)
147       {
148         if (sb.length() > 6)
149         {
150           sb.append("<br>");
151         }
152         sb.append(feature.getType()).append(" ").append(feature.getBegin())
153                 .append(":").append(feature.getEnd());
154       }
155     }
156     else
157     {
158       if (sb.length() > 6)
159       {
160         sb.append("<br>");
161       }
162       // TODO: remove this hack to display link only features
163       boolean linkOnly = feature.getValue("linkonly") != null;
164       if (!linkOnly)
165       {
166         sb.append(feature.getType()).append(" ");
167         if (rpos != 0)
168         {
169           // we are marking a positional feature
170           sb.append(feature.begin);
171         }
172         if (feature.begin != feature.end)
173         {
174           sb.append(" ").append(feature.end);
175         }
176
177         if (feature.getDescription() != null
178                 && !feature.description.equals(feature.getType()))
179         {
180           String tmpString = feature.getDescription();
181           String tmp2up = tmpString.toUpperCase();
182           int startTag = tmp2up.indexOf("<HTML>");
183           if (startTag > -1)
184           {
185             tmpString = tmpString.substring(startTag + 6);
186             tmp2up = tmp2up.substring(startTag + 6);
187           }
188           int endTag = tmp2up.indexOf("</BODY>");
189           if (endTag > -1)
190           {
191             tmpString = tmpString.substring(0, endTag);
192             tmp2up = tmp2up.substring(0, endTag);
193           }
194           endTag = tmp2up.indexOf("</HTML>");
195           if (endTag > -1)
196           {
197             tmpString = tmpString.substring(0, endTag);
198           }
199
200           if (startTag > -1)
201           {
202             sb.append("; ").append(tmpString);
203           }
204           else
205           {
206             if (tmpString.indexOf("<") > -1 || tmpString.indexOf(">") > -1)
207             {
208               // The description does not specify html is to
209               // be used, so we must remove < > symbols
210               tmpString = tmpString.replaceAll("<", "&lt;");
211               tmpString = tmpString.replaceAll(">", "&gt;");
212
213               sb.append("; ");
214               sb.append(tmpString);
215             }
216             else
217             {
218               sb.append("; ").append(tmpString);
219             }
220           }
221         }
222         // check score should be shown
223         if (!Float.isNaN(feature.getScore()))
224         {
225           float[][] rng = (minmax == null) ? null
226                   : minmax.get(feature.getType());
227           if (rng != null && rng[0] != null && rng[0][0] != rng[0][1])
228           {
229             sb.append(" Score=").append(String.valueOf(feature.getScore()));
230           }
231         }
232         String status = (String) feature.getValue("status");
233         if (status != null && status.length() > 0)
234         {
235           sb.append("; (").append(status).append(")");
236         }
237         String clinSig = (String) feature
238                 .getValue(GffConstants.CLINICAL_SIGNIFICANCE);
239         if (clinSig != null)
240         {
241           sb.append("; ").append(clinSig);
242         }
243       }
244     }
245   }
246
247   /**
248    * Format and appends any hyperlinks for the sequence feature to the string
249    * buffer
250    * 
251    * @param sb
252    * @param feature
253    */
254   void appendLinks(final StringBuffer sb, SequenceFeature feature)
255   {
256     if (feature.links != null)
257     {
258       if (linkImageURL != null)
259       {
260         sb.append(" <img src=\"" + linkImageURL + "\">");
261       }
262       else
263       {
264         for (String urlstring : feature.links)
265         {
266           try
267           {
268             for (List<String> urllink : createLinksFrom(null, urlstring))
269             {
270               sb.append("<br/> <a href=\"" + urllink.get(3) + "\" target=\""
271                       + urllink.get(0) + "\">"
272                       + (urllink.get(0).toLowerCase()
273                               .equals(urllink.get(1).toLowerCase())
274                                       ? urllink.get(0)
275                                       : (urllink.get(0) + ":"
276                                               + urllink.get(1)))
277                       + "</a></br>");
278             }
279           } catch (Exception x)
280           {
281             System.err.println(
282                     "problem when creating links from " + urlstring);
283             x.printStackTrace();
284           }
285         }
286       }
287
288     }
289   }
290
291   /**
292    * 
293    * @param seq
294    * @param link
295    * @return Collection< List<String> > { List<String> { link target, link
296    *         label, dynamic component inserted (if any), url }}
297    */
298   Collection<List<String>> createLinksFrom(SequenceI seq, String link)
299   {
300     Map<String, List<String>> urlSets = new LinkedHashMap<String, List<String>>();
301     UrlLink urlLink = new UrlLink(link);
302     if (!urlLink.isValid())
303     {
304       System.err.println(urlLink.getInvalidMessage());
305       return null;
306     }
307
308     urlLink.createLinksFromSeq(seq, urlSets);
309
310     return urlSets.values();
311   }
312
313   public void createSequenceAnnotationReport(final StringBuilder tip,
314           SequenceI sequence, boolean showDbRefs, boolean showNpFeats,
315           Map<String, float[][]> minmax)
316   {
317     createSequenceAnnotationReport(tip, sequence, showDbRefs, showNpFeats,
318             minmax, false);
319   }
320
321   /**
322    * Builds an html formatted report of sequence details and appends it to the
323    * provided buffer.
324    * 
325    * @param sb
326    *          buffer to append report to
327    * @param sequence
328    *          the sequence the report is for
329    * @param showDbRefs
330    *          whether to include database references for the sequence
331    * @param showNpFeats
332    *          whether to include non-positional sequence features
333    * @param minmax
334    * @param summary
335    * @return
336    */
337   int createSequenceAnnotationReport(final StringBuilder sb,
338           SequenceI sequence, boolean showDbRefs, boolean showNpFeats,
339           Map<String, float[][]> minmax, boolean summary)
340   {
341     String tmp;
342     sb.append("<i>");
343
344     int maxWidth = 0;
345     if (sequence.getDescription() != null)
346     {
347       tmp = sequence.getDescription();
348       sb.append("<br>").append(tmp);
349       maxWidth = Math.max(maxWidth, tmp.length());
350     }
351     SequenceI ds = sequence;
352     while (ds.getDatasetSequence() != null)
353     {
354       ds = ds.getDatasetSequence();
355     }
356     DBRefEntry[] dbrefs = ds.getDBRefs();
357     if (showDbRefs && dbrefs != null)
358     {
359       // note this sorts the refs held on the sequence!
360       Arrays.sort(dbrefs, comparator);
361       boolean ellipsis = false;
362       String source = null;
363       String lastSource = null;
364       int countForSource = 0;
365       int sourceCount = 0;
366       boolean moreSources = false;
367       int lineLength = 0;
368
369       for (DBRefEntry ref : dbrefs)
370       {
371         source = ref.getSource();
372         if (source == null)
373         {
374           // shouldn't happen
375           continue;
376         }
377         boolean sourceChanged = !source.equals(lastSource);
378         if (sourceChanged)
379         {
380           lineLength = 0;
381           countForSource = 0;
382           sourceCount++;
383         }
384         if (sourceCount > MAX_SOURCES && summary)
385         {
386           ellipsis = true;
387           moreSources = true;
388           break;
389         }
390         lastSource = source;
391         countForSource++;
392         if (countForSource == 1 || !summary)
393         {
394           sb.append("<br>");
395         }
396         if (countForSource <= MAX_REFS_PER_SOURCE || !summary)
397         {
398           String accessionId = ref.getAccessionId();
399           lineLength += accessionId.length() + 1;
400           if (countForSource > 1 && summary)
401           {
402             sb.append(", ").append(accessionId);
403             lineLength++;
404           }
405           else
406           {
407             sb.append(source).append(" ").append(accessionId);
408             lineLength += source.length();
409           }
410           maxWidth = Math.max(maxWidth, lineLength);
411         }
412         if (countForSource == MAX_REFS_PER_SOURCE && summary)
413         {
414           sb.append(COMMA).append(ELLIPSIS);
415           ellipsis = true;
416         }
417       }
418       if (moreSources)
419       {
420         sb.append("<br>").append(ELLIPSIS).append(COMMA).append(source)
421                 .append(COMMA).append(ELLIPSIS);
422       }
423       if (ellipsis)
424       {
425         sb.append("<br>(");
426         sb.append(MessageManager.getString("label.output_seq_details"));
427         sb.append(")");
428       }
429     }
430
431     /*
432      * add non-positional features if wanted
433      */
434     SequenceFeature[] features = sequence.getSequenceFeatures();
435     if (showNpFeats && features != null)
436     {
437       for (int i = 0; i < features.length; i++)
438       {
439         if (features[i].begin == 0 && features[i].end == 0)
440         {
441           int sz = -sb.length();
442           appendFeature(sb, 0, minmax, features[i]);
443           sz += sb.length();
444           maxWidth = Math.max(maxWidth, sz);
445         }
446       }
447     }
448     sb.append("</i>");
449     return maxWidth;
450   }
451
452   public void createTooltipAnnotationReport(final StringBuilder tip,
453           SequenceI sequence, boolean showDbRefs, boolean showNpFeats,
454           Map<String, float[][]> minmax)
455   {
456     int maxWidth = createSequenceAnnotationReport(tip, sequence, showDbRefs,
457             showNpFeats, minmax, true);
458
459     if (maxWidth > 60)
460     {
461       // ? not sure this serves any useful purpose
462       // tip.insert(0, "<table width=350 border=0><tr><td>");
463       // tip.append("</td></tr></table>");
464     }
465   }
466 }