001    package edu.nrao.sss.astronomy;
002    
003    import static edu.nrao.sss.astronomy.CelestialCoordinateSystem.*;
004    
005    import java.util.ArrayList;
006    import java.util.HashMap;
007    import java.util.HashSet;
008    import java.util.List;
009    import java.util.Map;
010    import java.util.Set;
011    
012    import edu.nrao.sss.geom.EarthPosition;
013    import edu.nrao.sss.measure.LocalSiderealTime;
014    
015    /**
016     * A coordinate converter that delegates its work to other
017     * converters.
018     * <p>
019     * <b>Version Info:</b>
020     * <table style="margin-left:2em">
021     *   <tr><td>$Revision: 1147 $</td></tr>
022     *   <tr><td>$Date: 2008-03-06 08:50:11 -0700 (Thu, 06 Mar 2008) $</td></tr>
023     *   <tr><td>$Author: dharland $ (last person to modify)</td></tr>
024     * </table></p>
025     * 
026     * @author David M. Harland
027     * @since 2008-02-29
028     */
029    public class CompositeConverter
030      implements CelestialCoordinateConverter
031    {
032      private static final String FROM_TO_SEP = "->";
033      
034      //These are the component converters keyed by a string that
035      //contains the "from" and "to" systems.  See createKey method.
036      private Map<String, CelestialCoordinateConverter> converters;
037      
038      //These are chains of converters that we can use in the event
039      //that we have no direct converter.  Eg, if we have a direct
040      //converter from A->B another from B->C, we can chain these
041      //together to get an indirect conversion from A->C.
042      private Set<CcsEpochChain> converterChains;
043    
044      /**
045       * Creates a new composite converter with no component converters.
046       * @see #getDefaultConverter()
047       */
048      public CompositeConverter()
049      {
050        converters      = new HashMap<String, CelestialCoordinateConverter>();
051        converterChains = new HashSet<CcsEpochChain>();
052      }
053      
054      /**
055       * Creates a new converter that is preequipped with component converters.
056       * The component converters can handle these conversions directly:
057       * <table border="1" cellpadding="5">
058       *   <tr><th>From</th><th>To</th></tr>
059       *   <tr><td>ECLIPTIC</td><td>EQUATORIAL (J2000)</td></tr>
060       *   <tr><td>ECLIPTIC</td><td>GALACTIC</td></tr>
061       *   <tr><td>EQUATORIAL (B1950)</td><td>EQUATORIAL (J2000)</td></tr>
062       *   <tr><td>EQUATORIAL (J2000)</td><td>ECLIPTIC</td></tr>
063       *   <tr><td>EQUATORIAL (J2000)</td><td>EQUATORIAL (B1950)</td></tr>
064       *   <tr><td>EQUATORIAL (J2000)</td><td>GALACTIC</td></tr>
065       *   <tr><td>EQUATORIAL (J2000)</td><td>HORIZONTAL</td></tr>
066       *   <tr><td>GALACTIC</td><td>ECLIPTIC</td></tr>
067       *   <tr><td>GALACTIC</td><td>EQUATORIAL (J2000)</td></tr>
068       * </table>
069       * A conversion from any one of the five systems listed above can be
070       * made to any of the others, if not directly, then by chaining two
071       * conversions together.  <b>Note</b>: any conversion involving
072       * B1950 coordinates requires internet connectivity.
073       * 
074       * @return a new composite converter preequipped with component converters.
075       */
076      public static CompositeConverter getDefaultConverter()
077      {
078        CompositeConverter cc = new CompositeConverter();
079        
080        HeasarcCoordConverter         heasarc  = new HeasarcCoordConverter();
081        EquatorialHorizontalConverter eqHorz   = new EquatorialHorizontalConverter();
082        StarlinkPalConverter          starlink = new StarlinkPalConverter();
083        
084        cc.setConverter(eqHorz, EQUATORIAL, Epoch.J2000, HORIZONTAL, Epoch.J2000);
085        cc.setConverter(eqHorz, HORIZONTAL, Epoch.J2000, EQUATORIAL, Epoch.J2000);
086    
087        cc.setConverter(heasarc, EQUATORIAL, Epoch.J2000, EQUATORIAL, Epoch.B1950);
088        cc.setConverter(heasarc, EQUATORIAL, Epoch.B1950, EQUATORIAL, Epoch.J2000);
089        
090        cc.setConverter(starlink, EQUATORIAL, Epoch.J2000, GALACTIC,   Epoch.J2000);
091        cc.setConverter(starlink, EQUATORIAL, Epoch.J2000, ECLIPTIC,   Epoch.J2000);
092        cc.setConverter(starlink, ECLIPTIC,   Epoch.J2000, GALACTIC,   Epoch.J2000);
093        cc.setConverter(starlink, ECLIPTIC,   Epoch.J2000, EQUATORIAL, Epoch.J2000);
094        cc.setConverter(starlink, GALACTIC,   Epoch.J2000, EQUATORIAL, Epoch.J2000);
095        cc.setConverter(starlink, GALACTIC,   Epoch.J2000, ECLIPTIC,   Epoch.J2000);
096        
097        return cc;
098      }
099      
100      //============================================================================
101      // PERFORMING CONVERSIONS
102      //============================================================================
103      
104      public SkyPosition createFrom(SkyPosition               position,
105                                    CelestialCoordinateSystem toSystem,
106                                    Epoch                     toEpoch,
107                                    EarthPosition             observer,
108                                    LocalSiderealTime         lst)
109        throws CoordinateConversionException
110      {
111        SkyPosition convertedPosition = null;
112        
113        CcsEpoch to   = new CcsEpoch(toSystem, toEpoch);
114        CcsEpoch from = new CcsEpoch(position.getCoordinateSystem(),
115                                     position.getEpoch());
116    
117        CelestialCoordinateConverter directConverter =
118          converters.get(createKey(from, to));
119    
120        //This is the most typical case
121        if (directConverter != null)
122        {
123          convertedPosition = directConverter.createFrom(position, toSystem,
124                                                         toEpoch, observer, lst);
125        }
126        //Special logic for self-conversions.  These normally just update
127        //based on time.  Note: we do NOT handle az/el -> az/el here.
128        else if (from.equals(to) && !from.system.equals(HORIZONTAL))
129        {
130          convertedPosition = selfConversion(position, observer, lst);
131        }
132        //No direct converter and not a self conversion, unless it's az/el -> az/el
133        else
134        {
135          CcsEpochChain conversionChain = findChainFor(from, to);
136            
137          if (conversionChain != null)
138          {
139            convertedPosition = executeChain(conversionChain, position,
140                                             observer,        lst);
141          }
142          else
143          {
144            StringBuilder errMsg = new StringBuilder();
145            errMsg.append("No converter found for position (ccs=");
146            errMsg.append(position.getCoordinateSystem()).append(",epoch=");
147            errMsg.append(position.getEpoch()).append(") to ");
148            errMsg.append(toSystem).append(',').append(toEpoch).append('.');
149            throw new CoordinateConversionException(errMsg.toString(),
150                                   new IllegalArgumentException(errMsg.toString()));
151          }
152        }
153    
154        return convertedPosition;
155      }
156      
157      /**
158       * Handles conversions where the from/to CCS and Epoch are same,
159       * unless system is az/el.
160       */
161      private SkyPosition selfConversion(SkyPosition       position,
162                                         EarthPosition     observer,
163                                         LocalSiderealTime lst)
164      {
165        if (position.getCoordinateSystem().equals(HORIZONTAL))
166          throw new IllegalArgumentException("PROGRAMMER ERROR: " +
167            "This method should not have been used to convert AZ/EL to AZ/EL.");
168        
169        return SimpleSkyPosition.copy(position, lst.toDate());
170      }
171      
172      /** Executes all the conversions in the chain. */
173      public SkyPosition executeChain(CcsEpochChain     chain,
174                                      SkyPosition       position,
175                                      EarthPosition     observer,
176                                      LocalSiderealTime lst)
177        throws CoordinateConversionException
178      {
179        SkyPosition result = position;
180        
181        int linkCount = chain.links.size();
182    
183        //Do a conversion using consecutive links in the chain.
184        for (int i=1; i < linkCount; i++)
185        {
186          CcsEpoch from = chain.links.get(i-1);
187          CcsEpoch to   = chain.links.get(i);
188          
189          CelestialCoordinateConverter converter = converters.get(createKey(from,
190                                                                            to));
191          result = converter.createFrom(result, to.system, to.epoch, observer, lst);
192        }
193        
194        return result;
195      }
196      
197      /** Returns the execution chain to use, if any, for from->to. */
198      private CcsEpochChain findChainFor(CcsEpoch from, CcsEpoch to)
199      {
200        CcsEpochChain result = null;
201        int           size   = Integer.MAX_VALUE;
202        
203        //Find smallest chain that starts w/ from & ends w/ to
204        for (CcsEpochChain chain : converterChains)
205        {
206          if (chain.startsWith(from) && chain.endsWith(to))
207          {
208            if (chain.links.size() < size)
209            {
210              result = chain;
211              size = chain.links.size();
212            }
213          }
214        }
215        
216        //Do some just-in-time clean up
217        removeChains(from, to, size);
218        
219        return result;
220      }
221      
222      //============================================================================
223      // ADDING NEW CONVERTERS AND FORMING CHAINS
224      //============================================================================
225      
226      /**
227       * Sets the converter to use for the given conversion.
228       * Adding a new conversion pathway may open up other pathways.
229       * For example, if this converter already handles A->B conversions
230       * and this method adds a B->C conversion, it will also construct
231       * an A->B->C conversion pathway, unless a direct A->C pathway
232       * already exists.
233       * 
234       * @param converter
235       *   the converter to use for the given from/to system.
236       * @param fromSystem
237       *   the celestial coordinate system that the given converter
238       *   can take as input.
239       * @param fromEpoch
240       *   the epoch that the given converter can take as input.
241       *   This is usually important only for the Equatorial coordinate system.
242       * @param toSystem
243       *   the celestial coordinate system that the given converter
244       *   can produce as output.
245       * @param toEpoch
246       *   the epoch that the given converter can produce as output.
247       *   This is usually important only for the Equatorial coordinate system.
248       */
249      public void setConverter(CelestialCoordinateConverter converter,
250                               CelestialCoordinateSystem fromSystem, Epoch fromEpoch,
251                               CelestialCoordinateSystem toSystem,   Epoch toEpoch)
252      {
253        if (converter == this)
254          throw new IllegalArgumentException("Cannot add CompositeConverter to itself.");
255    
256        CcsEpoch from = new CcsEpoch(fromSystem, fromEpoch);
257        CcsEpoch to   = new CcsEpoch(toSystem,   toEpoch);
258        
259        String newKey = createKey(from, to);
260    
261        //If we already have the from/to combo, go ahead and add to the converters
262        //map, because the converter itself may be new.  However, do NOT go
263        //through the process of creating new chains, because we've already
264        //been down that road for this combo.
265        boolean newCombo = !converters.containsKey(newKey);
266        
267        converters.put(newKey, converter);  //Add the direct path BEFORE chains
268        
269        if (newCombo)
270          addToChains(from, to);
271        
272        //It's possible that we had had indirect chains for this from/to combo
273        //that we now no longer need.  While it might be harmless to keep those
274        //chains, we instead tidy up a bit.
275        removeChains(from, to, 2);
276      }
277      
278      /** Gets rid of chains that start w/ from & end w/ to. */
279      private void removeChains(CcsEpoch from, CcsEpoch to, int linkCountMin)
280      {
281        //Gather the unwanted chains here so we don't disturb the loop control
282        List<CcsEpochChain> unwantedChains = new ArrayList<CcsEpochChain>();
283    
284        for (CcsEpochChain oldChain : converterChains)
285        {
286          if (oldChain.links.size() > linkCountMin &&
287              oldChain.startsWith(from) && oldChain.endsWith(to))
288            unwantedChains.add(oldChain);
289        }
290        
291        converterChains.removeAll(unwantedChains);
292      }
293    
294      /**
295       * Tries to create new conversion pathways from the population of
296       * current pathways and the new direct from->to conversion.
297       */
298      private void addToChains(CcsEpoch from, CcsEpoch to)
299      {
300        //Add the new direct pair.  This needed so that other chains
301        //may use this as a staring point.
302        converterChains.add(new CcsEpochChain(from, to));
303    
304        //Gather the new chains here so we don't disturb the loop control
305        List<CcsEpochChain> newChains = new ArrayList<CcsEpochChain>();
306    
307        //Don't make a chain if from and to are same system
308        if (!from.equals(to))
309        {
310          for (CcsEpochChain oldChain : converterChains)
311          {
312            //It doesn't make sense for us to create new chains w/ the from->to
313            //pair inserted into the middle.  Why not?  Because we would then have
314            //a new, longer, chain w/ the exact same start and end points, and doing
315            //extra conversions is wasteful.  Thus we look to create new chains only
316            //by appending or prepending.
317    
318            //Make new chain by prepending "from" to the beginning
319            if (okToStartWith(from, to, oldChain))
320            {
321              CcsEpochChain newChain = oldChain.clone();
322              newChain.links.add(0, from);
323              newChains.add(newChain);
324            }
325            
326            //Make new chain by appending "to" to the end
327            if (okToEndWith(from, to, oldChain))
328            {
329              CcsEpochChain newChain = oldChain.clone();
330              newChain.links.add(to);
331              newChains.add(newChain);
332            }
333          }
334        }
335        
336        //Add the new indirect chains
337        for (CcsEpochChain newChain : newChains)
338        {
339          if (!converterChains.contains(newChain))
340            converterChains.add(newChain);
341        }
342      }
343    
344      /**
345       * Returns true if we can form a new chain from oldChain and have it start
346       * with from->to.
347       */
348      private boolean okToStartWith(CcsEpoch from, CcsEpoch to, CcsEpochChain oldChain)
349      {
350        //Quick exit on no match for prepending
351        if (!oldChain.startsWith(to))
352          return false;
353    
354        int chainLength = oldChain.links.size();
355        CcsEpoch lastLink = oldChain.links.get(chainLength-1);
356        
357        //Do not make new chain if we already have a direct conversion from->to
358        if (converters.containsKey(createKey(from, lastLink)))
359          return false;
360        
361        //Special logic to allow HORZ->HORZ conversions.  We roll the dice a little
362        //and assume we'll be able to do this w/ a 3-link chain.
363        if (from.system.equals(HORIZONTAL) && lastLink.system.equals(HORIZONTAL) &&
364            oldChain.links.size() == 2)
365          return true;
366        
367        //In general, do not allow self conversions --
368        //in fact, do not allow chain if the "from" element is
369        //anywhere in the chain.
370        if (oldChain.links.contains(from))
371          return false;
372        
373        return true;
374      }
375    
376      /**
377       * Returns true if we can form a new chain from oldChain and have it end
378       * with from->to.
379       */
380      private boolean okToEndWith(CcsEpoch from, CcsEpoch to, CcsEpochChain oldChain)
381      {
382        //Quick exit on no match for prepending
383        if (!oldChain.endsWith(from))
384          return false;
385    
386        CcsEpoch firstLink = oldChain.links.get(0);
387        
388        //Do not make new chain if we already have a direct conversion from->to
389        if (converters.containsKey(createKey(firstLink, to)))
390          return false;
391        
392        //Special logic to allow HORZ->HORZ conversions.  We roll the dice a little
393        //and assume we'll be able to do this w/ a 3-link chain.
394        if (firstLink.system.equals(HORIZONTAL) && to.system.equals(HORIZONTAL) &&
395            oldChain.links.size() == 2)
396          return true;
397        
398        //In general, do not allow self conversions --
399        //in fact, do not allow chain if the "to" element is
400        //anywhere in the chain.
401        if (oldChain.links.contains(to))
402          return false;
403        
404        return true;
405      }
406      
407      /** Used for keeping map of converters. */
408      private String createKey(CcsEpoch from, CcsEpoch to)
409      {
410        return from.toString() + FROM_TO_SEP + to.toString();
411      }
412      
413      //============================================================================
414      // PRIVATE HELPER CLASSES
415      //============================================================================
416      
417      /** Marries a CelestialCoordinateSystem and an Epoch. */
418      private class CcsEpoch
419      {
420        CelestialCoordinateSystem system;
421        Epoch                     epoch;
422        
423        CcsEpoch(CelestialCoordinateSystem ccs, Epoch e)
424        {
425          system = ccs;
426          epoch  = e;
427        }
428        
429        public String toString()
430        {
431          String text = system.name();
432          
433          if (system.equals(EQUATORIAL))
434            text = text + "." + epoch.name();
435          
436          return text;
437        }
438        
439        public boolean equals(Object o)
440        {
441          CcsEpoch other = (CcsEpoch)o;
442          return other.system.equals(this.system) &&
443                 other.epoch.equals(this.epoch);
444        }
445      }
446      
447        
448      private class CcsEpochChain implements Cloneable
449      {
450        List<CcsEpoch> links = new ArrayList<CcsEpoch>();
451        
452        CcsEpochChain() { }
453        
454        CcsEpochChain(CcsEpoch link0, CcsEpoch link1)
455        {
456          links.add(link0);
457          links.add(link1);
458        }
459    
460        boolean startsWith(CcsEpoch link)
461        {
462          return links.get(0).equals(link);
463        }
464        
465        boolean endsWith(CcsEpoch link)
466        {
467          return links.get(links.size()-1).equals(link);
468        }
469        
470        public boolean equals(Object o)
471        {
472          return ((CcsEpochChain)o).links.equals(this.links);
473        }
474        
475        public CcsEpochChain clone()
476        {
477          CcsEpochChain clone = null;
478          
479          try
480          {
481            clone = (CcsEpochChain)super.clone();
482            //We don't need to clone the elements
483            clone.links = new ArrayList<CcsEpoch>(links);
484          }
485          catch (Exception ex)
486          {
487            throw new RuntimeException(ex);
488          }
489          
490          return clone;
491        }
492        
493        public String toString()  { return links.toString(); }
494      }
495      
496      //============================================================================
497      // 
498      //============================================================================
499      /*
500      public static void main(String... args) throws Exception
501      {
502        CompositeConverter cc = CompositeConverter.getDefaultConverter();
503        
504        LocalSiderealTime lst = new LocalSiderealTime();
505        EarthPosition observer = new EarthPosition(lst.getLocation(),
506                                                   Latitude.parse("34d 04' 43.497\""),
507                                                   lst.getTimeZone());
508        
509        SkyPosition eqPos = new SimpleSkyPosition(EQUATORIAL, Epoch.J2000);
510        ((SimpleSkyPosition)eqPos).setLatitude(new Latitude(50.0));
511        ((SimpleSkyPosition)eqPos).setLongitude(new Longitude(60.0));
512        displayPosition(eqPos);
513        
514        SkyPosition glPos = cc.createFrom(eqPos, GALACTIC, Epoch.J2000, observer, lst);
515        displayPosition(glPos);
516        
517        SkyPosition hzPos = cc.createFrom(eqPos, HORIZONTAL, Epoch.J2000, observer, lst);
518        displayPosition(hzPos);
519        
520        SkyPosition hzPos2 = cc.createFrom(glPos, HORIZONTAL, Epoch.J2000, observer, lst);
521        displayPosition(hzPos2);
522        
523        SkyPosition eqPos2 = cc.createFrom(hzPos, EQUATORIAL, Epoch.J2000, observer, lst);
524        displayPosition(eqPos2);
525        
526        SkyPosition eqPos3 = cc.createFrom(glPos, EQUATORIAL, Epoch.J2000, observer, lst);
527        displayPosition(eqPos3);
528        
529        SkyPosition eqPos4 = cc.createFrom(hzPos2, EQUATORIAL, Epoch.J2000, observer, lst);
530        displayPosition(eqPos4);
531      }
532      private static void displayPosition(SkyPosition pos)
533      {
534        System.out.print(pos.getCoordinateSystem());
535        if (pos.getCoordinateSystem().equals(EQUATORIAL))
536          System.out.print("-"+pos.getEpoch());
537        System.out.print(", ");
538        System.out.print(pos.getLongitude().toStringHms());
539        System.out.print(", ");
540        System.out.print(pos.getLatitude().toStringDms());
541        System.out.println();
542      }
543      */
544      /*
545      public static void main(String... args) throws Exception
546      {
547        CompositeConverter cc = new CompositeConverter();
548        
549        HeasarcCoordConverter         heasarc  = new HeasarcCoordConverter();
550        EquatorialHorizontalConverter eqHorz   = new EquatorialHorizontalConverter();
551        StarlinkPalConverter          starlink = new StarlinkPalConverter();
552        
553        cc.setConverter(eqHorz, EQUATORIAL, Epoch.J2000, HORIZONTAL, Epoch.J2000);
554        System.out.println(cc.converterChains);
555        cc.setConverter(eqHorz, HORIZONTAL, Epoch.J2000, EQUATORIAL, Epoch.J2000);
556        System.out.println(cc.converterChains);
557    
558        cc.setConverter(heasarc, EQUATORIAL, Epoch.J2000, EQUATORIAL, Epoch.B1950);
559        System.out.println(cc.converterChains);
560        cc.setConverter(heasarc, EQUATORIAL, Epoch.B1950, EQUATORIAL, Epoch.J2000);
561        System.out.println(cc.converterChains);
562        
563        cc.setConverter(starlink, EQUATORIAL, Epoch.J2000, GALACTIC, Epoch.J2000);
564        System.out.println(cc.converterChains);
565        cc.setConverter(starlink, EQUATORIAL, Epoch.J2000, ECLIPTIC, Epoch.J2000);
566        System.out.println(cc.converterChains);
567        cc.setConverter(starlink, ECLIPTIC, Epoch.J2000, GALACTIC, Epoch.J2000);
568        System.out.println(cc.converterChains);
569        cc.setConverter(starlink, ECLIPTIC, Epoch.J2000, EQUATORIAL, Epoch.J2000);
570        System.out.println(cc.converterChains);
571        cc.setConverter(starlink, GALACTIC, Epoch.J2000, EQUATORIAL, Epoch.J2000);
572        System.out.println(cc.converterChains);
573        cc.setConverter(starlink, GALACTIC, Epoch.J2000, ECLIPTIC, Epoch.J2000);
574        System.out.println(cc.converterChains);
575        
576        cc.findChainFor(cc.new CcsEpoch(HORIZONTAL, Epoch.J2000),
577                        cc.new CcsEpoch(ECLIPTIC, Epoch.J2000));
578        System.out.println("\n"+cc.converterChains);
579      }
580      */
581    }