001    package edu.nrao.sss.model.resource.evla;
002    
003    import edu.nrao.sss.measure.ArcUnits;
004    import edu.nrao.sss.measure.Angle;
005    import edu.nrao.sss.measure.Latitude;
006    import edu.nrao.sss.measure.Longitude;
007    import edu.nrao.sss.measure.TimeDuration;
008    import edu.nrao.sss.measure.TimeUnits;
009    import edu.nrao.sss.model.project.scan.AntennaWrap;
010    
011    import java.math.BigDecimal;
012    import java.util.ArrayList;
013    import java.util.List;
014    
015    import org.apache.log4j.Logger;
016    
017    /**
018     * TODO: Verify ALL constants returned by this class!!
019     */
020    public class EvlaTelescopeMotionSimulator
021    {
022      private static final Logger log = Logger.getLogger(EvlaTelescopeMotionSimulator.class);
023    
024      private static final Angle MIN_AZ = new Angle("-85.0");
025      private static final Angle MAX_AZ = new Angle("445.0");
026      private static final Angle MIN_EL = new Angle(  "8.0");
027      private static final Angle MAX_EL = new Angle("125.0");
028    
029      private static final Angle MIN_AZ_plus_360 = MIN_AZ.clone().add("360.0");
030      private static final Angle MAX_AZ_less_360 = MAX_AZ.clone().subtract("360.0");
031    
032      //Assume deg/s for rates and deg/s/s for acc.
033      private static final double VEL_AZ = 40.0/60;
034      private static final double VEL_EL = 20.0/60;
035    
036      private static final double ACC_AZ = 1000;
037      private static final double ACC_EL = 1000;
038    
039      private static final TimeDuration MIN_SETUP_TIME =
040        new TimeDuration("0.0", TimeUnits.SECOND);
041    
042      private static final TimeDuration SETTLING_TIME =
043        new TimeDuration("7.0", TimeUnits.SECOND);
044    
045      //Default currentAz/El to midpoint of their ranges.
046      private Angle currentAz = new Angle("225.0");
047      private Angle currentEl = new Angle( "35.0");
048    
049      //These variables keep track of what the new Az/El will be set to during the
050      //moveTime calculation
051      private Angle newAz = null;
052      private Angle newEl = null;
053    
054      private List<Error> errors = new ArrayList<Error>();
055    
056      /**
057       * Creates a EvlaTelescopeMotionSimulator
058       */
059      public EvlaTelescopeMotionSimulator() {}
060    
061      /**
062       * Creates a EvlaTelescopeMotionSimulator with an initial position of {@code
063       * startAz} and {@code startEl}.
064       *
065       * @throws IllegalArgumentException if startAz or startEl are out of range.
066       */
067      public EvlaTelescopeMotionSimulator(Angle startAz, Angle startEl)
068      {
069        setCurrentAntennaAzimuth(startAz);
070        setCurrentAntennaElevation(startEl);
071      }
072    
073      /**
074       * Creates a EvlaTelescopeMotionSimulator with an initial position of {@code
075       * startAz} and {@code startEl} that is on wrap {@code wrap}.
076       *
077       * @throws IllegalArgumentException if startEl is out of range.
078       */
079      public EvlaTelescopeMotionSimulator(Longitude startAz, Latitude startEl, AntennaWrap wrap)
080      {
081        setCurrentAntennaAzimuth(startAz, wrap);
082        setCurrentAntennaElevation(startEl);
083      }
084    
085      /**
086       * Returns the estimated move time from our current saved position to {@code
087       * az, el}.  For the very first calculation, a starting position due south at
088       * 65 degrees elevation is assumed.  The method takes into account antenna
089       * wrap by calculating 4 different move times and returning the smallest
090       * <em>valid</em> move time.  The 4 cases are as follows:
091       *
092       * <ol>
093       * <li>
094       *  The position we're moving to is on the COUNTERCLOCKWISE (left) wrap.  We
095       *  simple rotate the antenna to that position and see how long it takes.
096       *  This is a valid option only if {@code wrap} is not CLOCKWISE and {@code
097       *  goOverTheTop} is false.
098       * </li>
099       * <li>
100       *  The position is on the CLOCKWISE (right) wrap.  Again, we calculate how
101       *  long it will take to rotate the antenna to that position on that wrap.
102       *  This is a valid option only if {@code wrap} is not COUNTERCLOCKWISE and
103       *  {@code goOverTheTop} is false.
104       * </li>
105       * <li>
106       *  We rotate the antenna 180 degrees and then go over the top to get to our
107       *  target position. (i.e. we're subtracting 180 degrees from the AZ in case
108       *  1 above.)  This is only valid if {@code goOverTheTop} is true and the
109       *  elevation is greater than 55 degrees (180 - MAX_EL).
110       * </li>
111       * <li>
112       *  The same as 3 above, but we <em>add</em> 180 to the az instead of
113       *  subtracting.
114       * </li>
115       * </ol>
116       *
117       * This method also clears and refills the list of errors encountered while
118       * attempting this move.
119       */
120      public TimeDuration moveTo(Longitude azStart, Latitude elStart,
121                                 Longitude azEnd,   Latitude elEnd,
122                                 AntennaWrap wrap,  boolean goOverTheTop)
123      {
124        this.newAz = null;
125        this.newEl = null;
126        this.errors.clear();
127    
128        //Initialize min to something large say, 1 day.
129        TimeDuration min = new TimeDuration("24.0");
130    
131        //Initialize wrap to a default if it was null
132        AntennaWrap w = (wrap == null)? AntennaWrap.NO_PREFERENCE : wrap;
133    
134        Angle toAz = azStart.toAngle().convertTo(ArcUnits.DEGREE);
135        Angle toAzEnd = azEnd.toAngle().convertTo(ArcUnits.DEGREE);
136    
137        //Put the toAz on the left or COUNTERCLOCKWISE wrap.
138        //if toAz > minaz + 360: toAz -= 360;
139        if (toAz.compareTo(MIN_AZ_plus_360) > 0)
140          toAz.subtract("360.0");
141    
142        // Make sure the end position is on the same wrap!  This is checked
143        // separately from the above because we aren't guaranteed that just because
144        // we need to subtract 360 from the start pos that we need to do so to the
145        // end as well.  This is mainly a concern for scans that cross the 0d/360d
146        // boundary.
147        if (toAzEnd.compareTo(MIN_AZ_plus_360) > 0)
148          toAzEnd.subtract("360.0");
149    
150        Angle toEl = elStart.toAngle().convertTo(ArcUnits.DEGREE);
151        Angle toElEnd = elEnd.toAngle().convertTo(ArcUnits.DEGREE);
152    
153        //log.debug("Moving : (" + toAz + ", " + toEl + ") : (" + toAzEnd + ", " + toElEnd + ")");
154    
155        //Whereas Longitude is always gaurunteed to be in bounds of our motion,
156        //Latitude (elevation) is not, so we have to check that here before going
157        //any further.  We only check the min, because the max a Latitude can hold
158        //is 90 and our MAX_EL is 125 or so.
159        if (toEl.compareTo(MIN_EL) < 0)
160        {
161          this.errors.add(Error.ELEVATION_OUT_OF_RANGE);
162          toEl = MIN_EL.clone();
163        }
164    
165        // We need to do the same check for the elevation END position too!
166        if (toElEnd.compareTo(MIN_EL) < 0)
167        {
168          this.errors.add(Error.SCAN_END_ELEVATION_OUT_OF_RANGE);
169          toElEnd = MIN_EL.clone();
170        }
171    
172        // This checks to make sure that the if the user selected a CLOCKWISE
173        // preference, but the source is in the region where the wraps do NOT
174        // overlap, we still get a valid calculation.  The reason this comes up is
175        // because for much of the logic below we're considering a wrap to be a
176        // full 360 degrees when it is only actually 265.  So, a source could be in
177        // the CLOCKWISE wrap (180 - 445 degrees) but be missed in the logic below
178        // if it's position is less than 275 degrees. (which is within the range
179        // (-85, -85 + 360)
180        if (AntennaWrap.CLOCKWISE.equals(w) && toAz.compareTo(new Angle("85.0")) > 0)
181          w = AntennaWrap.NO_PREFERENCE;
182    
183    
184        //tmp variables for calculations
185        Angle tmpAz = toAz.clone();
186        Angle tmpAzEnd = toAzEnd.clone();
187        Angle tmpEl = toEl.clone();
188        Angle tmpElEnd = toElEnd.clone();
189    
190        if (!AntennaWrap.CLOCKWISE.equals(w))
191        {
192          //Case 3:
193          if (goOverTheTop)
194          {
195            tmpAz = toAz.clone().subtract("180.0");
196            tmpAzEnd = toAzEnd.clone().subtract("180.0");
197            tmpEl = new Angle("180.0").subtract(toEl);
198            tmpElEnd = new Angle("180.0").subtract(toElEnd);
199    
200            if (tmpAz.compareTo(MIN_AZ) >= 0 && tmpEl.compareTo(MAX_EL) <= 0)
201            {
202              if (updateMin(min, tmpAz, tmpEl))
203              {
204                this.newAz = tmpAzEnd;
205                this.newEl = tmpElEnd;
206              }
207            }
208          }
209    
210          //Case 1:
211          else
212          {
213            if (updateMin(min, tmpAz, tmpEl))
214            {
215              this.newAz = tmpAzEnd;
216              this.newEl = tmpElEnd;
217              //log.debug("Case 1 min = " + min);
218            }
219          }
220        }
221    
222        if (!AntennaWrap.COUNTERCLOCKWISE.equals(w))
223        {
224          //Case 4:
225          if (goOverTheTop)
226          {
227            tmpAz = toAz.clone().add("180.0");
228            tmpEl = new Angle("180.0").subtract(toEl);
229            tmpAzEnd = toAzEnd.clone().add("180.0");
230            tmpElEnd = new Angle("180.0").subtract(toElEnd);
231    
232            if (tmpAz.compareTo(MAX_AZ) <= 0 && tmpEl.compareTo(MAX_EL) <= 0)
233            {
234              if (updateMin(min, tmpAz, tmpEl))
235              {
236                this.newAz = tmpAzEnd;
237                this.newEl = tmpElEnd;
238              }
239            }
240          }
241    
242          //Case 2:
243          else
244          {
245            tmpAz = toAz.clone().add("360.0");
246            tmpAzEnd = toAzEnd.clone().add("360.0");
247    
248            tmpEl = toEl.clone();
249            tmpElEnd = toElEnd.clone();
250    
251            if (tmpAz.compareTo(MAX_AZ) < 0)
252            {
253              if (updateMin(min, tmpAz, tmpEl))
254              {
255                this.newAz = tmpAzEnd;
256                this.newEl = tmpElEnd;
257                //log.debug("Case 2 min = " + min);
258              }
259            }
260          }
261        }
262    
263        if (this.newAz == null || this.newEl == null)
264        {
265          this.errors.add(Error.MOVE_NOT_POSSIBLE);
266        }
267    
268        else
269        {
270          if (MIN_AZ.compareTo(this.newAz) > 0)
271          {
272            //log.debug("newAz < min: " + this.newAz);
273            this.errors.add(Error.SCAN_END_AZIMUTH_OUT_OF_RANGE);
274            this.currentAz = MIN_AZ.clone();
275          }
276          
277          else if (MAX_AZ.compareTo(this.newAz) < 0)
278          {
279            //log.debug("newAz > max: " + this.newAz);
280            this.errors.add(Error.SCAN_END_AZIMUTH_OUT_OF_RANGE);
281            this.currentAz = MAX_AZ.clone();
282          }
283    
284          else
285            this.currentAz = this.newAz;
286    
287          if (MIN_EL.compareTo(this.newEl) > 0)
288          {
289            this.errors.add(Error.SCAN_END_ELEVATION_OUT_OF_RANGE);
290            this.currentEl = MIN_EL.clone();
291          }
292    
293          else if (MAX_EL.compareTo(this.newEl) < 0)
294          {
295            this.errors.add(Error.SCAN_END_ELEVATION_OUT_OF_RANGE);
296            this.currentEl = MAX_EL.clone();
297          }
298    
299          else
300            this.currentEl = this.newEl;
301    
302          //Add any settling time necessary.
303          min.add(SETTLING_TIME);
304    
305          //If the slew took less time than the min. time it takes to prepare the
306          //antenna for observing, use that min. instead.
307          if (min.compareTo(MIN_SETUP_TIME) < 0)
308            min = MIN_SETUP_TIME.clone();
309        }
310    
311        return min;
312      }
313    
314      /**
315       * Returns a list of any errrors that may have occured during the lastest
316       * call to the {@code moveTo} method.
317       */
318      public List<Error> getErrors()
319      {
320        return new ArrayList<Error>(this.errors);
321      }
322    
323      /**
324       * Returns which AntennaWrap this simulator is currently on using the
325       * following rules.  If the currentAz is greater than 275 degrees (MIN_AZ +
326       * 360) then we're in the CLOCKWISE (right) wrap.  Otherwise we're in the
327       * COUNTERCLOCKWISE (left) wrap.  XXX This could be changed to split the
328       * range in half: greater than 180 degrees is CLOCKWISE, the rest is
329       * COUNTERCLOCKWISE.  As it is now, you are only in the CLOCKWISE wrap if you
330       * are in the overlapping region.
331       */
332      public AntennaWrap getCurrentAntennaWrap()
333      {
334        return (currentAz.compareTo(new Angle("180.0")) > 0)?
335          AntennaWrap.CLOCKWISE :
336          AntennaWrap.COUNTERCLOCKWISE;
337      }
338    
339      /** -85 to 445 degrees */
340      public Angle getCurrentAntennaAzimuth()
341      {
342        return this.currentAz.clone();
343      }
344    
345      /** sets the current Antenna Az to a clone of {@code a} if a is within range. */
346      public void setCurrentAntennaAzimuth(Angle a)
347      {
348        if (a != null && MIN_AZ.compareTo(a) <= 0 && MAX_AZ.compareTo(a) >= 0)
349          this.currentAz = a.clone().convertTo(ArcUnits.DEGREE);
350    
351        else
352          throw new IllegalArgumentException("Invalid Antenna Azimuth: " + a);
353      }
354    
355      /** sets the current Antenna Az to a Angle equivalent to {@code a} at wrap {@code w}. */
356      public void setCurrentAntennaAzimuth(Longitude a, AntennaWrap w)
357      {
358        setCurrentAntennaAzimuth(toAntennaAzimuth(a, w));
359      }
360    
361      /** 8 to 125 degrees */
362      public Angle getCurrentAntennaElevation()
363      {
364        return this.currentEl.clone();
365      }
366    
367      /** sets the current Antenna El to a clone of {@code a} if a is within range. */
368      public void setCurrentAntennaElevation(Angle a)
369      {
370        if (a != null && MIN_EL.compareTo(a) <= 0 && MAX_EL.compareTo(a) >= 0)
371          this.currentEl = a.clone().convertTo(ArcUnits.DEGREE);
372    
373        else
374          throw new IllegalArgumentException("Invalid Antenna Elevation: " + a);
375      }
376    
377      /** sets the current Antenna Az to a Angle equivalent to {@code a} at wrap {@code w}. */
378      public void setCurrentAntennaElevation(Latitude a)
379      {
380        setCurrentAntennaElevation(a.toAngle().convertTo(ArcUnits.DEGREE));
381      }
382    
383      /**
384       * Returns an Angle in degrees between -85 and 445 degrees that represents
385       * {@code az} at AntennaWrap {@code w}.
386       */
387      public static Angle toAntennaAzimuth(Longitude az, AntennaWrap w)
388      {
389        Angle antennaAz = null;
390        if (az != null)
391        {
392          Angle a = az.toAngle().convertTo(ArcUnits.DEGREE);
393    
394          //az is 0 - 360 degrees
395          switch (w)
396          {
397            //180 to 445 degrees
398            case CLOCKWISE:
399              //if a < (maxaz - 360): a += 360;
400              if (a.compareTo(MAX_AZ_less_360) < 0)
401                a.add("360.0");
402    
403              antennaAz = a;
404              break;
405    
406            //-85 to 180 degrees
407            case COUNTERCLOCKWISE:
408              //if a > minaz + 360: a -= 360;
409              if (a.compareTo(MIN_AZ_plus_360) > 0)
410                a.subtract("360.0");
411    
412              antennaAz = a;
413              break;
414    
415            default:
416          }
417        }
418    
419        return antennaAz;
420      }
421      
422      /** Returns the minimum azimuth value for EVLA antenna pointings. */
423      public static Angle getAzimuthMinimum()
424      {
425        return MIN_AZ.clone();
426      }
427      
428      /** Returns the maximum azimuth value for EVLA antenna pointings. */
429      public static Angle getAzimuthMaximum()
430      {
431        return MAX_AZ.clone();
432      }
433    
434      /** Returns the default azimuth value for EVLA antenna pointings. */
435      public static Angle getAzimuthDefault()
436      {
437        return new Angle("225.0");
438      }
439      
440      /** Returns the minimum elevation value for EVLA antenna pointings. */
441      public static Angle getElevationMinimum()
442      {
443        return MIN_EL.clone();
444      }
445      
446      /** Returns the maximum elevation value for EVLA antenna pointings. */
447      public static Angle getElevationMaximum()
448      {
449        return MAX_EL.clone();
450      }
451    
452      /** Returns the default elevation value for EVLA antenna pointings. */
453      public static Angle getElevationDefault()
454      {
455        return new Angle("35.0");
456      }
457    
458      /**
459       * This method updates the passed in {@code currentMin} variable with the
460       * minimum value of currentMin, the time it takes to move from currentAz to
461       * az, and the time it takes to move from currentEl to el.  If the currentMin
462       * is changed, we return true.
463       */
464      private boolean updateMin(TimeDuration currentMin, Angle az, Angle el)
465      {
466        //log.debug("updateMin: (" + az + ", " + el + ") " + currentMin);
467        TimeDuration taz = calcAzMoveTime(currentAz, az);
468        TimeDuration tel = calcElMoveTime(currentEl, el);
469    
470        //t is the larger of taz and tel
471        TimeDuration t = (taz.compareTo(tel) > 0)? taz : tel;
472    
473        //if currentMin is > t
474        if (currentMin.compareTo(t) > 0)
475        {
476          currentMin.set(t);
477          return true;
478        }
479    
480        return false;
481      }
482    
483      /**
484       * calculates the move time from angle {@code f} to angle {@code t} in
485       * Azimuth.
486       */
487      private TimeDuration calcAzMoveTime(Angle f, Angle t)
488      {
489        double azd = t.toUnits(ArcUnits.DEGREE).subtract(
490                     f.toUnits(ArcUnits.DEGREE)).abs().doubleValue();
491    
492        //Time it takes to reach full speed
493        //Accounts for both acceleration & decceleration.
494        double timeAccAz = 2 * VEL_AZ / ACC_AZ;
495    
496        //Distance it takes to reach full speed
497        double distAccAz = (VEL_AZ * VEL_AZ) / ACC_AZ;
498    
499        double telSlewAz;
500    
501        //If the antenna never reaches full speed, use this equation.
502        if (azd < distAccAz)
503        {
504          telSlewAz = 2 * Math.sqrt(azd / ACC_AZ);
505        }
506    
507        //otherwise, use this equation.
508        else
509        {
510          telSlewAz = timeAccAz + (azd - distAccAz) / VEL_AZ;
511        }
512    
513        return new TimeDuration(BigDecimal.valueOf(telSlewAz), TimeUnits.SECOND);
514      }
515    
516      /**
517       * calculates the move time from angle {@code f} to angle {@code t} in
518       * Elevation.
519       */
520      private TimeDuration calcElMoveTime(Angle f, Angle t)
521      {
522        double eld = t.toUnits(ArcUnits.DEGREE).subtract(
523                     f.toUnits(ArcUnits.DEGREE)).abs().doubleValue();
524    
525        //Time it takes to reach full speed
526        //Accounts for both acceleration & decceleration.
527        double timeAccEl = 2 * VEL_EL / ACC_EL;
528    
529        //Distance it takes to reach full speed
530        double distAccEl = (VEL_EL * VEL_EL) / ACC_EL;
531    
532        double telSlewEl;
533    
534        //If the antenna never reaches full speed, use this equation.
535        if (eld < distAccEl)
536        {
537          telSlewEl = 2 * Math.sqrt(eld / ACC_EL);
538        }
539    
540        //otherwise, use this equation.
541        else
542        {
543          telSlewEl = timeAccEl + (eld - distAccEl) / VEL_EL;
544        }
545    
546        return new TimeDuration(BigDecimal.valueOf(telSlewEl), TimeUnits.SECOND);
547      }
548    
549      public static enum Error
550      {
551        INVALID_ANTENNA_WRAP_REQUESTED,
552        ELEVATION_OUT_OF_RANGE,
553        SCAN_END_ELEVATION_OUT_OF_RANGE,
554        SCAN_END_AZIMUTH_OUT_OF_RANGE,
555        MOVE_NOT_POSSIBLE;
556      }
557    }