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