001    package edu.nrao.sss.astronomy;
002    
003    import java.math.BigDecimal;
004    import java.util.ArrayList;
005    import java.util.Collection;
006    import java.util.Date;
007    
008    import edu.nrao.sss.measure.Angle;
009    import edu.nrao.sss.measure.ArcUnits;
010    import edu.nrao.sss.measure.Distance;
011    import edu.nrao.sss.measure.Latitude;
012    import edu.nrao.sss.measure.Longitude;
013    import edu.nrao.sss.measure.LongitudeInterval;
014    import edu.nrao.sss.util.Filter;
015    
016    /**
017     * A filter that operates on {@link SkyPosition}s.
018     * <p>
019     * This filter allows you to select positions based on their latitudes,
020     * longitudes, and distances from some origin.  In addition, this
021     * filter has special {@link #setCone(SkyPosition, Angle) setCone}
022     * methods that can be used separately or in conjunction with the
023     * typical min/max ranges available for latitude and longitude.
024     * That is, you could select only those positions that were both
025     * in a given cone search and within given min/max latitude/longitude
026     * values.</p>
027     * <p>
028     * <b>Revision Info:</b>
029     * <table style="margin-left:2em">
030     *   <tr><td>$Revision: 1239 $</td></tr>
031     *   <tr><td>$Date: 2008-04-25 10:34:57 -0600 (Fri, 25 Apr 2008) $</td></tr>
032     *   <tr><td>$Author: dharland $ (last person to modify)</td></tr>
033     * </table></p>
034     *  
035     * @author David M. Harland
036     * @since 2006-06-27
037     */
038    public class SkyPositionFilter
039      implements Filter<SkyPosition>
040    {
041      /**
042       * A constant that may be used to indicate that the minimum or maximum
043       * longitude is unbounded.
044       */
045      public static final Longitude ANY_LONGITUDE = null;
046    
047      /**
048       * A constant that may be used to indicate that the minimum or maximum
049       * latitude is unbounded.
050       */
051      public static final Latitude  ANY_LATITUDE  = null;
052    
053      /**
054       * A constant that may be used to indicate that the minimum or maximum
055       * distance is unbounded.
056       */
057      public static final Distance  ANY_DISTANCE = null;
058    
059      /**
060       * A constant that indicates the current time should be used when evaluating
061       * the information held by a position.
062       */
063      public static final Date      CURRENT_TIME = null;
064      
065      //Filtering criteria
066      private LongitudeInterval lonRange;
067      private Latitude          latMin;
068      private Latitude          latMax;
069      private Distance          distMin;
070      private Distance          distMax;
071      
072      private boolean           coneSearch;
073      private boolean           coneTouchesPole;
074      private Angle             coneRadius;
075      private SimpleSkyPosition coneCenter;
076      private LongitudeInterval coneLonRange;
077      private Latitude          coneLatMin,  coneLatMax;
078      
079      private Date queryTime;
080      
081      /**
082       * Creates a new wide-open filter that allows all positions to pass.
083       */
084      public SkyPositionFilter()
085      {
086        coneCenter   = new SimpleSkyPosition();
087        coneLonRange = new LongitudeInterval();
088        lonRange     = new LongitudeInterval();
089        
090        clearAll();
091      }
092    
093      //============================================================================
094      // CLEARING THE FILTERING CRITERIA
095      //============================================================================
096      
097      /**
098       * Sets this filter to a wide-open state.
099       * After this call all filtering criteria will be in their wide-open
100       * states and this filter will allow all positions to pass. 
101       */
102      public void clearAll()
103      {
104        clearCone();
105        clearLongitude();
106        clearLatitude();
107        clearDistance();
108        clearQueryTime();
109      }
110      
111      /**
112       * Stops this filter from applying a cone search.
113       * 
114       * @see #setCone(Latitude, Longitude, Angle)
115       */
116      public void clearCone()
117      {
118        coneSearch = false;
119        //It's not necessary to clear the other cone variables
120      }
121      
122      /**
123       * Sets the longitude criterion to its wide-open state.
124       * 
125       * @see #setLongitude(Longitude, Angle)
126       * @see #setLongitudeRange(Longitude, Longitude)
127       */
128      public void clearLongitude()
129      {
130        lonRange.reset();
131      }
132      
133      /**
134       * Sets the latitude criterion to its wide-open state.
135       * 
136       * @see #setLatitude(Latitude, Angle)
137       * @see #setLatitudeRange(Latitude, Latitude)
138       */
139      public void clearLatitude()
140      {
141        latMin = ANY_LATITUDE;
142        latMax = ANY_LATITUDE;
143      }
144      
145      /**
146       * Sets the distance criterion to its wide-open state.
147       * 
148       * @see #setDistance(Distance, Distance)
149       * @see #setDistanceRange(Distance, Distance)
150       */
151      public void clearDistance()
152      {
153        distMin = ANY_DISTANCE;
154        distMax = ANY_DISTANCE;
155      }
156      
157      /**
158       * Sets this filter so that it will use the current time when querying
159       * positions sent to the {@link #allows(SkyPosition)} or
160       * {@link #blocks(SkyPosition)} methods.
161       */
162      public void clearQueryTime()
163      {
164        queryTime = CURRENT_TIME;
165      }
166    
167      //============================================================================
168      // SETTING THE FILTER CRITERIA
169      //============================================================================
170    
171      /**
172       * Sets this filter so that only positions within the cone described
173       * by the center and the search radius may pass.
174       * Clients may further narrow the search by explicitly
175       * setting minimum and maximum latitudes, longitudes, and distances.
176       * <p/>
177       * This is a convenience method that is equivalent to calling:
178       * <pre>
179       *   setCone(center.getLatitude(), center.getLongitude(), searchRadius);
180       * </pre>
181       * 
182       * @param center the center of the search cone.
183       * 
184       * @param searchRadius the radius of the cone.  The radius may not be
185       *                     larger than one-quarter circle (e.g., 90 degrees).
186       *                     If it is, an illegal argument exception is thrown.
187       */
188      public void setCone(SkyPosition center, Angle searchRadius)
189      {
190        setCone(center.getLatitude(), center.getLongitude(), searchRadius);
191      }
192      
193      private static final BigDecimal POS_90 = new BigDecimal("90");
194      private static final BigDecimal NEG_90 = new BigDecimal("-90");
195      
196      /**
197       * Sets this filter so that only positions within the cone described
198       * by the center latitude / longitude pair and the search radius
199       * may pass.  Clients may further narrow the search by explicitly
200       * setting minimum and maximum latitudes, longitudes, and distances.
201       * 
202       * @param centerLatitude the latitude of the center point of the cone.
203       * 
204       * @param centerLongitude the longitude of the center point of the cone.
205       * 
206       * @param searchRadius the radius of the cone.  The radius may not be
207       *                     larger than one-quarter circle (e.g., 90 degrees).
208       *                     If it is, an illegal argument exception is thrown.
209       */
210      public void setCone(Latitude centerLatitude, Longitude centerLongitude,
211                          Angle    searchRadius)
212      {
213        //Keep diameter <= 180 degrees
214        if (searchRadius.getValue().compareTo(searchRadius.getUnits().toQuarterCircle()) > 0)
215          throw new IllegalArgumentException(
216            "searchRadius may not be greater than 1/4 circle.  Found: " +
217            searchRadius);
218        
219        coneSearch = true;
220        
221        coneCenter.setLatitude(centerLatitude.clone());
222        coneCenter.setLongitude(centerLongitude.clone());
223        
224        coneRadius = searchRadius.clone();
225        
226        //Performing a cone search involves lots of expensive trigonometry calcs.
227        //We set these latitude and longitude ranges so that we can quickly
228        //weed out the obvious mismatches.
229        
230        //For latitude, we can be exact.
231        //Need to beware crossing pole.
232        coneTouchesPole = false;
233        
234        BigDecimal degrees =
235          centerLatitude.toUnits(ArcUnits.DEGREE)
236                        .subtract(searchRadius.toUnits(ArcUnits.DEGREE));
237      
238        if (degrees.compareTo(NEG_90) <= 0)
239        {
240          degrees         = NEG_90;
241          coneTouchesPole = true;
242        }
243        coneLatMin = new Latitude(degrees, ArcUnits.DEGREE);
244        
245        degrees = centerLatitude.toUnits(ArcUnits.DEGREE)
246                                .add(searchRadius.toUnits(ArcUnits.DEGREE));
247        
248        if (degrees.compareTo(POS_90) >= 0)
249        {
250          degrees         = POS_90;
251          coneTouchesPole = true;
252        }
253        coneLatMax = new Latitude(degrees, ArcUnits.DEGREE);
254        
255        //We cannot use the same simple formula for longitude.
256        //To see this imagine placing a small circle on the equator.
257        //This circle spans a certain longitude range.  As you move that circle
258        //closer to one of the poles, it covers a larger and larger range until
259        //it covers all lines of longitude once it touches the pole.
260        //Since we are restricting the radius to 90 degrees, we can at least
261        //trim the longitude range to 180d, unless our cone touches a pole.
262        if (coneTouchesPole)
263        {
264          coneLonRange.reset();
265        }
266        else
267        {
268          Angle quarterCircle = new Angle(POS_90);
269    
270          coneLonRange.set(centerLongitude.clone().subtract(quarterCircle),
271                           centerLongitude.clone().add(     quarterCircle));
272        }
273      }
274      
275      /**
276       * Sets that latitude and longitude ranges for this filter.
277       * Calling this method is equivalent to calling:
278       * <pre>
279       *   setLatitude(centerLatitude, halfWidth);
280       *   setLongitude(centerLongitude, halfWidth);
281       * </pre>
282       * 
283       * @param centerLatitude the central value of the latitude range.
284       * @param centerLongitude the central value of the longitude range.
285       * @param halfWidth half the width of both the latitude and longitude ranges.
286       */
287      public void setLatitudeLongitudeRectangle(Latitude  centerLatitude,
288                                                Longitude centerLongitude,
289                                                Angle     halfWidth)
290      {
291        setLatitude (centerLatitude,  halfWidth);
292        setLongitude(centerLongitude, halfWidth);
293      }
294      
295      /**
296       * Sets the longitude range for this filter.
297       * The range is from <tt>center</tt> <i>minus</i> <tt>halfWidth</tt> through
298       * <tt>center</tt> <i>plus</i> <tt>halfWidth</tt>.
299       * 
300       * @param center the central point of the range.
301       * @param halfWidth half the width of the range.
302       */
303      public void setLongitude(Longitude center, Angle halfWidth)
304      {
305        if ((center == null) || (center == ANY_LONGITUDE))
306        {
307          clearLongitude();
308        }
309        else
310        {
311          lonRange.set(center.clone().subtract(halfWidth),
312                       center.clone().add     (halfWidth));
313        }
314      }
315      
316      /**
317       * Sets the longitude range for this filter.
318       * Note that the {@code from} value is allowed to be numerically larger
319       * than the {@code to} value.  This would be the case, for example, if
320       * you wanted to select positions whose longitudes where in the range
321       * 355 to 10 degrees.
322       * 
323       * @param from
324       *          the start of the range of longitude values that are allowed to
325       *          pass through this filter.  If this value is <i>null</i> or
326       *          <tt>ANY_LONGITUDE</tt>, a longitude of zero will be used.
327       * @param to
328       *          the end of the range of longitude values that are allowed to
329       *          pass through this filter.  If this value is <i>null</i> or
330       *          <tt>ANY_LONGITUDE</tt>, a longitude whose value is equal
331       *          to a full circle will be used.
332       */
333      public void setLongitudeRange(Longitude from, Longitude to)
334      {
335        Longitude intervalStart = new Longitude("0.0");
336    
337        if (from != ANY_LONGITUDE)
338          intervalStart.set(from.getValue(), from.getUnits());
339        
340        Longitude intervalEnd = new Longitude();
341        
342        if (to == ANY_LONGITUDE)
343        {
344          intervalEnd.setToFullCircle();
345        }
346        else
347        {
348          BigDecimal value = to.getValue();
349          ArcUnits   units = to.getUnits();
350          
351          if (value.compareTo(units.toFullCircle()) == 0)
352            intervalEnd.setToFullCircle();
353          else
354            intervalEnd.set(value, units);
355        }
356        
357        lonRange.set(intervalStart, intervalEnd);
358      }
359    
360      /**
361       * Sets the longitude range for this filter.
362       * @param newInterval the new range.
363       */
364      public void setLongitudeRange(LongitudeInterval newInterval)
365      {
366        lonRange.set(newInterval.getStart(), newInterval.getEnd());
367      }
368      
369      /**
370       * Sets the latitude range for this filter.
371       * The range is from <tt>center</tt> <i>minus</i> <tt>halfWidth</tt> through
372       * <tt>center</tt> <i>plus</i> <tt>halfWidth</tt>.
373       * 
374       * @param center the central point of the range.
375       * @param halfWidth half the width of the range.
376       */
377      public void setLatitude(Latitude center, Angle halfWidth)
378      {
379        if ((center == null) || (center == ANY_LATITUDE))
380        {
381          clearLatitude();
382        }
383        else
384        {
385          latMin = center.clone().subtract(halfWidth);
386          latMax = center.clone().add     (halfWidth);
387        }
388      }
389      
390      /**
391       * Sets the latitude range for this filter.
392       * To specify an unbounded minimum and/or maximum, use the
393       * constant {@link #ANY_LATITUDE}.
394       * 
395       * @param min the minimum latitude that is allowed to pass
396       *            through this filter.
397       * @param max the maximum latitude that is allowed to pass
398       *            through this filter.
399       */
400      public void setLatitudeRange(Latitude min, Latitude max)
401      {
402        latMin = (min == null) ? ANY_LATITUDE : min.clone();
403        latMax = (max == null) ? ANY_LATITUDE : max.clone();
404      }
405    
406      /**
407       * Sets the distance range for this filter.
408       * The range is from <tt>center</tt> <i>minus</i> <tt>halfWidth</tt> through
409       * <tt>center</tt> <i>plus</i> <tt>halfWidth</tt>.
410       * 
411       * @param center the central point of the range.
412       * @param halfWidth half the width of the range.
413       */
414      public void setDistance(Distance center, Distance halfWidth)
415      {
416        if ((center == null) || (center == ANY_DISTANCE))
417        {
418          clearDistance();
419        }
420        else
421        {
422          distMin = center.clone().subtract(halfWidth);
423          distMax = center.clone().add     (halfWidth);
424        }
425      }
426      
427      /**
428       * Sets the distance range for this filter.
429       * To specify an unbounded minimum and/or maximum, use the
430       * constant {@link #ANY_DISTANCE}.
431       * 
432       * @param min the minimum distance that is allowed to pass
433       *            through this filter.
434       * @param max the maximum distance that is allowed to pass
435       *            through this filter.
436       */
437      public void setDistanceRange(Distance min, Distance max)
438      {
439        distMin = (min == null) ? ANY_DISTANCE : min.clone();
440        distMax = (max == null) ? ANY_DISTANCE : max.clone();
441      }
442      
443      /**
444       * Sets the date and time at which the filtered position is
445       * queried for its longitude, latitude, and distance.
446       * 
447       * @param time the time used by the {@link #allows(SkyPosition)}
448       *             and {@link #blocks(SkyPosition)} methods when
449       *             querying a position.
450       */
451      public void setQueryTime(Date time)
452      {
453        queryTime = (time == null) ? CURRENT_TIME : time;
454      }
455    
456      //============================================================================
457      // APPLYING THIS FILTER
458      //============================================================================
459    
460      /**
461       * Returns <i>true</i> if this filter blocks the given position.
462       * <i>Null</i> positions are always blocked.
463       * 
464       * @param sp the position to be filtered.
465       * 
466       * @return <i>true</i> if this filter blocks {@code sp}.
467       */
468      public boolean blocks(SkyPosition sp)
469      {
470        //Filter blocks all null positions
471        if (sp == null)
472          return true;
473        
474        //Set up the query time
475        Date qt = (queryTime == CURRENT_TIME) ? new Date() : queryTime;
476        
477        Longitude ra = sp.getLongitude(qt);
478        
479        //Block sp if its longitude is out of range
480        if (!lonRange.contains(ra))
481          return true;
482        
483        Latitude dec = sp.getLatitude(qt);
484    
485        //Block sp if its latitude is less than minimum
486        if ((latMin != ANY_LATITUDE) && (dec.compareTo(latMin) < 0))
487          return true;
488    
489        //Block sp if its latitude is greater than maximum
490        if ((latMax != ANY_LATITUDE) && (dec.compareTo(latMax) > 0))
491          return true;
492        
493        Distance dist = sp.getDistance(qt);
494    
495        //Block sp if its distance is less than minimum
496        if ((distMin != ANY_DISTANCE) && (dist.compareTo(distMin) < 0))
497          return true;
498    
499        //Block sp if its distance is greater than maximum
500        if ((distMax != ANY_DISTANCE) && (dist.compareTo(distMax) > 0))
501          return true;
502    
503        //If we survived all the other tests, see if a cone search was requested
504        if (coneSearch)
505        {
506          //Block if sp is outside calculated latitude range
507          if (dec.compareTo(coneLatMin) < 0 || dec.compareTo(coneLatMax) > 0)
508            return true;
509          
510          //Block if sp is outside calculated longitude range
511          if (!coneTouchesPole)
512          {
513            if (!coneLonRange.contains(ra))
514              return true;
515          }
516          
517          //Block if sp is outside of search cone
518          if (coneCenter.getAngularSeparation(sp).compareTo(coneRadius) > 0)
519            return true;
520        }
521        
522        //sp was not blocked
523        return false;
524      }
525      
526      /**
527       * Returns <i>true</i> if this filter allows the given position
528       * to pass through it.
529       * <i>Null</i> positions are always blocked.
530       * 
531       * @param sp the position to be filtered.
532       * 
533       * @return <i>true</i> if this filter allows {@code sp} to pass through it.
534       */
535      public boolean allows(SkyPosition sp)
536      {
537        return !blocks(sp);
538      }
539    
540      /**
541       * Removes from {@code flow} any position that is blocked by this filter.
542       * 
543       * @param flow a collection of positions that this filter will alter by
544       *             removing blocked positions.
545       *              
546       * @return {@code flow}, after the removal of blocked positions.
547       *         If {@code flow} is <i>null</i>, a new empty collection is returned.
548       */
549      public Collection<? extends SkyPosition>
550        removeAllBlockedParticlesFrom(Collection<? extends SkyPosition> flow)
551      {
552        //Quick exit if no flow
553        if (flow == null)
554          return new ArrayList<SkyPosition>();
555        
556        ArrayList<SkyPosition> blockedPositions = new ArrayList<SkyPosition>();
557        
558        for (SkyPosition position : flow)
559          if (this.blocks(position))
560            blockedPositions.add(position);
561        
562        flow.removeAll(blockedPositions);
563        
564        return flow;
565      }
566    }