Merge branch 'documentation/JAL-3407_2.11.1_release' into releases/Release_2_11_1_Branch
[jalview.git] / src / jalview / ext / ensembl / EnsemblMap.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.ext.ensembl;
22
23 import jalview.datamodel.AlignmentI;
24 import jalview.datamodel.DBRefSource;
25 import jalview.datamodel.GeneLociI;
26 import jalview.datamodel.GeneLocus;
27 import jalview.datamodel.Mapping;
28 import jalview.util.MapList;
29
30 import java.io.BufferedReader;
31 import java.io.IOException;
32 import java.net.MalformedURLException;
33 import java.net.URL;
34 import java.util.ArrayList;
35 import java.util.Collections;
36 import java.util.Iterator;
37 import java.util.List;
38
39 import org.json.simple.JSONArray;
40 import org.json.simple.JSONObject;
41 import org.json.simple.parser.JSONParser;
42 import org.json.simple.parser.ParseException;
43
44 /**
45  * A client for the Ensembl REST service /map endpoint, to convert from
46  * coordinates of one genome assembly to another.
47  * <p>
48  * Note that species and assembly identifiers passed to this class must be valid
49  * in Ensembl. They are not case sensitive.
50  * 
51  * @author gmcarstairs
52  * @see https://rest.ensembl.org/documentation/info/assembly_map
53  * @see https://rest.ensembl.org/info/assembly/human?content-type=text/xml
54  * @see https://rest.ensembl.org/info/species?content-type=text/xml
55  */
56 public class EnsemblMap extends EnsemblRestClient
57 {
58   private static final String MAPPED = "mapped";
59
60   private static final String MAPPINGS = "mappings";
61
62   private static final String CDS = "cds";
63
64   private static final String CDNA = "cdna";
65
66   /**
67    * Default constructor (to use rest.ensembl.org)
68    */
69   public EnsemblMap()
70   {
71     super();
72   }
73
74   /**
75    * Constructor given the target domain to fetch data from
76    * 
77    * @param
78    */
79   public EnsemblMap(String domain)
80   {
81     super(domain);
82   }
83
84   @Override
85   public String getDbName()
86   {
87     return DBRefSource.ENSEMBL;
88   }
89
90   @Override
91   public AlignmentI getSequenceRecords(String queries) throws Exception
92   {
93     return null; // not used
94   }
95
96   /**
97    * Constructs a URL of the format <code>
98    * http://rest.ensembl.org/map/human/GRCh38/17:45051610..45109016:1/GRCh37?content-type=application/json
99    * </code>
100    * 
101    * @param species
102    * @param chromosome
103    * @param fromRef
104    * @param toRef
105    * @param startPos
106    * @param endPos
107    * @return
108    * @throws MalformedURLException
109    */
110   protected URL getAssemblyMapUrl(String species, String chromosome, String fromRef,
111           String toRef, int startPos, int endPos)
112           throws MalformedURLException
113   {
114     /*
115      * start-end might be reverse strand - present forwards to the service
116      */
117     boolean forward = startPos <= endPos;
118     int start = forward ? startPos : endPos;
119     int end = forward ? endPos : startPos;
120     String strand = forward ? "1" : "-1";
121     String url = String.format(
122             "%s/map/%s/%s/%s:%d..%d:%s/%s?content-type=application/json",
123             getDomain(), species, fromRef, chromosome, start, end, strand,
124             toRef);
125     return new URL(url);
126   }
127
128   @Override
129   protected boolean useGetRequest()
130   {
131     return true;
132   }
133
134   @Override
135   protected URL getUrl(List<String> ids) throws MalformedURLException
136   {
137     return null; // not used
138   }
139
140   /**
141    * Calls the REST /map service to get the chromosomal coordinates (start/end)
142    * in 'toRef' that corresponding to the (start/end) queryRange in 'fromRef'
143    * 
144    * @param species
145    * @param chromosome
146    * @param fromRef
147    * @param toRef
148    * @param queryRange
149    * @return
150    * @see http://rest.ensemblgenomes.org/documentation/info/assembly_map
151    */
152   public int[] getAssemblyMapping(String species, String chromosome,
153           String fromRef, String toRef, int[] queryRange)
154   {
155     URL url = null;
156     BufferedReader br = null;
157
158     try
159     {
160       url = getAssemblyMapUrl(species, chromosome, fromRef, toRef, queryRange[0],
161               queryRange[1]);
162       br = getHttpResponse(url, null);
163       return (parseAssemblyMappingResponse(br));
164     } catch (Throwable t)
165     {
166       System.out.println("Error calling " + url + ": " + t.getMessage());
167       return null;
168     } finally
169     {
170       if (br != null)
171       {
172         try
173         {
174           br.close();
175         } catch (IOException e)
176         {
177           // ignore
178         }
179       }
180     }
181   }
182
183   /**
184    * Parses the JSON response from the /map/&lt;species&gt;/ REST service. The
185    * format is (with some fields omitted)
186    * 
187    * <pre>
188    *  {"mappings": 
189    *    [{
190    *       "original": {"end":45109016,"start":45051610},
191    *       "mapped"  : {"end":43186384,"start":43128978} 
192    *  }] }
193    * </pre>
194    * 
195    * @param br
196    * @return
197    */
198   protected int[] parseAssemblyMappingResponse(BufferedReader br)
199   {
200     int[] result = null;
201     JSONParser jp = new JSONParser();
202
203     try
204     {
205       JSONObject parsed = (JSONObject) jp.parse(br);
206       JSONArray mappings = (JSONArray) parsed.get(MAPPINGS);
207
208       Iterator rvals = mappings.iterator();
209       while (rvals.hasNext())
210       {
211         // todo check for "mapped"
212         JSONObject val = (JSONObject) rvals.next();
213         JSONObject mapped = (JSONObject) val.get(MAPPED);
214         int start = Integer.parseInt(mapped.get("start").toString());
215         int end = Integer.parseInt(mapped.get("end").toString());
216         String strand = mapped.get("strand").toString();
217         if ("1".equals(strand))
218         {
219           result = new int[] { start, end };
220         }
221         else
222         {
223           result = new int[] { end, start };
224         }
225       }
226     } catch (IOException | ParseException | NumberFormatException e)
227     {
228       // ignore
229     }
230     return result;
231   }
232
233   /**
234    * Calls the REST /map/cds/id service, and returns a DBRefEntry holding the
235    * returned chromosomal coordinates, or returns null if the call fails
236    * 
237    * @param division
238    *          e.g. Ensembl, EnsemblMetazoa
239    * @param accession
240    *          e.g. ENST00000592782, Y55B1AR.1.1
241    * @param start
242    * @param end
243    * @return
244    */
245   public GeneLociI getCdsMapping(String division, String accession,
246           int start, int end)
247   {
248     return getIdMapping(division, accession, start, end, CDS);
249   }
250
251   /**
252    * Calls the REST /map/cdna/id service, and returns a DBRefEntry holding the
253    * returned chromosomal coordinates, or returns null if the call fails
254    * 
255    * @param division
256    *          e.g. Ensembl, EnsemblMetazoa
257    * @param accession
258    *          e.g. ENST00000592782, Y55B1AR.1.1
259    * @param start
260    * @param end
261    * @return
262    */
263   public GeneLociI getCdnaMapping(String division, String accession,
264           int start, int end)
265   {
266     return getIdMapping(division, accession, start, end, CDNA);
267   }
268
269   GeneLociI getIdMapping(String division, String accession, int start,
270           int end, String cdsOrCdna)
271   {
272     URL url = null;
273     BufferedReader br = null;
274
275     try
276     {
277       String domain = new EnsemblInfo().getDomain(division);
278       if (domain != null)
279       {
280         url = getIdMapUrl(domain, accession, start, end, cdsOrCdna);
281         br = getHttpResponse(url, null);
282         if (br != null)
283         {
284           return (parseIdMappingResponse(br, accession, domain));
285         }
286       }
287       return null;
288     } catch (Throwable t)
289     {
290       System.out.println("Error calling " + url + ": " + t.getMessage());
291       return null;
292     } finally
293     {
294       if (br != null)
295       {
296         try
297         {
298           br.close();
299         } catch (IOException e)
300         {
301           // ignore
302         }
303       }
304     }
305   }
306
307   /**
308    * Constructs a URL to the /map/cds/<id> or /map/cdna/<id> REST service. The
309    * REST call is to either ensembl or ensemblgenomes, as determined from the
310    * division, e.g. Ensembl or EnsemblProtists.
311    * 
312    * @param domain
313    * @param accession
314    * @param start
315    * @param end
316    * @param cdsOrCdna
317    * @return
318    * @throws MalformedURLException
319    */
320   URL getIdMapUrl(String domain, String accession, int start, int end,
321           String cdsOrCdna) throws MalformedURLException
322   {
323     String url = String
324             .format("%s/map/%s/%s/%d..%d?include_original_region=1&content-type=application/json",
325                     domain, cdsOrCdna, accession, start, end);
326     return new URL(url);
327   }
328
329   /**
330    * Parses the JSON response from the /map/cds/ or /map/cdna REST service. The
331    * format is
332    * 
333    * <pre>
334    * {"mappings":
335    *   [
336    *    {"assembly_name":"TAIR10","end":2501311,"seq_region_name":"1","gap":0,
337    *     "strand":-1,"coord_system":"chromosome","rank":0,"start":2501114},
338    *    {"assembly_name":"TAIR10","end":2500815,"seq_region_name":"1","gap":0,
339    *     "strand":-1,"coord_system":"chromosome","rank":0,"start":2500714}
340    *   ]
341    * }
342    * </pre>
343    * 
344    * @param br
345    * @param accession
346    * @param domain
347    * @return
348    */
349   GeneLociI parseIdMappingResponse(BufferedReader br, String accession,
350           String domain)
351   {
352     JSONParser jp = new JSONParser();
353
354     try
355     {
356       JSONObject parsed = (JSONObject) jp.parse(br);
357       JSONArray mappings = (JSONArray) parsed.get(MAPPINGS);
358
359       Iterator rvals = mappings.iterator();
360       String assembly = null;
361       String chromosome = null;
362       int fromEnd = 0;
363       List<int[]> regions = new ArrayList<>();
364
365       while (rvals.hasNext())
366       {
367         JSONObject val = (JSONObject) rvals.next();
368         JSONObject original = (JSONObject) val.get("original");
369         fromEnd = Integer.parseInt(original.get("end").toString());
370
371         JSONObject mapped = (JSONObject) val.get(MAPPED);
372         int start = Integer.parseInt(mapped.get("start").toString());
373         int end = Integer.parseInt(mapped.get("end").toString());
374         String ass = mapped.get("assembly_name").toString();
375         if (assembly != null && !assembly.equals(ass))
376         {
377           System.err
378                   .println("EnsemblMap found multiple assemblies - can't resolve");
379           return null;
380         }
381         assembly = ass;
382         String chr = mapped.get("seq_region_name").toString();
383         if (chromosome != null && !chromosome.equals(chr))
384         {
385           System.err
386                   .println("EnsemblMap found multiple chromosomes - can't resolve");
387           return null;
388         }
389         chromosome = chr;
390         String strand = mapped.get("strand").toString();
391         if ("-1".equals(strand))
392         {
393           regions.add(new int[] { end, start });
394         }
395         else
396         {
397           regions.add(new int[] { start, end });
398         }
399       }
400
401       /*
402        * processed all mapped regions on chromosome, assemble the result,
403        * having first fetched the species id for the accession
404        */
405       final String species = new EnsemblLookup(domain)
406               .getSpecies(accession);
407       final String as = assembly;
408       final String chr = chromosome;
409       List<int[]> fromRange = Collections.singletonList(new int[] { 1,
410           fromEnd });
411       Mapping mapping = new Mapping(new MapList(fromRange, regions, 1, 1));
412       return new GeneLocus(species == null ? "" : species, as, chr,
413               mapping);
414     } catch (IOException | ParseException | NumberFormatException e)
415     {
416       // ignore
417     }
418
419     return null;
420   }
421
422 }