001    package edu.nrao.sss.model.project.scan;
002    
003    import java.text.DateFormat;
004    import java.util.Date;
005    
006    import javax.xml.bind.annotation.XmlElement;
007    import javax.xml.bind.annotation.XmlType;
008    
009    import edu.nrao.sss.measure.LocalSiderealTime;
010    import edu.nrao.sss.measure.TimeDuration;
011    import edu.nrao.sss.measure.TimeOfDay;
012    import edu.nrao.sss.measure.TimeUnits;
013    import edu.nrao.sss.util.DateUtility;
014    
015    /**
016     * Specification for the timing of a {@link Scan}.
017     * <p>
018     * <b>Version Info:</b>
019     * <table style="margin-left:2em">
020     *   <tr><td>$Revision: 1709 $</td></tr>
021     *   <tr><td>$Date: 2008-11-14 11:22:37 -0700 (Fri, 14 Nov 2008) $</td></tr>
022     *   <tr><td>$Author: dharland $ (last person to modify)</td></tr>
023     * </table></p>
024     * 
025     * @author David M. Harland
026     * @since 2008-08-05
027     */
028    @XmlType(propOrder={"timeType", "duration", "lst", "time"})
029    public class ScanTimeSpecification
030      implements Cloneable
031    {
032      //Persisted properties
033      private ScanTimeType timeType;
034      private String       timeText;
035      
036      //Transient properties
037      private Date         utDateTime;
038      private TimeOfDay    lst;
039      private TimeDuration duration;
040      
041      /**
042       * Creates a new time specification.
043       */
044      public ScanTimeSpecification()
045      {
046        init();
047      }
048      
049      private void init()
050      {
051        timeType   = ScanTimeType.ON_SOURCE_SIDEREAL;
052        duration   = new TimeDuration("5.0", TimeUnits.MINUTE);
053        timeText   = duration.toStringHms();
054        lst        = null;
055        utDateTime = null;
056      }
057      
058      /**
059       * Returns this object to the same state as a newly created specification. 
060       */
061      public void reset()
062      {
063        init();
064      }
065      
066      //============================================================================
067      // SETTING PROPERTIES
068      //============================================================================
069      
070      /**
071       * Specifies the timing for a scan.
072       * <p>
073       * The values of the two parameters must make sense together.  If they
074       * do not, an {@link IllegalArgumentException} is thrown.  These are the
075       * rules for the parameters:</p>
076       * <ul>
077       *   <li>If {@code newType} is a duration, {@code newTimeText} must conform
078       *       to the rules given by {@link TimeDuration#parse(String)}.</li>
079       *       
080       *   <li>If {@code newType} is an LST, {@code newTimeText} must conform
081       *       to the rules given by {@link TimeOfDay#parse(String)}.</li>
082       *       
083       *   <li>If {@code newType} is a non-LST point in time, {@code newTimeText}
084       *       must be in the ISO 8601 form yyyy-MM-ddTHH:mm:ss.SSS+####,
085       *       where 'T' is the literal date/time separator.  The parser is
086       *       very strict:
087       *       <ol type="a">
088       *         <li>There must be a decimal point, and the number of places
089       *             after the point must be 1, 2, or 3.</li>
090       *         <li>After the decimal seconds there must be either a
091       *             '+' or '-' sign.</li>
092       *         <li>After the plus or minus sign there must be at least four
093       *             digits to represent the timezone offset from GMT.
094       *             "-0700" means "seven hours earlier than GMT"; 
095       *             "+1230" means "twelve and one half hours later than GMT".</li>
096       *       </ol>
097       *       If you already have a <tt>Date</tt> object, you should use
098       *       {@link #set(ScanTimeType, Date)} instead.</li> 
099       * </ul>
100       * 
101       * @param newType
102       *   the kind of time that {@code newTimeText} represents.
103       *   
104       * @param newTimeText
105       *   a text representation of a duration or point in time, depending
106       *   on the value of {@code newType}.
107       *   
108       * @throws IllegalArgumentException
109       *   if the parameters do not make sense as a pair, or if {@code newTimeText}
110       *   cannot be parsed.
111       *   
112       * @see #set(ScanTimeType, Date)
113       * @see #set(ScanTimeType, LocalSiderealTime)
114       * @see #set(ScanTimeType, TimeDuration)
115       * @see #set(ScanTimeType, TimeOfDay)
116       */
117      public void set(ScanTimeType newType, String newTimeText)
118      {
119        if (newType.isDuration())
120        {
121          setDuration(newType, newTimeText);
122        }
123        else //a point in time
124        {
125          if (newType.isSiderealTime())    //LST time of day
126            setLst(newType, newTimeText);
127          else                             //UT date/time
128            setUt(newType, newTimeText);
129        }
130      }
131      
132      /** Helps public set(ScanTimeType, String) method. */
133      private void setDuration(ScanTimeType newType, String newTimeText)
134      {
135        TimeDuration newDur;
136        
137        try
138        {
139          newDur = TimeDuration.parse(newTimeText);
140        }
141        catch (Exception ex)
142        {
143          throw new IllegalArgumentException("Could not parse '" + newTimeText +
144            "' as a time duration.", ex);
145        }
146        
147        update(newType, newTimeText, null, null, newDur);
148      }
149      
150      /** Helps public set(ScanTimeType, String) method. */
151      private void setLst(ScanTimeType newType, String newTimeText)
152      {
153        TimeOfDay newLst;
154        
155        try
156        {
157          newLst = TimeOfDay.parse(newTimeText);
158        }
159        catch (Exception ex)
160        {
161          throw new IllegalArgumentException("Could not parse '" + newTimeText +
162            "' as a local sidereal time of day.", ex);
163        }
164        
165        update(newType, newTimeText, null, newLst, null);
166      }
167      
168      private static final DateFormat DATE_PARSER_8601 =
169        DateUtility.makeIso8601Formatter();
170      
171      /** Helps public set(ScanTimeType, String) method. */
172      private void setUt(ScanTimeType newType, String newTimeText)
173      {
174        Date newUt;
175        
176        try
177        {
178          newUt = DATE_PARSER_8601.parse(newTimeText);
179        }
180        catch (Exception ex)
181        {
182          throw new IllegalArgumentException("Could not parse '" + newTimeText +
183            "' as a UT date/time.", ex);
184        }
185        
186        update(newType, DATE_PARSER_8601.format(newUt), newUt, null, null);
187      }
188      
189      /**
190       * Specifies the duration for a scan.
191       * 
192       * @param newType
193       *   must be a {@link ScanTimeType#isDuration() duration} type.
194       *   If it is not, an {@code IllegalArgumentException} will be thrown.
195       *   
196       * @param newDuration
197       *   the new duration for this specification.  May not be <i>null</i>.
198       *   
199       * @throws IllegalArgumentException
200       *   if {@code newType} is not a duration or
201       *   if {@code newDuration} is <i>null</i>.
202       */
203      public void set(ScanTimeType newType, TimeDuration newDuration)
204      {
205        if (!newType.isDuration())
206          throw new IllegalArgumentException("Type '"+newType+"' is not a duration.");
207        
208        if (newDuration == null)
209          throw new IllegalArgumentException("NULL durations are not allowed.");
210        
211        update(newType, newDuration.toStringHms(), null, null, newDuration.clone());
212      }
213      
214      /**
215       * Specifies the local sidereal time for a scan.
216       * 
217       * @param newType
218       *   must be both a {@link ScanTimeType#isPointInTime() point-in-time} and
219       *   a {@link ScanTimeType#isSiderealTime() sidereal time}.
220       *   If it is not, an {@code IllegalArgumentException} will be thrown.
221       *   At this time this was written, the only legal values were:
222       *   {@link ScanTimeType#START_LST} and {@link ScanTimeType#STOP_LST}.
223       *   
224       * @param newLst
225       *   the new LST for this specification.  May not be <i>null</i>.
226       *   
227       * @throws IllegalArgumentException
228       *   if the specifications above are not met.
229       */
230      public void set(ScanTimeType newType, TimeOfDay newLst)
231      {
232        if (!newType.isSiderealTime())
233          throw new IllegalArgumentException("Type '"+newType+"' is not a sidereal time.");
234        
235        if (!newType.isPointInTime())
236          throw new IllegalArgumentException("Type '"+newType+"' is not a point in time.");
237        
238        if (newLst == null)
239          throw new IllegalArgumentException("NULL times are not allowed.");
240        
241        update(newType, newLst.toString(), null, newLst.clone(), null);
242      }
243      
244      /**
245       * Specifies the local sidereal time for a scan.
246       * 
247       * @param newType
248       *   must be both a {@link ScanTimeType#isPointInTime() point-in-time} and
249       *   a {@link ScanTimeType#isSiderealTime() sidereal time}.
250       *   If it is not, an {@code IllegalArgumentException} will be thrown.
251       *   At this time this was written, the only legal values were:
252       *   {@link ScanTimeType#START_LST} and {@link ScanTimeType#STOP_LST}.
253       *   
254       * @param newLst
255       *   the new LST for this specification.  May not be <i>null</i>.
256       *   The {@link LocalSiderealTime} class is a date/time class, but
257       *   only the time-of-day portion of this parameter will be used here.
258       *   
259       * @throws IllegalArgumentException
260       *   if the specifications above are not met.
261       */
262      public void set(ScanTimeType newType, LocalSiderealTime newLst)
263      {
264        if (!newType.isSiderealTime())
265          throw new IllegalArgumentException("Type '"+newType+"' is not a sidereal time.");
266        
267        if (!newType.isPointInTime())
268          throw new IllegalArgumentException("Type '"+newType+"' is not a point in time.");
269        
270        if (newLst == null)
271          throw new IllegalArgumentException("NULL times are not allowed.");
272        
273        TimeOfDay newToD = newLst.toTimeOfDay();
274        
275        update(newType, newToD.toString(), null, newToD, null);
276      }
277      
278      /**
279       * Specifies the universal date and time for a scan.
280       * 
281       * @param newType
282       *   must be both a {@link ScanTimeType#isPointInTime() point-in-time} and
283       *   a {@link ScanTimeType#isUniversalTime() universal time}.
284       *   If it is not, an {@code IllegalArgumentException} will be thrown.
285       *   At this time this was written, the only legal values were:
286       *   {@link ScanTimeType#START_UT} and {@link ScanTimeType#STOP_UT}.
287       *   
288       * @param newDateTime
289       *   the new date and time for this specification.  May not be <i>null</i>.
290       *   
291       * @throws IllegalArgumentException
292       *   if the specifications above are not met.
293       */
294      public void set(ScanTimeType newType, Date newDateTime)
295      {
296        if (!newType.isUniversalTime())
297          throw new IllegalArgumentException("Type '"+newType+"' is not a universal time.");
298        
299        if (!newType.isPointInTime())
300          throw new IllegalArgumentException("Type '"+newType+"' is not a point in time.");
301        
302        if (newDateTime == null)
303          throw new IllegalArgumentException("NULL times are not allowed.");
304        
305        update(newType, DATE_PARSER_8601.format(newDateTime), newDateTime, null, null);
306      }
307    
308      /** Sets all the instance variables. */
309      private void update(ScanTimeType newType, String newText,
310                          Date newUt, TimeOfDay newLst, TimeDuration newDur)
311      {
312        timeType = newType;
313        timeText = newText;
314        
315        utDateTime = newUt;
316        lst        = newLst;
317        duration   = newDur;
318      }
319      
320      //============================================================================
321      // SPECIAL CODE FOR JAXB & HIBERNATE
322      //============================================================================
323      
324      private ScanTimeType tempType = null;
325      private String       tempText = null;
326      
327      @XmlElement
328      @SuppressWarnings("unused")
329      private void setTimeType(ScanTimeType newType)
330      {
331        if (newType != null)
332        {
333          tempType = newType;
334          validate();
335        }
336      }
337      
338      @XmlElement
339      @SuppressWarnings("unused")
340      private void setDuration(TimeDuration newDur)
341      {
342        if (newDur != null)
343        {
344          tempText = newDur.toStringHms();
345          validate();
346        }
347      }
348      
349      @XmlElement
350      @SuppressWarnings("unused")
351      private void setLst(TimeOfDay newLst)
352      {
353        if (newLst != null)
354        {
355          tempText = newLst.toString();
356          validate();
357        }
358      }
359      
360      @XmlElement
361      @SuppressWarnings("unused")
362      private void setTime(Date newDate)
363      {
364        if (newDate != null)
365        {
366          tempText = DATE_PARSER_8601.format(newDate);
367          validate();
368        }
369      }
370      
371      @SuppressWarnings("unused")
372      private String getTimeValue()
373      {
374        return timeText;
375      }
376      
377      @SuppressWarnings("unused")
378      private void setTimeValue(String newValue)
379      {
380        if (newValue != null)
381        {
382          tempText = newValue;
383          validate();
384        }
385      }
386      
387      private void validate()
388      {
389        //Wait until we've received both items
390        if (tempType != null && tempText != null)
391        {
392          set(tempType, tempText);
393          tempType = null;
394          tempText = null;
395        }
396      }
397      
398      //============================================================================
399      // FETCHTING PROPERTIES
400      //============================================================================
401      
402      /**
403       * Returns the type of time held by this specification.
404       * @return the type of time held by this specification.
405       */
406      public ScanTimeType getTimeType()  { return timeType; }
407      
408      /**
409       * Returns a text representation of the duration or point in time held by
410       * this specification.
411       * 
412       * @return
413       *   a text representation of the time held by this specification.
414       */
415      public String getTimeText()  { return timeText; }
416    
417      /**
418       * Returns the specified duration, provided the type of this specification
419       * is a duration.  Otherwise returns <i>null</i>.
420       * <p>
421       * The type of duration returned can be determined by calling
422       * {@link #getTimeType()}, or by using the <tt>is<i>Xxx</i></tt>
423       * boolean methods.</p>
424       * <p>
425       * The object returned is <i>not</i> the one held internally by this
426       * specification, so any changes made to it will not affect this
427       * specification.</p>
428       * 
429       * @return
430       *   the specified duration, or <i>null</i> if this specification
431       *   represents a point in time.
432       */
433      public TimeDuration getDuration()
434      {
435        return duration == null ? null : duration.clone();
436      }
437      
438      /**
439       * Returns the specified local sidereal time of day, provided the type of this
440       * specification is an LST.  Otherwise returns <i>null</i>.
441       * <p>
442       * Whether the returned time is a <i>start</i> or <i>stop</i> time can be
443       * determined by calling {@link #getTimeType()}, {@link #isStartTime()}, or
444       * {@link #isStopTime()}.</p>
445       * <p>
446       * The object returned is <i>not</i> the one held internally by this
447       * specification, so any changes made to it will not affect this
448       * specification.</p>
449       *  
450       * @return
451       *   the specified LST, or <i>null</i> if this specification represents
452       *   a duration or UT point in time.
453       */
454      public TimeOfDay getLst()
455      {
456        return lst == null ? null : lst.clone();
457      }
458      
459      /**
460       * Returns the specified UT date and time, provided the type of this
461       * specification is a UT point in time.  Otherwise returns <i>null</i>.
462       * <p>
463       * Whether the returned time is a <i>start</i> or <i>stop</i> time can be
464       * determined by calling {@link #getTimeType()}, {@link #isStartTime()}, or
465       * {@link #isStopTime()}.</p>
466       * <p>
467       * The object returned is <i>not</i> the one held internally by this
468       * specification, so any changes made to it will not affect this
469       * specification.</p>
470       * 
471       * @return
472       *   the specified UT date and time, or <i>null</i> if this specification
473       *   represents a duration or local sidereal time.
474       */
475      public Date getTime()
476      {
477        return utDateTime == null ? null : (Date)utDateTime.clone();
478      }
479    
480      //============================================================================
481      // PASS-THROUGHS TO ScanTimeType
482      //============================================================================
483      
484      /**
485       * Returns <i>true</i> if this specification is a duration
486       * (as opposed to a point in time).
487       * 
488       * @return <i>true</i> if this specification is a duration.
489       */
490      public boolean isLengthOfTime()  { return timeType.isDuration(); }
491      
492      /**
493       * Returns <i>true</i> if this specification is a point in time
494       * (as opposed to a duration).
495       * 
496       * @return <i>true</i> if this specification is a point in time.
497       */
498      public boolean isPointInTime()  { return timeType.isPointInTime(); }
499      
500      /**
501       * Returns <i>true</i> if this specification is a time-on-source duration.
502       * @return <i>true</i> if this specification is a time-on-source duration.
503       */
504      public boolean isOnSourceDuration()  { return timeType.isOnSourceDuration(); }
505      
506      /**
507       * Returns <i>true</i> if this specification is a total duration.
508       * @return <i>true</i> if this specification is a total duration.
509       */
510      public boolean isTotalDuration()  { return timeType.isTotalDuration(); }
511      
512      /**
513       * Returns <i>true</i> if this specification is a start time.
514       * @return <i>true</i> if this specification is a start time.
515       */
516      public boolean isStartTime()  { return timeType.isStartTime(); }
517      
518      /**
519       * Returns <i>true</i> if this specification is a stop time.
520       * @return <i>true</i> if this specification is a stop time.
521       */
522      public boolean isStopTime()  { return timeType.isStopTime(); }
523      
524      /**
525       * Returns <i>true</i> if this specification is expressed in SI units.
526       * @return <i>true</i> if this specification is expressed in SI units.
527       */
528      public boolean isUniversalTime()  { return timeType.isUniversalTime(); }
529      
530      /**
531       * Returns <i>true</i> if this specification is expressed in sidereal units.
532       * @return <i>true</i> if this specification is expressed in sidereal units.
533       */
534      public boolean isSiderealTime()  { return timeType.isSiderealTime();  }
535      
536      //============================================================================
537      // 
538      //============================================================================
539      
540      /**
541       * Returns a text representation of this specification.
542       */
543      @Override
544      public String toString()
545      {
546        return timeText + " " + timeType.toString();
547      }
548      
549      /**
550       * Returns a copy of this specification.
551       * <p>
552       * If anything goes wrong during the cloning procedure,
553       * a {@code RuntimeException} will be thrown.</p>
554       */
555      @Override
556      public ScanTimeSpecification clone()
557      {
558        ScanTimeSpecification clone = null;
559        
560        try
561        {
562          //This line takes care of the primitive fields properly
563          clone = (ScanTimeSpecification)super.clone();
564          
565          clone.duration   = (this.duration   == null) ? null : this.duration.clone();
566          clone.lst        = (this.lst        == null) ? null : this.lst.clone();
567          clone.utDateTime = (this.utDateTime == null) ? null : (Date)this.utDateTime.clone();
568        }
569        catch (Exception ex)
570        {
571          throw new RuntimeException(ex);
572        }
573        
574        return clone;
575      }
576      
577      /** Returns <i>true</i> if {@code o} is equal to this specification. */
578      @Override
579      public boolean equals(Object o)
580      {
581        //Quick exit if o is this
582        if (o == this)
583          return true;
584        
585        //Quick exit if o is null
586        if (o == null)
587          return false;
588        
589        //Quick exit if classes are different
590        if (!o.getClass().equals(this.getClass()))
591          return false;
592        
593        ScanTimeSpecification other = (ScanTimeSpecification)o;
594        
595        return
596          this.timeText.equals(other.timeText) &&
597          this.timeType.equals(other.timeType);
598      }
599      
600      /** Returns a hash code value for this specification. */
601      @Override
602      public int hashCode()
603      {
604        //Taken from the Effective Java book by Joshua Bloch.
605        //The constants 17 & 37 are arbitrary & carry no meaning.
606        int result = 17;
607        
608        result = 37 * result + timeText.hashCode();
609        result = 37 * result + timeType.hashCode();
610        
611        return result;
612      }
613    
614      //============================================================================
615      // 
616      //============================================================================
617      /*
618      public static void main(String... args) throws Exception
619      {
620        ScanTimeSpecification spec = new ScanTimeSpecification();
621        
622        spec.set(ScanTimeType.STOP_UT, new Date());
623        
624        System.out.println(spec.getTimeText());
625        
626        spec.set(ScanTimeType.STOP_UT, "2010-09-08T12:34:56.12+1234567");
627        //spec.set(ScanTimeType.STOP_UT, "QUACK");
628        
629        System.out.println(spec.getTimeText());
630        //System.out.println(spec.getTime());
631      }
632      */
633    }