001    package edu.nrao.sss.astronomy;
002    
003    import java.io.BufferedReader;
004    import java.io.IOException;
005    import java.io.InputStreamReader;
006    import java.io.UnsupportedEncodingException;
007    import java.net.MalformedURLException;
008    import java.net.URL;
009    import java.net.URLEncoder;
010    import java.util.Date;
011    
012    import edu.nrao.sss.geom.EarthPosition;
013    import edu.nrao.sss.measure.Latitude;
014    import edu.nrao.sss.measure.LocalSiderealTime;
015    import edu.nrao.sss.measure.Longitude;
016    
017    /**
018     * A celestial coordinate system converter provided by NASA's
019     * High Energy Astrophysics Science Archive Research Center (HEASARC).
020     * <p>
021     * This converter sends a CGI query to HEASARC and parses the response.
022     * Coordinate converter web addresses:
023     * <a href="http://heasarc.gsfc.nasa.gov/cgi-bin/Tools/convcoord/convcoord.pl">
024     * CGI</a>,
025     * <a href="http://heasarc.gsfc.nasa.gov/Tools/convcoord_help.html">Help</a>.</p>
026     * <p>
027     * <b>Version Info:</b>
028     * <table style="margin-left:2em">
029     *   <tr><td>$Revision: 1137 $</td></tr>
030     *   <tr><td>$Date: 2008-02-29 16:06:15 -0700 (Fri, 29 Feb 2008) $</td></tr>
031     *   <tr><td>$Author: dharland $</td></tr>
032     * </table></p>
033     * 
034     * @author David M. Harland
035     * @since 2007-04-13
036     */
037    public class HeasarcCoordConverter
038      implements CelestialCoordinateConverter
039    {
040      private static final String CGI_SCRIPT =
041        "http://heasarc.gsfc.nasa.gov/cgi-bin/Tools/convcoord/convcoord.pl";
042      
043      private static final String CONST_PARAMS =
044        "?Resolver=GRB%2FSIMBAD%2FNED&NoCache=on&Epoch=&Output=Batch";
045      
046      private static final String QUERY_BASE = CGI_SCRIPT + CONST_PARAMS;
047      
048      private static final char PARAM_DELIM = '&';
049      
050      private static final char POS_DELIM = ',';
051      
052      private static final String ENCODING = "UTF-8";
053      
054      /**
055       * Returns a new position that is equivalent to {@code position},
056       * but in the given coordinate system and epoch.
057       * <p>
058       * <b>Note:</b> the success of this method depends on an active internet
059       * connection.</p>
060       * 
061       * @param position
062       *   the position to be converted.
063       *   
064       * @param toSystem
065       *   the coordinate system for the returned position.
066       *   
067       * @param toEpoch
068       *   the epoch for the returned position.
069       *   
070       * @param observer
071       *   the location of the the observer; not used by this converter.
072       *   (This converter cannot handle the <tt>HORIZONTAL</tt> coordinate system.)
073       *   
074       * @param lst
075       *   the local sidereal time at the observer's location.
076       *   Used for getting the coordinates of <tt>position</tt> at this time.
077       *   If this value is <i>null</i>, those coordinates will be calculated
078       *   as of the current time on the system clock.
079       * 
080       * @return a new position that is equivalent to {@code thisPosition},
081       *         but is expressed in a coordinate system of {@code toSystem}
082       *         for epoch {@code toEpoch}.
083       *         
084       * @throws CoordinateConversionException if anything goes wrong during
085       *           conversion.  The exception thrown will have a non-null
086       *           cause.  This is a list of the types of causes and the most
087       *           common explanation for each:
088       *           <dl>
089       *             <dt>UnsupportedEncodingException</dt>
090       *               <dd>This cause is very unlikely.  An exception of this type
091       *                   would arise if for some reason the information in
092       *                   {@code thisPosition} could not be converted into
093       *                   UTF-8</dd>
094       *             <dt>MalformedURLException</dt>
095       *               <dd>Thrown if we build the query string improperly or if
096       *                   HEASARC moves or eliminates its service.</dd>
097       *             <dt>IOException</dt>
098       *               <dd>Thrown if we have problems opening, reading, or
099       *                   closing the results of the query.</dd>
100       *             <dt>IllegalArgumentException</dt>
101       *               <dd>There are a couple of situations that lead to this
102       *                   type of cause.  The most common is if our latitude
103       *                   and/or longitude classes could not parse the query
104       *                   results.  The next most common would be calling this
105       *                   method with a {@code toSystem} and/or {@code toEpoch}
106       *                   value for which this method was not prepared.</dd>
107       *             <dt>NullPointerException</dt>
108       *               <dd>The most common situation that leads to this type of
109       *                   cause is if one or more of the parameters is
110       *                   <i>null</i>.</dd>
111       *           </dl>
112       */
113      public SkyPosition createFrom(SkyPosition               position,
114                                    CelestialCoordinateSystem toSystem,
115                                    Epoch                     toEpoch,
116                                    EarthPosition             observer,
117                                    LocalSiderealTime         lst)
118        throws CoordinateConversionException
119      {
120        SkyPosition newPosition = null;
121    
122        String queryString = null;  //Used in exception messages
123    
124        CelestialCoordinateSystem fromSystem = position.getCoordinateSystem();
125        Epoch                     fromEpoch  = position.getEpoch();
126        
127        try
128        {
129          //No need to convert
130          if (toSystem.equals(fromSystem) && toEpoch.equals(fromEpoch))
131          {
132            newPosition = SimpleSkyPosition.copy(position, lst.toDate());
133          }
134          //Call NASA's HEASARC query service
135          else
136          {
137            checkToFrom(fromSystem, fromEpoch);
138            
139            queryString = buildQuery(position, lst);
140    
141            URL heasarc = new URL(queryString);
142    
143            BufferedReader page =
144              new BufferedReader(new InputStreamReader(heasarc.openStream()));
145    
146            //First line returned has sexagesimal, which we won't use
147            page.readLine();
148            
149            //Second line has decimal positions, which we'll use
150            newPosition = makePosition(page.readLine(), toSystem, toEpoch);
151            
152            page.close();
153          }
154        }
155        catch (UnsupportedEncodingException uee)
156        {
157          throw new CoordinateConversionException(
158                      "Problem encoding position data for use in URL.", uee);
159        }
160        catch (MalformedURLException mue)
161        {
162          throw new CoordinateConversionException(
163                      "Problem with this URL: " + queryString, mue);
164        }
165        catch (IOException ioe)
166        {
167          throw new CoordinateConversionException(
168                      "Problem opening, reading, or closing query results.", ioe);
169        }
170        catch (IllegalArgumentException iae)
171        {
172          throw new CoordinateConversionException(
173                      "Problem creating position from query results.", iae);
174        }
175        catch (NullPointerException npe)
176        {
177          if (position == null || toSystem == null || toEpoch == null)
178          {
179            throw new CoordinateConversionException(
180                        "Make sure all parameters are non-null.", npe);
181          }
182          else
183          {
184            throw new CoordinateConversionException("Unexpected NPE.", npe);
185          }
186        }
187        
188        return newPosition;
189      }
190      
191      /**
192       * Returns a string that represents a query URI.
193       * The parameters of the query are based on the given sky position.
194       */
195      private String buildQuery(SkyPosition pos, LocalSiderealTime lst)
196        throws UnsupportedEncodingException
197      {
198        StringBuilder query = new StringBuilder(QUERY_BASE);
199        
200        //Coordinate Sytem or Epoch
201        query.append(PARAM_DELIM).append("CoordType=");
202        if (pos.getCoordinateSystem().equals(CelestialCoordinateSystem.EQUATORIAL))
203        {
204          query.append(URLEncoder.encode(pos.getEpoch().toString(), ENCODING));
205        }
206        else
207        {
208          query.append(URLEncoder.encode(pos.getCoordinateSystem().toString(),
209                                         ENCODING));
210        }
211        
212        //Longitude & Latitude
213        Date time = (lst == null) ? new Date() : lst.toDate();
214        
215        String posText =
216          new StringBuilder(pos.getLongitude(time).toStringHms()).append(POS_DELIM)
217                     .append(pos.getLatitude(time).toStringDms()).toString();
218        
219        query.append(PARAM_DELIM).append("CoordVal=");
220        query.append(URLEncoder.encode(posText, ENCODING));
221        
222        return query.toString();
223      }
224    
225      /**
226       * Parses heasarcLine and returns a sky position using the portion
227       * of the line that represents the given coordinate system and epoch.
228       */
229      private SkyPosition makePosition(String                    heasarcLine,
230                                       CelestialCoordinateSystem coordSys,
231                                       Epoch                     epoch)
232        throws IllegalArgumentException
233      {
234        checkToFrom(coordSys, epoch);
235        
236        SimpleSkyPosition newPos = new SimpleSkyPosition();
237        
238        newPos.setCoordinateSystem(coordSys);
239        newPos.setEpoch(epoch);
240        
241        int lonIndex = -1;
242        int latIndex = -1;
243        
244        switch (coordSys)
245        {
246          case EQUATORIAL:
247            if (epoch.equals(Epoch.J2000))
248            {
249              lonIndex = 0;
250              latIndex = 1;
251            }
252            else if (epoch.equals(Epoch.B1950))
253            {
254              lonIndex = 2;
255              latIndex = 3;
256            }
257            break;
258          
259          case GALACTIC:
260            lonIndex = 4;
261            latIndex = 5;
262            break;
263    
264          case ECLIPTIC:
265            lonIndex = 6;
266            latIndex = 7;
267            break;
268        }
269        
270        if (lonIndex < 0 || latIndex < 0)
271          throw new IllegalArgumentException(
272            "PROGRAMMER ERROR: makePosition is unprepared for coordSys=" + coordSys +
273            ", epoch=" + epoch + ", or both.");
274        
275        String[] tokens = heasarcLine.split("\\|");
276    
277        newPos.setLongitude(Longitude.parse(tokens[lonIndex]));
278        newPos.setLatitude ( Latitude.parse(tokens[latIndex]));
279        
280        return newPos;
281      }
282    
283      /** Throws exception if system or epoch can't be converted. */
284      private void checkToFrom(CelestialCoordinateSystem system, Epoch epoch)
285      {
286        switch (system)
287        {
288          case ECLIPTIC:    //intentional fall-through
289          case EQUATORIAL:  //intentional fall-through
290          case GALACTIC:
291            break;
292            
293          default:
294            throw new IllegalArgumentException(
295              "HEASARC cannot convert to or from celestial coordinate system " +
296              system);
297        }
298        
299        if (!epoch.equals(Epoch.B1950) && !epoch.equals(Epoch.J2000))
300          throw new IllegalArgumentException(
301            "HEASARC cannot convert to or from epoch" + epoch);
302      }
303      
304      //============================================================================
305      // 
306      //============================================================================
307      /*
308      public static void main(String[] args) throws Exception
309      {
310        HeasarcCoordConverter conv = new HeasarcCoordConverter();
311        
312        SkyPosition original  = new SimpleSkyPosition();
313        SkyPosition converted = conv.createFrom(original,
314                                                CelestialCoordinateSystem.GALACTIC,
315                                                Epoch.J2000);
316        System.out.println(original);
317        System.out.println(converted);
318      }
319      */
320    }