001    package edu.nrao.sss.model.source.sort;
002    
003    import java.util.Comparator;
004    import java.util.Date;
005    import java.util.HashMap;
006    import java.util.Map;
007    import java.util.TimeZone;
008    
009    import org.apache.log4j.Logger;
010    
011    import edu.nrao.sss.astronomy.CelestialCoordinateSystem;
012    import edu.nrao.sss.astronomy.CoordinateConversionException;
013    import edu.nrao.sss.astronomy.Epoch;
014    import edu.nrao.sss.astronomy.SimpleSkyPosition;
015    import edu.nrao.sss.astronomy.SkyPosition;
016    import edu.nrao.sss.geom.EarthPosition;
017    import edu.nrao.sss.measure.Angle;
018    import edu.nrao.sss.measure.ArcUnits;
019    import edu.nrao.sss.measure.Latitude;
020    import edu.nrao.sss.measure.LocalSiderealTime;
021    import edu.nrao.sss.measure.Longitude;
022    import edu.nrao.sss.model.resource.TelescopeType;
023    import edu.nrao.sss.model.source.Source;
024    import edu.nrao.sss.model.source.SourceCatalogEntry;
025    import edu.nrao.sss.sort.DoubleSortKey;
026    import edu.nrao.sss.sort.Orderable;
027    import edu.nrao.sss.sort.SortOrder;
028    
029    /**
030     * Compares {@link Source sources} based on their proximity to a given point
031     * in the sky.
032     * <p>
033     * <b>Version Info:</b>
034     * <table style="margin-left:2em">
035     *   <tr><td>$Revision: 1617 $</td></tr>
036     *   <tr><td>$Date: 2008-10-10 16:52:33 -0600 (Fri, 10 Oct 2008) $</td></tr>
037     *   <tr><td>$Author: dharland $ (last person to modify)</td></tr>
038     * </table></p>
039     * 
040     * @author David M. Harland
041     * @since 2008-10-08
042     */
043    public class SourceProximitySortKey
044      implements Orderable, Comparator<SourceCatalogEntry>
045    {
046      private static final Logger log = Logger.getLogger(SourceProximitySortKey.class);
047      
048      private static final EarthPosition VLA_LOCATION  = TelescopeType.EVLA.getLocation();
049      private static final TimeZone      VLA_TIME_ZONE = TimeZone.getTimeZone("US/Mountain");
050    
051      //Client-settable properties
052      private SkyPosition   centralPosition;
053      private EarthPosition observerLocation;
054      private TimeZone      observerTimeZone;
055      private Date          requestedTime;
056      
057      //Derived from client-settable properties
058      private Date                      queryTime;
059      private LocalSiderealTime         queryLst;
060      private CelestialCoordinateSystem cpCoordSys;
061      private Epoch                     cpEpoch;
062      private Latitude                  cpLatitude;
063      private Longitude                 cpLongitude;
064      
065      private DegreesSorter sorter;
066    
067      //DMH did a test sorting ~450 sources.  Using a map whose keys were
068      //SCEs took ~5s; using a map w/ String keys took ~0.75s.
069      //We therefore try to use String map, but since two diff sources
070      //could have same name, we sometimes go to slow map.
071      
072      //This map will hold only those SCEs that have the same name as another
073      //SCE in the sort.  If there are n SCEs with the same name, one will
074      //be in the fast map and the other n-1 will be here.
075      private Map<SourceCatalogEntry, Double> proximitiesSlow;
076      
077      //The first SCE seen with a given name will be held here.
078      //The SCE name is used as the key.
079      private Map<String, Double> proximitiesFast;
080      
081      //This map tell us whether the proximity for a given SCE is in the fast
082      //or slow map.  For a given name, the SCE returned as a value by this
083      //map will have its proximity in the fast map.  Any other SCEs with that
084      //name will have their proximities held in the slow map.
085      private Map<String, SourceCatalogEntry> fastMapEntries;
086      
087      /**
088       * Tells this key to get position information as of the current system time.
089       */
090      public static final Date NOW = (Date)null;
091      
092      /**
093       * Creates a new key for sorting sources by their proximity to a particular
094       * sky position.
095       */
096      public SourceProximitySortKey()
097      {
098        proximitiesSlow = new HashMap<SourceCatalogEntry, Double>();
099        proximitiesFast = new HashMap<String, Double>();
100        fastMapEntries  = new HashMap<String, SourceCatalogEntry>();
101        
102        sorter = new DegreesSorter();
103    
104        centralPosition = new SimpleSkyPosition();
105        
106        observerLocation = VLA_LOCATION;
107        observerTimeZone = VLA_TIME_ZONE;
108        
109        requestedTime = NOW;
110        queryTime     = new Date();
111        queryLst      = new LocalSiderealTime(queryTime, VLA_LOCATION.getLongitude(),
112                                              VLA_TIME_ZONE);
113        updateCentralPositionVariables();
114      }
115      
116      /**
117       * Returns the angular separation, in degrees, between {@code sce}
118       * and the {@link #setCentralPosition(SkyPosition) central position}
119       * of this object.
120       * 
121       * @param sce
122       *   a celestial source.
123       * 
124       * @return
125       *   the angular separation between {@code source} and this object's
126       *   central sky position.
127       */
128      public double getSeparationInDegrees(SourceCatalogEntry sce)
129      {
130        Double degrees = getPrecalculatedDegrees(sce);
131        
132        if (degrees == null)
133          degrees = calculateAndUpdateSeparation(sce);
134        
135        return degrees == null ? 180.0 : degrees;
136      }
137      
138      private Double getPrecalculatedDegrees(SourceCatalogEntry sce)
139      {
140        Double degrees = null;
141        
142        String             name         = sce.getName();
143        SourceCatalogEntry fastMapEntry = fastMapEntries.get(name);
144    
145        //If the returned SCE is not null, we've seen an SCE w/ the same name
146        //as the one sent to this method.  It may or may not be the same object.
147        if (fastMapEntry != null)
148        {
149          //If fastMapEntry equals sce, it means its proximity value is
150          //in the fast map.  If not equal, then either it is in the
151          //slow map, or it is the first time we've seen this sce
152          //(which happens to have the same name as one or more that
153          //we've already seen).
154          degrees = fastMapEntry.equals(sce) ? proximitiesFast.get(name)
155                                             : proximitiesSlow.get(sce);
156        }
157        
158        return degrees;
159      }
160      
161      /**
162       * Sets the point in the sky from which all other sources will be measured.
163       * 
164       * @param newPoint
165       *   a point in the sky.  Sources will be compared to each other based
166       *   on their angular distance from this point.  A value of <i>null</i>
167       *   is not allowed.
168       * 
169       * @throws IllegalArgumentException
170       *   if {@code newPoint} is <i>null</i>.
171       */
172      public void setCentralPosition(SkyPosition newPoint)
173      {
174        //Do not accept null position
175        if (newPoint == null)
176          throw new IllegalArgumentException("Central sky position may not be null.");
177     
178        centralPosition = newPoint;
179        updateCentralPositionVariables();
180        compareTime = 0L;
181        proximitiesFast.clear();
182        proximitiesSlow.clear();
183        fastMapEntries.clear();
184      }
185      
186      private void updateCentralPositionVariables()
187      {
188        cpCoordSys  = centralPosition.getCoordinateSystem();
189        cpEpoch     = centralPosition.getEpoch();
190        cpLatitude  = centralPosition.getLatitude(queryTime);
191        cpLongitude = centralPosition.getLongitude(queryTime);
192      }
193      
194      /**
195       * Sets the earth position and time zone to be used in coordinate conversions.
196       * This method is rarely used.  The observer's parameters come into play
197       * only when dealing with conversions to or from AZ/EL.  The default
198       * position and time zone are those of the EVLA.
199       *  
200       * @param location
201       *   the location of the observer to be used in coordinate conversion
202       *   calculations.  A value of <i>null</i> will result in the use of
203       *   the EVLA's position.
204       *  
205       * @param timeZone
206       *   the time zone of the observer to be used in coordinate conversion
207       *   calculations.  A value of <i>null</i> will result in the use of
208       *   the EVLA's position.
209       *   
210       * @since 2008-10-08
211       */
212      public void setObserverParameters(EarthPosition location, TimeZone timeZone)
213      {
214        observerLocation = (location == null) ? VLA_LOCATION  : location;
215        observerTimeZone = (timeZone == null) ? VLA_TIME_ZONE : timeZone;
216        
217        queryLst.setLocationAndTimeZone(observerLocation.getLongitude(),
218                                        observerTimeZone);
219        compareTime = 0L;
220        proximitiesFast.clear();
221        proximitiesSlow.clear();
222        fastMapEntries.clear();
223      }
224      
225      /**
226       * Sets the time for which position information will be requested.
227       * The constant {@link #NOW} may be used as a signal to use the
228       * current system time.
229       * 
230       * @param newTime the time at which position information will be evaluated.
231       */
232      public void setQueryTime(Date newTime)
233      {
234        requestedTime = newTime;
235    
236        queryTime = (requestedTime == NOW) ? new Date() : requestedTime;
237        
238        queryLst.setSolarTime(queryTime);
239    
240        updateCentralPositionVariables();
241        
242        compareTime = 0L;
243        
244        proximitiesFast.clear();
245        proximitiesSlow.clear();
246        fastMapEntries.clear();
247      }
248    
249      /* (non-Javadoc)
250       * @see edu.nrao.sss.sort.Orderable#setOrder(edu.nrao.sss.sort.SortOrder)
251       */
252      public void setOrder(SortOrder newOrder)
253      {
254        sorter.setOrder(newOrder);
255        compareTime = 0L;
256      }
257      
258      /* (non-Javadoc)
259       * @see edu.nrao.sss.sort.Orderable#getOrder()
260       */
261      public SortOrder getOrder()
262      {
263        return sorter.getOrder();
264      }
265      
266      private long compareTime = 0L;
267      
268      /* (non-Javadoc)
269       * @see Comparator#compare(Object, Object)
270       */
271      public int compare(SourceCatalogEntry sce1, SourceCatalogEntry sce2)
272      {
273        //Attempt to figure out whether or not this is a comparison of
274        //an in-progress sort or the first comparison of a new sort.
275        //We do this only when the client wants to use the current time
276        //for position determination.  We are trying to use one time for
277        //an entire sort, but update the current time for a new sort.
278        if (requestedTime == NOW)
279        {
280          long now = System.currentTimeMillis();
281          
282          //If we haven't done a comparison in awhile, it's probably a new sort
283          if ((now - compareTime) > 1000L) //1 second
284          {
285            queryTime = new Date();
286            queryLst.setSolarTime(queryTime);
287            proximitiesFast.clear();
288            proximitiesSlow.clear();
289            fastMapEntries.clear();
290          }
291    
292          compareTime = now;
293        }
294    
295        Double degrees1 = getPrecalculatedDegrees(sce1);
296        Double degrees2 = getPrecalculatedDegrees(sce2);
297        
298        if (degrees1 == null)
299          degrees1 = calculateAndUpdateSeparation(sce1);
300        
301        if (degrees2 == null)
302          degrees2 = calculateAndUpdateSeparation(sce2);
303        
304        return sorter.compare(degrees1, degrees2);
305      }
306      
307      private Double calculateAndUpdateSeparation(SourceCatalogEntry sce)
308      {
309        Double degrees;
310        
311        //Use the time to get source from what might be a lookup table
312        Source source = sce.get(queryTime);
313        
314        //Source could be null if queryTime is earlier than earliest time
315        //in table.  Put at end of the line.
316        if (source == null)
317        {
318          degrees = 180.0;
319        }
320        else 
321        {
322          //Convert source's position to system used by centralPosition
323          SkyPosition srcPos = source.getCentralSubsource().getPosition();
324          
325          try
326          {
327            srcPos = srcPos.toPosition(cpCoordSys,       cpEpoch,
328                                       observerLocation, queryLst);
329          }
330          catch (CoordinateConversionException ex)
331          {
332            log.warn("Source position conversion failed for source '" + source.getName() +
333                     "'.  Using unconverted position.",ex);
334            //Work with unconverted values
335          }
336    
337          Angle separation = EarthPosition.calculateAngularSeparation(
338                               srcPos.getLatitude(queryTime),  cpLatitude,
339                               srcPos.getLongitude(queryTime), cpLongitude);
340          
341          degrees = separation.toUnits(ArcUnits.DEGREE).doubleValue();
342        }
343        
344        //Use the fast map if it does not already have an entry for an
345        //SCE with this name.  Otherwise use the slow map.
346        String name = sce.getName();
347        if (!fastMapEntries.containsKey(name))
348        {
349          fastMapEntries.put(name, sce);
350          proximitiesFast.put(name, degrees);
351        }
352        else
353        {
354          proximitiesSlow.put(sce, degrees);
355        }
356        
357        return degrees;
358      }
359      
360      private class DegreesSorter extends DoubleSortKey implements Comparator<Double>
361      {
362        public int compare(Double d1, Double d2)
363        {
364          return compareObjects(d1, d2);
365        }
366      }
367    }