001    package edu.nrao.sss.model.project.scan;
002    
003    import java.math.BigDecimal;
004    import java.util.ArrayList;
005    import java.util.Collections;
006    import java.util.Comparator;
007    import java.util.List;
008    
009    import javax.xml.bind.annotation.XmlElement;
010    import javax.xml.bind.annotation.XmlElementWrapper;
011    import javax.xml.bind.annotation.XmlRootElement;
012    import javax.xml.bind.annotation.XmlType;
013    
014    import edu.nrao.sss.measure.Angle;
015    import edu.nrao.sss.measure.ArcUnits;
016    import edu.nrao.sss.measure.TimeDuration;
017    import edu.nrao.sss.model.source.SourceCatalogEntry;
018    
019    /**
020     * A scan that tips the telescope to several elevations along a given
021     * azimuth.
022     * <p>
023     * <b>CVS Info:</b>
024     * <table style="margin-left:2em">
025     *   <tr><td>$Revision: 2146 $</td></tr>
026     *   <tr><td>$Date: 2009-04-01 11:21:10 -0600 (Wed, 01 Apr 2009) $</td></tr>
027     *   <tr><td>$Author: btruitt $</td></tr>
028     * </table></p>
029     *  
030     * @author David M. Harland
031     * @since 2006-07-18
032     */
033    @XmlRootElement
034    @XmlType(propOrder= {"azimuth", "order", "allowReverseOrder", "elevations"})
035    public class TippingScan
036      extends Scan
037    {
038      //Persisted attributes
039      private Angle                 azimuth;
040      private List<TippingPosition> elevations;
041      private TippingOrder          order;
042      private boolean               allowReverseOrder;
043      
044      //Other attributes
045      
046      /** Creates a new instance. */
047      TippingScan()
048      {
049        super();
050        
051        azimuth           = new Angle("165");
052        elevations        = new ArrayList<TippingPosition>();
053        order             = TippingOrder.getDefault();
054        allowReverseOrder = true;
055      }
056      
057      /** Does nothing.  Tipping scans may not have sources. */
058      @Override
059      public void setSourceCatalogEntry(SourceCatalogEntry sourceOrTable)
060      {
061        super.setSourceCatalogEntry(null);
062      }
063      
064      /**
065       * Sets the azimuth along which the tipping will be done.
066       * @param newAzimuth the azimuth along which the tipping will be done.
067       */
068      public void setAzimuth(Angle newAzimuth)
069      {
070        azimuth = (newAzimuth == null) ? new Angle() : newAzimuth;
071      }
072      
073      /**
074       * Returns the azimuth along which the tipping will be done.
075       * @return the azimuth along which the tipping will be done.
076       */
077      public Angle getAzimuth()
078      {
079        return azimuth;
080      }
081      
082      /**
083       * Sets the tipping order to either low-to-high or high-to-low.
084       * 
085       * @param newOrder the order in which the tipping should be
086       *                     performed.  A value of <i>null</i> is treated as
087       *                     a signal to perform the tipping in a default
088       *                     order.
089       */
090      public void setOrder(TippingOrder newOrder)
091      {
092        order = (newOrder == null) ? TippingOrder.getDefault() : newOrder;
093      }
094      
095      /**
096       * Returns the order (low-to-high or high-to-low) in which the
097       * tipping is performed.
098       * @return the order in which the tipping is performed.
099       */
100      public TippingOrder getOrder()
101      {
102        return order;
103      }
104      
105      /**
106       * Configures this scan so that even numbered iterations of this scan
107       * proceed in either the opposite or same order as the first iteration.
108       * <p>
109       * Example: An observer sets the tipping order to low-to-high.  There is
110       * enough time in this scan to do multiple iterations of the tipping.
111       * If {@code allow} is <i>true</i>, then the second (and all other even-numbered)
112       * tipping(s) will be from high-to-low -- the reverse order of the first.
113       * If {@code allow} is <i>false</i>, all tippings will be from low-to-high.</p>
114       * 
115       * @param allow allows subsequent iterations of this scan to cycle through
116       *              the elevations in reverse order, relative to the prior
117       *              iteration.
118       */
119      public void setAllowReverseOrder(boolean allow)
120      {
121        allowReverseOrder = allow;
122      }
123      
124      /**
125       * Returns <i>true</i> if successive iterations of this scan are allowed
126       * to cycle through the elevations in opposite orders.
127       * 
128       * @see #setAllowReverseOrder(boolean)
129       * 
130       * @return <i>true</i> if successive iterations of this scan are allowed
131       *         to cycle through the elevations in opposite orders.
132       */
133      public boolean getAllowReverseOrder()
134      {
135        return allowReverseOrder;
136      }
137      
138      /**
139       * Adds a series of tipping elevations to this scan's list.
140       * The number of new elevations added is {@code elevationCount}.
141       * The lowest new elevation is {@code minEl}, the largest is
142       * {@code maxEl}.  The other elevations are derived in such
143       * a way that the graph of elevation index versus the secant
144       * of the zenith angle is linear.
145       *  
146       * @param minEl the minimum elevation to be added.
147       *              While a minimum elevation of zero is permitted,
148       *              using a zero elevation is unlikely to give
149       *              useful results as the algorithm used approaches
150       *              infinity for one of its intermediate values.
151       *              
152       * @param maxEl the maximum elevation to be added.
153       * 
154       * @param elevationCount the number of new elevations to be added.
155       *          Special cases:<ul>
156       *            <li>elevationCount < 1: No new elevations will be added.</li>
157       *            <li>elevationCount == 1: Only the {@code minEl} will be
158       *                                     added.</li>
159       *            <li>elevationCount == 2: Only the {@code minEl} and
160       *                                     {@code maxEl} will be added.</li>
161       *          </ul>
162       * 
163       * @throws IllegalArgumentException if either {@code minEl} or
164       *           {@code maxEl} are not within the range [0,quarterCircle].
165       */
166      public void addElevations(Angle minEl, Angle maxEl, int elevationCount,
167                                TimeDuration timePerElevation)
168        throws IllegalArgumentException
169      {
170        //Throw illegal arg exception if either elevation is invalid
171        validateElevationArgument(minEl, "minEl");
172        validateElevationArgument(maxEl, "maxEl");
173        
174        //Special cases
175        if (elevationCount < 1)               //No action
176        {
177          return;
178        }
179        else if (elevationCount <= 2)         //Add only min (& possibly max) elev
180        {
181          TippingPosition lowestPos = new TippingPosition();
182          lowestPos.setElevation(minEl.clone());
183          lowestPos.getTimeAtPosition().set(timePerElevation);
184          elevations.add(lowestPos);
185          
186          if (elevationCount == 2)            //Add only min & max elevations
187          {
188            TippingPosition highestPos = new TippingPosition();
189            highestPos.setElevation(maxEl.clone());
190            highestPos.getTimeAtPosition().set(timePerElevation);
191            elevations.add(highestPos);
192          }
193        }
194        //Normal logic
195        else
196        {
197          double[] radianAltitudes =
198            calculateElevations(minEl.toUnits(ArcUnits.RADIAN).doubleValue(),
199                                maxEl.toUnits(ArcUnits.RADIAN).doubleValue(),
200                                elevationCount);
201    
202          for (double altitude : radianAltitudes)
203          {
204            TippingPosition tippingPos = new TippingPosition();
205            tippingPos.setElevation(new Angle(BigDecimal.valueOf(altitude),
206                                              ArcUnits.RADIAN));
207            tippingPos.getTimeAtPosition().set(timePerElevation);
208            elevations.add(tippingPos);
209          }
210        }
211      }
212      
213      /**
214       * Calculates and returns a set of elevations, in radians.
215       * NOTE: This method does no error checking on its params.  Since this
216       *       is a private method under the control of this class, this is
217       *       OK.  HOWEVER, if you broaden the scope of this method, you
218       *       will need to incorporate error checking.
219       */
220      private double[] calculateElevations(double minEl, double maxEl, int count)
221      {
222        double[] result = new double[count];  //Elevations, in radians
223        
224        //Work with zenith angles
225        final double HALF_PI = Math.PI / 2.0;
226    
227        double minZA = HALF_PI - maxEl;
228        double maxZA = HALF_PI - minEl;
229    
230        double secMin = 1.0 / Math.cos(minZA);
231        double secMax = 1.0 / Math.cos(maxZA);
232        
233        double secSpread = (secMax - secMin) / (count - 1);
234        
235        double secant = secMin;
236    
237        for (int a=1; a < (count-1); a++)
238        {
239          secant += secSpread;
240          double zenithAngle = Math.acos(1.0 / secant);
241          result[a] = HALF_PI - zenithAngle;  //Convert back to elevation
242        }
243        
244        //End points
245        result[0]       = minEl;
246        result[count-1] = maxEl;
247    
248        return result;
249      }
250      
251      /**
252       * Used for checking elevation parameters of other methods.
253       * Throws IllegalArgumentException if elev not in valid range.
254       */
255      private void validateElevationArgument(Angle elev, String name)
256        throws IllegalArgumentException
257      {
258        double angle         = elev.getValue().doubleValue();
259        double quarterCircle = elev.getUnits().toQuarterCircle().doubleValue();
260        
261        if ((angle < 0.0) || (angle > quarterCircle))
262        {
263          StringBuilder buff = new StringBuilder("Illegal value (");
264          
265          buff.append(elev).append(") for parameter ").append(name);
266          buff.append(".  Elevation must be >= 0 && <= ").append(quarterCircle);
267          buff.append(elev.getUnits().getSymbol());
268          
269          throw new IllegalArgumentException(buff.toString());
270        }
271      }
272    
273      /**
274       * Sets the list of tipping elevations held by this scan.
275       * A <i>null</i> {@code replacementList} will be interpreted
276       * as a new empty list.
277       * <p>
278       * This scan will hold a reference to {@code replacementList}
279       * (unless it is <i>null</i>), so any changes made to the list
280       * after calling this method will be reflected in this object.</p>
281       * 
282       * @param replacementList a list of tipping offsets to be held by this
283       *                        scan.
284       */
285      public void setElevations(List<TippingPosition> replacementList)
286      {
287        elevations = (replacementList == null) ? new ArrayList<TippingPosition>()
288                                               : replacementList;
289      }
290    
291      /**
292       * Returns a sorted list of tipping elevations.  The returned list,
293       * which is guaranteed to be non-null, is
294       * ordered either from low-to-high or high-to-low, depending on the 
295       * value of the {@link #getOrder() tipping order}.
296       * <p>
297       * The returned list is the actual list held by this scan,
298       * so changes made to the list will be reflected in this
299       * object.</p>
300       * 
301       * @return a sorted list of tipping elevations.
302       */
303      @XmlElementWrapper
304      @XmlElement(name="tippingPosition")
305      public List<TippingPosition> getElevations()
306      {
307        //Just-in-time sorting of list
308        Collections.sort(elevations, getComparatorFor(getOrder()));
309        
310        return elevations;
311      }
312      
313      /**
314       * Returns a list of tipping elevations that has been sorted in a way
315       * that is appropriate for the given iteration of this scan.
316       * <p>
317       * The appropriate sorting order is determined by the value of the
318       * {@link #getAllowReverseOrder()} property and the iteration number.
319       * If {@code getAllowReverseOrder()} is true and the iteration is
320       * an even number, then the sorting is in the opposite direction of
321       * that specified by {@link #getOrder()}.</p>
322       * <p>
323       * Note that the returned list is <i>not</i> the list held internally
324       * by the scan, so changes to the list will not be reflected in this
325       * object.  However, the elevations held in the returned list are the
326       * same elevations held by this object's internal list, so changing
327       * one of those objects will impact this one.</p>
328       *  
329       * @param iteration one more than the number of times this scan has
330       *                  already been run.  The minimum acceptable value
331       *                  is one.
332       *                  
333       * @return a sorted list of tipping elevations.
334       * 
335       * @throws IllegalArgumentException 
336       */
337      public List<TippingPosition> getElevations(int iteration)
338      {
339        if (iteration < 1)
340          throw new IllegalArgumentException("Iteration must be >= 1.");
341        
342        //Clone the elevation list.  (Don't need to clone elements.)
343        List<TippingPosition> result = new ArrayList<TippingPosition>();
344        result.addAll(elevations);
345        
346        //Determine the tipping order
347        TippingOrder tipOrd = getOrder();
348        
349        if (getAllowReverseOrder() && (iteration % 2 == 0))
350          tipOrd = tipOrd.getReverseOrder();
351        
352        //Sort the cloned list according to the tipping order for this iteration
353        Collections.sort(result, getComparatorFor(tipOrd));
354        
355        return result;
356      }
357      
358      /**
359       * Returns a comparator for sorting a list of angles that is appropriate
360       * for the given tipping order.
361       */
362      private Comparator<TippingPosition> getComparatorFor(TippingOrder tipOrder)
363      {
364        //Low-to-high is the natural order of angle.
365        //Null is a signal to use natural order.
366        Comparator<TippingPosition> comparator = null;
367        
368        //If client wants high-to-low, use a reverse-order comparator
369        if (tipOrder.equals(TippingOrder.HIGH_TO_LOW))
370          comparator = Collections.reverseOrder();
371    
372        return comparator;
373      }
374    
375      //============================================================================
376      // 
377      //============================================================================
378    
379      /* (non-Javadoc)
380       * @see edu.nrao.sss.model.project.scan.ScanLoopElement#toString()
381       */
382      public String toSummaryString()
383      {
384        StringBuilder buff = new StringBuilder(super.toSummaryString());
385        
386        buff.append(", az=").append(azimuth);
387        buff.append(", order=").append(order);
388        buff.append(", allowRev=").append(allowReverseOrder);
389        buff.append(", elevCnt=").append(elevations.size());
390        
391        return buff.toString();
392      }
393    
394      /**
395       *  Returns a tipping scan that is a copy of this one.
396       *  <p>
397       *  The returned scan is, for the most part, a deep copy of this one.
398       *  However, there are a few exceptions noted in the
399       *  {@link ScanLoop#clone() clone method} of this class's parent.</p>
400       *  <p>
401       *  If anything goes wrong during the cloning procedure,
402       *  a {@code RuntimeException} will be thrown.</p>
403       */
404      public TippingScan clone()
405      {
406        TippingScan clone = null;
407    
408        try
409        {
410          //This line takes care of the primitive & immutable fields properly
411          clone = (TippingScan)super.clone();
412          
413          clone.azimuth = this.azimuth.clone();
414          
415          //Need to clone set AND contained elements.
416          clone.elevations = new ArrayList<TippingPosition>();
417          for (TippingPosition tp : this.elevations)
418            clone.elevations.add(tp.clone());
419        }
420        catch (Exception ex)
421        {
422          throw new RuntimeException(ex);
423        }
424        
425        return clone;
426      }
427    
428      /**
429       * Returns <i>true</i> if {@code o} is equal to this tipping scan.
430       * <p>
431       * In order for {@code o} to be equal to this scan, it must have
432       * equal tipping positions in the same order as those of this scan.
433       * It must also follow the rules set forth in the
434       * {@link ScanLoop#equals(Object) equals method} of this class's parent.</p>
435       */
436      public boolean equals(Object o)
437      {
438        //Quick exit if o is this object
439        if (o == this)
440          return true;
441        
442        //Not equal if super class says not equal
443        if (!super.equals(o))
444          return false;
445        
446        //Super class tested for Class equality, so cast is safe
447        TippingScan other = (TippingScan)o;
448        
449        //Compare attributes
450        return other.azimuth.equals(this.azimuth) &&
451               other.order.equals(this.order) &&
452               other.allowReverseOrder == this.allowReverseOrder &&
453               other.elevations.equals(this.elevations);
454      }
455    
456      /* (non-Javadoc)
457       * @see edu.nrao.sss.model.project.scan.Scan#hashCode()
458       */
459      public int hashCode()
460      {
461        //Taken from the Effective Java book by Joshua Bloch.
462        //The constant 37 is arbitrary & carries no meaning.
463        int result = 37 * super.hashCode();
464        
465        result = 37 * result + azimuth.hashCode();
466        result = 37 * result + order.hashCode();
467        result = 37 * result + new Boolean(allowReverseOrder).hashCode();
468        result = 37 * result + elevations.hashCode();
469        
470        return result;
471      }
472    
473      /*
474      public static void main(String[] args)
475      {
476        TippingScan ts = new TippingScan();
477        
478        ts.addElevations(new Angle(20.0), new Angle(70.0), 7, new TimeDuration(0.02));
479        
480        //ts.setOrder(TippingOrder.HIGH_TO_LOW);
481        
482        System.out.println("Tip #; Elev (deg); ZA (deg); sec(ZA); delta(sec(ZA));");
483        double prevSec = -1.0;
484        double currSec = -1.0;
485        int tip=0;
486        for (TippingPosition tp : ts.getElevations())
487        {
488          Angle a = tp.getElevation();
489          System.out.printf("%1$4s", ++tip + "; ");
490          //Elev, in degrees
491          System.out.printf("%1$.3f", a.toUnits(ArcUnits.DEGREE));
492          System.out.print("; ");
493          //ZA, in deg
494          System.out.printf("%1$.3f", (90.0 - a.toUnits(ArcUnits.DEGREE)));
495          System.out.print("; ");
496          //Secant of zenith angle
497          Angle ZA = a.clone().negate().add(Math.PI/2.0);
498          currSec = 1.0/Math.cos(ZA.toUnits(ArcUnits.RADIAN));
499          System.out.printf("%1$.10f", currSec);
500          System.out.print("; ");
501          if (prevSec >= 0.0)
502            System.out.printf("%1$.10f", currSec-prevSec);
503          else
504            System.out.print("             ");
505          System.out.println(';');
506          
507          prevSec = currSec;
508        }
509      }
510      */
511    }