001    package edu.nrao.sss.model.project;
002    
003    import java.math.BigDecimal;
004    import java.net.URL;
005    import java.util.ArrayList;
006    import java.util.HashMap;
007    import java.util.List;
008    import java.util.Map;
009    
010    import javax.xml.bind.annotation.XmlAccessType;
011    import javax.xml.bind.annotation.XmlAccessorType;
012    import javax.xml.bind.annotation.XmlElement;
013    import javax.xml.parsers.SAXParser;
014    import javax.xml.parsers.SAXParserFactory;
015    
016    import org.apache.log4j.Logger;
017    import org.xml.sax.Attributes;
018    import org.xml.sax.SAXException;
019    import org.xml.sax.helpers.DefaultHandler;
020    
021    import edu.nrao.sss.math.MathUtil;
022    import edu.nrao.sss.measure.Angle;
023    import edu.nrao.sss.measure.LinearVelocity;
024    import edu.nrao.sss.model.resource.ReceiverBand;
025    import edu.nrao.sss.model.resource.TelescopeConfiguration;
026    
027    import static edu.nrao.sss.measure.ArcUnits.DEGREE;
028    import static edu.nrao.sss.measure.LinearVelocityUnits.METERS_PER_SECOND;
029    
030    /**
031     * Scheduling constraints related to environmental conditions.
032     * An instance of this object is held by a {@link SchedulingBlock}.
033     * <p>
034     * <b>Version Info:</b>
035     * <table style="margin-left:2em">
036     *   <tr><td>$Revision: 1915 $</td></tr>
037     *   <tr><td>$Date: 2009-01-21 16:15:07 -0700 (Wed, 21 Jan 2009) $</td></tr>
038     *   <tr><td>$Author: dharland $ (last person to modify)</td></tr>
039     * </table></p>
040     * 
041     * @author David M. Harland
042     * @since 2008-07-15
043     */
044    @XmlAccessorType(XmlAccessType.NONE)
045    public class EnvironmentalConstraints
046      implements Cloneable
047    {
048      private static final Logger log = Logger.getLogger(EnvironmentalConstraints.class);
049      
050      //Constants to indicate that a given constraint is not in use
051      private static final BigDecimal NO_MAX_PHASE     = new BigDecimal("180.0");
052      private static final BigDecimal NO_MAX_WIND      = MathUtil.POSITIVE_INFINITY;
053      private static final BigDecimal NO_MIN_SOLAR_SEP = new BigDecimal("-1");
054      private static final BigDecimal NO_MAX_OPACITY   = MathUtil.POSITIVE_INFINITY;
055      
056      private static final BigDecimal ANTENNA_MIN_ELEV = new BigDecimal("8.0");
057      
058      private LinearVelocity windSpeedMax;
059      private Angle          tropospherePhaseMax;
060      private Angle          ionospherePhaseMax;
061      private Angle          solarSepMin;
062      private Angle          elevationMin;
063      private BigDecimal     opacityMax;
064      private boolean        avoidSunrise;
065      private boolean        avoidSunset;
066      
067      /**
068       * Creates a new container of environmental scheduling constraints.
069       */
070      public EnvironmentalConstraints()
071      {
072        windSpeedMax        = new LinearVelocity();
073        tropospherePhaseMax = new Angle();
074        ionospherePhaseMax  = new Angle();
075        solarSepMin         = new Angle();
076        elevationMin        = new Angle();
077    
078        privateReset();
079      }
080      
081      private void privateReset()
082      {
083        resetMaxAllowableWindSpeed();
084        resetMaxAllowableApi();
085        resetMaxAllowableIonospherePhase();
086        resetMinAllowableSolarSeparation();
087        resetMinAllowableElevation();
088        resetMaxAllowableOpacity();
089        resetAvoidSunrise();
090        resetAvoidSunset();
091      }
092      
093      /**
094       * Puts the constraints held by this object back to their default values.
095       * The default values are those that typically would not prevent scheduling.
096       */
097      public void reset()
098      {
099        privateReset();
100      }
101      
102      //============================================================================
103      // SIMPLE GET/SET METHODS
104      //============================================================================
105      //----------------------------------------------------------------------------
106      // Wind Speed
107      //----------------------------------------------------------------------------
108    
109      /**
110       * Returns the maximum allowable wind speed.
111       * If the actual wind speed is above this value, the scheduling block
112       * holding this object should not be scheduled for execution.
113       * <p>
114       * The velocity returned is the one held internally by this object,
115       * so changes made to it will be reflected herein.</p>
116       * 
117       * @return the maximum allowable wind speed.
118       * 
119       * @see #hasWindSpeedConstraint()
120       * @see #resetMaxAllowableWindSpeed()
121       */
122      public LinearVelocity getMaxAllowableWindSpeed()
123      {
124        return windSpeedMax;
125      }
126    
127      /**
128       * Puts the <tt>maxAllowableWindSpeed</tt> constraint back to its default
129       * value.
130       * The default value is one that typically would not prevent scheduling.
131       * 
132       * @see #getMaxAllowableWindSpeed()
133       * @see #hasWindSpeedConstraint()
134       */
135      public final void resetMaxAllowableWindSpeed()
136      {
137        windSpeedMax.set(NO_MAX_WIND, METERS_PER_SECOND);
138      }
139      
140      /**
141       * Returns <i>false</i> if the wind speed constraint is so relaxed as to
142       * practically not exist.  This method is intended mainly for handling
143       * situations at the user interface level where one first wants to say
144       * I do or do not care about the wind speed,
145       * and only if I care will I enter a value.
146       * Most other clients can ignore this method and use
147       * {@link #getMaxAllowableWindSpeed()}; if wind speed is not
148       * a concern, that method will return a value sufficiently high that it
149       * will not constrain scheduling.
150       * 
151       * @return
152       *   <i>true</i> if this set of constraints has a wind speed constraint.
153       *   
154       * @see #getMaxAllowableWindSpeed()
155       * @see #resetMaxAllowableWindSpeed()
156       */
157      public boolean hasWindSpeedConstraint()
158      {
159        return !windSpeedMax.isInfinite();
160      }
161      
162      //----------------------------------------------------------------------------
163      // Atmospheric Phase Index
164      //----------------------------------------------------------------------------
165    
166      /**
167       * Returns the maximum allowable atmospheric (tropospheric) phase.
168       * If the actual API is above this value, the scheduling block
169       * holding this object should not be scheduled for execution.
170       * <p>
171       * The angle returned is the one held internally by this object,
172       * so changes made to it will be reflected herein.</p>
173       * 
174       * @return the maximum allowable atmospheric phase.
175       * 
176       * @see #hasApiConstraint()
177       * @see #resetMaxAllowableApi()
178       */
179      public Angle getMaxAllowableApi()
180      {
181        return tropospherePhaseMax;
182      }
183    
184      /**
185       * Puts the <tt>maxAllowableApi</tt> constraint back to its default value.
186       * The default value is one that typically would not prevent scheduling
187       * and results in {@link #hasApiConstraint()} returning <i>false</i>.
188       * 
189       * @see #getMaxAllowableApi()
190       * @see #hasApiConstraint()
191       */
192      public final void resetMaxAllowableApi()
193      {
194        tropospherePhaseMax.set(NO_MAX_PHASE, DEGREE);
195      }
196      
197      /**
198       * Returns <i>false</i> if the API constraint is so relaxed as to practically
199       * not exist.  This method is intended mainly for handling situations at the
200       * user interface level where one first wants to say I do or do not care
201       * about the API, and only if I care will I enter a value.
202       * Most other clients can ignore this method and use
203       * {@link #getMaxAllowableApi()}; if API is not
204       * a concern, that method will return a value sufficiently high that it
205       * will not constrain scheduling.
206       * 
207       * @return
208       *   <i>true</i> if this set of constraints has an API constraint.
209       *   
210       * @see #getMaxAllowableApi()
211       * @see #resetMaxAllowableApi()
212       */
213      public boolean hasApiConstraint()
214      {
215        return tropospherePhaseMax.convertToPositiveNormal().toUnits(DEGREE)
216                                  .compareTo(NO_MAX_PHASE) < 0;
217      }
218      
219      //----------------------------------------------------------------------------
220      // Ionospheric Phase
221      //----------------------------------------------------------------------------
222    
223      /**
224       * Returns the maximum allowable ionospheric phase.
225       * If the actual phase is above this value, the scheduling block
226       * holding this object should not be scheduled for execution.
227       * <p>
228       * The angle returned is the one held internally by this object,
229       * so changes made to it will be reflected herein.</p>
230       * 
231       * @return the maximum allowable ionospheric phase.
232       * 
233       * @see #hasIonosphereConstraint()
234       * @see #resetMaxAllowableIonospherePhase()
235       */
236      public Angle getMaxAllowableIonospherePhase()
237      {
238        return ionospherePhaseMax;
239      }
240    
241      /**
242       * Puts the <tt>maxAllowableIonospherePhase</tt> constraint back to its
243       * default value.
244       * The default value is one that typically would not prevent scheduling
245       * and results in {@link #hasIonosphereConstraint()} returning <i>false</i>.
246       * 
247       * @see #getMaxAllowableIonospherePhase()
248       * @see #hasIonosphereConstraint()
249       */
250      public final void resetMaxAllowableIonospherePhase()
251      {
252        ionospherePhaseMax.set(NO_MAX_PHASE, DEGREE);
253      }
254      
255      /**
256       * Returns <i>false</i> if the ionospheric phase constraint is so relaxed
257       * as to practically not exist.  This method is intended mainly for handling
258       * situations at the user interface level where one first wants to say I do
259       * or do not care about the ionospheric phase,
260       * and only if I care will I enter a value.
261       * Most other clients can ignore this method and use
262       * {@link #getMaxAllowableIonospherePhase()}; if ionospheric phase is not
263       * a concern, that method will return a value sufficiently high that it
264       * will not constrain scheduling.
265       * 
266       * @return
267       *   <i>true</i> if this set of constraints has an ionosphere phase constraint.
268       *   
269       * @see #getMaxAllowableIonospherePhase()
270       * @see #resetMaxAllowableIonospherePhase()
271       */
272      public boolean hasIonosphereConstraint()
273      {
274        return ionospherePhaseMax.convertToPositiveNormal().toUnits(DEGREE)
275                                 .compareTo(NO_MAX_PHASE) < 0;
276      }
277    
278      //----------------------------------------------------------------------------
279      // Solar Separation
280      //----------------------------------------------------------------------------
281    
282      /**
283       * Returns the minimum allowable separation between the observed source
284       * and the sun.
285       * If the actual angle is below this value, the scheduling block
286       * holding this object should not be scheduled for execution.
287       * <p>
288       * The angle returned is the one held internally by this object,
289       * so changes made to it will be reflected herein.</p>
290       * 
291       * @return
292       *   the minimum allowable separation between the observed source and the sun.
293       * 
294       * @see #hasSolarSeparationConstraint()
295       * @see #resetMinAllowableSolarSeparation()
296       */
297      public Angle getMinAllowableSolarSeparation()
298      {
299        return solarSepMin;
300      }
301    
302      /**
303       * Puts the <tt>minAllowableSolarSeparation</tt> constraint back to its
304       * default value.
305       * The default value is one that typically would not prevent scheduling.
306       * 
307       * @see #getMinAllowableSolarSeparation()
308       * @see #hasSolarSeparationConstraint()
309       */
310      public final void resetMinAllowableSolarSeparation()
311      {
312        solarSepMin.set(NO_MIN_SOLAR_SEP, DEGREE);
313      }
314      
315      /**
316       * Returns <i>false</i> if the solar separation constraint is so relaxed as to
317       * practically not exist.  This method is intended mainly for handling
318       * situations at the user interface level where one first wants to say
319       * I do or do not care about the solar separation angle,
320       * and only if I care will I enter a value.
321       * Most other clients can ignore this method and use
322       * {@link #getMinAllowableSolarSeparation()}; if solar separation is not
323       * a concern, that method will return a value sufficiently low that it
324       * will not constrain scheduling.
325       * 
326       * @return
327       *   <i>true</i> if this set of constraints has a solar separation constraint.
328       *   
329       * @see #getMinAllowableSolarSeparation()
330       * @see #resetMinAllowableSolarSeparation()
331       */
332      public boolean hasSolarSeparationConstraint()
333      {
334        return solarSepMin.toUnits(DEGREE).compareTo(NO_MIN_SOLAR_SEP) > 0;
335      }
336    
337      //----------------------------------------------------------------------------
338      // Source Elevation
339      //----------------------------------------------------------------------------
340    
341      /**
342       * Returns the minimum allowable elevation for an observed source.
343       * If the actual elevation is below this value, the scheduling block
344       * holding this object should not be scheduled for execution.
345       * <p>
346       * The angle returned is the one held internally by this object,
347       * so changes made to it will be reflected herein.</p>
348       * 
349       * @return
350       *   the minimum allowable angle between the observed source and the horizon.
351       * 
352       * @see #hasSolarSeparationConstraint()
353       * @see #resetMinAllowableSolarSeparation()
354       */
355      public Angle getMinAllowableElevation()
356      {
357        return elevationMin;
358      }
359    
360      /**
361       * Puts the <tt>minAllowableElevation</tt> constraint back to its
362       * default value.
363       * The default value is the minimum elevation at which an EVLA antenna
364       * can point.
365       * 
366       * @see #getMinAllowableSolarSeparation()
367       * @see #hasSolarSeparationConstraint()
368       */
369      public final void resetMinAllowableElevation()
370      {
371        elevationMin.set(ANTENNA_MIN_ELEV, DEGREE);
372      }
373    
374      /**
375       * Always returns <i>true</i> because the antennas themselves may point
376       * only so low.
377       * 
378       * @return <i>true</i>
379       *   
380       * @see #getMinAllowableElevation()
381       * @see #resetMinAllowableElevation()
382       */
383      public boolean hasMinimumElevationConstraint()
384      {
385        return true;
386      }
387    
388      //----------------------------------------------------------------------------
389      // Atmospheric Opacity
390      //----------------------------------------------------------------------------
391    
392      /**
393       * Returns the maximum allowable atmospheric opacity.
394       * If the actual opacity is above this value, the scheduling block
395       * holding this object should not be scheduled for execution.
396       * 
397       * @return the maximum allowable atmospheric opacity.
398        * 
399       * @see #hasOpacityConstraint()
400       * @see #resetMaxAllowableOpacity()
401       * @see #setMaxAllowableOpacity(double)
402      */
403      public double getMaxAllowableOpacity()
404      {
405        return opacityMax.doubleValue();
406      }
407      
408      /**
409       * Sets the maximum allowable atmospheric opacity.
410       * 
411       * @param newMax
412       *   the maximum allowable atmospheric opacity.
413       * 
414       * @see #getMaxAllowableOpacity()
415       * @see #hasOpacityConstraint()
416       * @see #resetMaxAllowableOpacity()
417       */
418      public void setMaxAllowableOpacity(double newMax)
419      {
420        opacityMax = BigDecimal.valueOf(newMax);
421      }
422      
423      /**
424       * Puts the <tt>maxAllowableOpacity</tt> constraint back to its
425       * default value.
426       * The default value is one that typically would not prevent scheduling.
427       *   
428       * @see #getMaxAllowableOpacity()
429       * @see #hasOpacityConstraint()
430       * @see #setMaxAllowableOpacity(double)
431       */
432      public void resetMaxAllowableOpacity()
433      {
434        opacityMax = NO_MAX_OPACITY;
435      }
436      
437      /**
438       * Returns <i>false</i> if the opacity constraint is so relaxed as to
439       * practically not exist.  This method is intended mainly for handling
440       * situations at the user interface level where one first wants to say
441       * I do or do not care about the opacity,
442       * and only if I care will I enter a value.
443       * Most other clients can ignore this method and use
444       * {@link #getMaxAllowableOpacity()}; if opacity is not
445       * a concern, that method will return a value sufficiently low that it
446       * will not constrain scheduling.
447       * 
448       * @return
449       *   <i>true</i> if this set of constraints has an opacity constraint.
450       *   
451       * @see #getMaxAllowableOpacity()
452       * @see #resetMaxAllowableOpacity()
453       * @see #setMaxAllowableOpacity(double)
454       */
455      public boolean hasOpacityConstraint()
456      {
457        return !MathUtil.doubleValueIsInfinite(opacityMax);
458      }
459    
460      //----------------------------------------------------------------------------
461      // Sunrise & Sunset
462      //----------------------------------------------------------------------------
463    
464      /**
465       * Returns <i>true</i> if the scheduling block holding this object should
466       * not be scheduled near sunrise.
467       * 
468       * @return <i>true</i> if sunrise should be avoided.
469       */
470      @XmlElement
471      public boolean getAvoidSunrise()  { return avoidSunrise; }
472      
473      /**
474       * Used to specify whether or not the scheduling block holding this object
475       * should avoid execution near sunrise.
476       * 
477       * @param avoid
478       *   <i>true</i> if sunrise should be avoided.
479       *   
480       * @see #setAvoidSunriseAndSunset(boolean)
481       */
482      public void setAvoidSunrise(boolean avoid)  { avoidSunrise = avoid; }
483      
484      /**
485       * Puts the <tt>avoidSunrise</tt> constraint back to its default value.
486       * The default value is one that typically would not prevent scheduling.
487       */
488      public final void resetAvoidSunrise()  { avoidSunrise = false; }
489      
490      /**
491       * Returns <i>true</i> if the scheduling block holding this object should
492       * not be scheduled near sunset.
493       * 
494       * @return <i>true</i> if sunset should be avoided.
495       */
496      @XmlElement
497      public boolean getAvoidSunset()  { return avoidSunset; }
498      
499      /**
500       * Used to specify whether or not the scheduling block holding this object
501       * should avoid execution near sunset.
502       * 
503       * @param avoid
504       *   <i>true</i> if sunset should be avoided.
505       *   
506       * @see #setAvoidSunriseAndSunset(boolean)
507       */
508      public void setAvoidSunset(boolean avoid)  { avoidSunset = avoid; }
509      
510      /**
511       * Puts the <tt>avoidSunset</tt> constraint back to its default value.
512       * The default value is one that typically would not prevent scheduling.
513       */
514      public final void resetAvoidSunset()  { avoidSunset = false; }
515      
516      /**
517       * A convenience method for avoiding both sunrise and sunset.
518       * 
519       * @param avoid
520       *   <i>true</i> if both sunrise and sunset should be avoided.
521       *   
522       * @see #setAvoidSunrise(boolean)
523       * @see #setAvoidSunset(boolean)
524       */
525      public void setAvoidSunriseAndSunset(boolean avoid)
526      {
527        avoidSunrise = avoid;
528        avoidSunset  = avoid;
529      }
530      
531      //============================================================================
532      // SOLAR SEPARATION AIDES
533      //============================================================================
534      
535      private static Map<ReceiverBand,
536                         Map<TelescopeConfiguration, Angle>> MIN_SOLAR_SEPS = null;
537      
538      private static boolean successfulRead = false;
539      
540      /**
541       * Returns the suggested minimum angular separation between a target source
542       * and the sun.  This value varies based on the frequency observed and the
543       * array configuration.
544       * 
545       * @param receiver
546       *   the receiver band used to observe the source.
547       * 
548       * @param arrayConfig
549       *   the configuration of the telescope at the time the source is observed.
550       * 
551       * @return
552       *   the minimum solar separation angle.
553       */
554      public static Angle
555        getSuggestedMinimumSolarSeparation(ReceiverBand           receiver,
556                                           TelescopeConfiguration arrayConfig)
557      {
558        if (!successfulRead)
559        {
560          if (MIN_SOLAR_SEPS == null)
561            MIN_SOLAR_SEPS = new HashMap<ReceiverBand, Map<TelescopeConfiguration,Angle>>();
562          
563          new SolSepTableLoader().loadInto(MIN_SOLAR_SEPS);
564        }
565        
566        Angle minSep = null;
567        
568        Map<TelescopeConfiguration, Angle> innerMap = MIN_SOLAR_SEPS.get(receiver);
569        
570        if (innerMap != null)
571          minSep = innerMap.get(arrayConfig);
572        
573        return minSep == null ? new Angle(BigDecimal.TEN) : minSep;
574      }
575      
576      /** Reads table from XML file. */
577      static class SolSepTableLoader extends DefaultHandler
578      {
579        void loadInto(Map<ReceiverBand, Map<TelescopeConfiguration, Angle>> table)
580        {
581          this.table = table;
582          
583          URL tableFile = EnvironmentalConstraints.class.getResource("SolarSeparationMinimums.xml");
584          
585          if (tableFile != null)
586          {
587            SolSepTableLoader callbackTarget = this;
588            SAXParserFactory factory = SAXParserFactory.newInstance();
589            try
590            {
591              SAXParser parser = factory.newSAXParser();
592              parser.parse(tableFile.openStream(), callbackTarget);
593              successfulRead = true;
594            }
595            catch (Exception ex)
596            {
597              log.warn("Trouble loading SolarSeparationMinimums.xml:", ex);
598            }
599          }
600          else
601          {
602            log.warn("URL for SolarSeparationMinimums.xml is null.");
603          }
604        }
605        
606        private Map<ReceiverBand, Map<TelescopeConfiguration, Angle>> table;
607        private Map<TelescopeConfiguration, Angle> bandMap;
608        
609        /** Determines which element is being processed now. */
610        @Override
611        public void startElement(String uri,
612                                 String localName,
613                                 String qName,
614                                 Attributes attributes)
615          throws SAXException
616        {
617          if (qName.equals("band"))
618          {
619            bandMap = new HashMap<TelescopeConfiguration, Angle>();
620            
621            String bandName = "EVLA_" + attributes.getValue("id");
622            ReceiverBand band = ReceiverBand.fromString(bandName);
623            
624            table.put(band, bandMap);
625          }
626          else if (qName.equals("config"))
627          {
628            String cfgName = "VLA_" + attributes.getValue("id");
629            TelescopeConfiguration cfg = TelescopeConfiguration.fromString(cfgName);
630            
631            BigDecimal degrees = new BigDecimal(attributes.getValue("degrees"));
632            Angle minAngle = new Angle(degrees);
633            
634            bandMap.put(cfg, minAngle);
635          }
636        }
637      }
638      
639      //============================================================================
640      // PERSISTENCE
641      //============================================================================
642      
643      //Since the units are not expected to vary much by user, we're opting to store
644      //the values w/ out the units in both XML and database.
645      //We're also considering the elements to optional, so we deal with null
646      //on both output and input.
647      
648      @XmlElement
649      @SuppressWarnings("unused")
650      private BigDecimal getMaxWindSpeedMetersPerSec()
651      {
652        if (hasWindSpeedConstraint())
653          return windSpeedMax.toUnits(METERS_PER_SECOND).stripTrailingZeros();
654        else
655          return null;
656      }
657      @SuppressWarnings("unused")
658      private void setMaxWindSpeedMetersPerSec(BigDecimal value)
659      {
660        if (value != null)
661          windSpeedMax.set(value, METERS_PER_SECOND);
662        else
663          resetMaxAllowableWindSpeed();
664      }
665      
666      @XmlElement
667      @SuppressWarnings("unused")
668      private BigDecimal getMaxApiDegrees()
669      {
670        if (hasApiConstraint())
671          return tropospherePhaseMax.toUnits(DEGREE).stripTrailingZeros();
672        else
673          return null;
674      }
675      @SuppressWarnings("unused")
676      private void setMaxApiDegrees(BigDecimal value)
677      {
678        if (value != null)
679          tropospherePhaseMax.set(value, DEGREE);
680        else
681          resetMaxAllowableApi();
682      }
683      
684      @XmlElement
685      @SuppressWarnings("unused")
686      private BigDecimal getMaxIonosphereDegrees()
687      {
688        if (hasIonosphereConstraint())
689          return ionospherePhaseMax.toUnits(DEGREE).stripTrailingZeros();
690        else
691          return null;
692      }
693      @SuppressWarnings("unused")
694      private void setMaxIonosphereDegrees(BigDecimal value)
695      {
696        if (value != null)
697          ionospherePhaseMax.set(value, DEGREE);
698        else
699          resetMaxAllowableIonospherePhase();
700      }
701      
702      @XmlElement
703      @SuppressWarnings("unused")
704      private BigDecimal getMinSolarSepDegrees()
705      {
706        if (hasSolarSeparationConstraint())
707          return solarSepMin.toUnits(DEGREE).stripTrailingZeros();
708        else
709          return null;
710      }
711      @SuppressWarnings("unused")
712      private void setMinSolarSepDegrees(BigDecimal value)
713      {
714        if (value != null)
715          solarSepMin.set(value, DEGREE);
716        else
717          resetMinAllowableSolarSeparation();
718      }
719      
720      @XmlElement
721      @SuppressWarnings("unused")
722      private BigDecimal getMinElevationDegrees()
723      {
724        BigDecimal elevMinDeg = elevationMin.toUnits(DEGREE);
725        
726        if (elevMinDeg.compareTo(ANTENNA_MIN_ELEV) > 0)
727          return elevMinDeg.stripTrailingZeros();
728        else
729          return null;
730      }
731      @SuppressWarnings("unused")
732      private void setMinElevationDegrees(BigDecimal value)
733      {
734        if (value != null)
735          elevationMin.set(value, DEGREE);
736        else
737          resetMinAllowableElevation();
738      }
739      
740      @XmlElement
741      @SuppressWarnings("unused")
742      private BigDecimal getMaxOpacity()
743      {
744        if (hasOpacityConstraint())
745          return opacityMax;
746        else
747          return null;
748      }
749      @SuppressWarnings("unused")
750      private void setMaxOpacity(BigDecimal value)
751      {
752        if (value != null)
753          opacityMax = value;
754        else
755          resetMaxAllowableOpacity();
756      }
757      
758      //============================================================================
759      // EVALUATION OF CONDITIONS
760      //============================================================================
761      
762      /**
763       * Returns a list of all the constraints that should block scheduling based
764       * on the given conditions.  A subset of the conditions may be tested by
765       * using <i>null</i> or <i>false</i> for those conditions you do not want
766       * to test.
767       * 
768       * @param windSpeed
769       *   the wind velocity to be tested against this constraint.
770       *   If you do not wish to use wind in the test for blocking constraints,
771       *   use a value of <i>null</i>.
772       *   
773       * @param api
774       *   the atmospheric phase to be tested against this constraint.
775       *   If you do not wish to use API in the test for blocking constraints,
776       *   use a value of <i>null</i>.
777       *   
778       * @param ionosphericPhase
779       *   the ionospheric phase to be tested against this constraint.
780       *   If you do not wish to use this phase in the test for blocking constraints,
781       *   use a value of <i>null</i>.
782       *   
783       * @param solarSeparation
784       *   the angle between the sun and the target to be tested against this constraint.
785       *   If you do not wish to use this angle in the test for blocking constraints,
786       *   use a value of <i>null</i>.
787       *   
788       * @param nearSunrise
789       *   an indication that the observing time is near sunrise.
790       *   If you do not wish to use this quantity in the test for blocking constraints,
791       *   use a value of <i>false</i>.
792       *   
793       * @param nearSunset
794       *   an indication that the observing time is near sunset.
795       *   If you do not wish to use this quantity in the test for blocking constraints,
796       *   use a value of <i>false</i>.
797       *   
798       * @return
799       *   a list of all the constraints that failed based on the given conditions.
800       */
801      //TODO update for minimum elevation.  Also, don't make client calc solarSeparation --
802      //     this class should do that as a service.
803      //     Add'l params: Date start, Date end, SkyPosition sourcePos.
804      //     Remove param Angle solarSeparation.
805      public List<SchedulingConstraint> getBlockingConstraints(LinearVelocity windSpeed,
806                                                               Angle api,
807                                                               Angle ionosphericPhase,
808                                                               Angle solarSeparation,
809                                                               boolean nearSunrise,
810                                                               boolean nearSunset)
811      {
812        return testConstraints(false,
813                               windSpeed, api, ionosphericPhase,
814                               solarSeparation, nearSunrise, nearSunset);
815      }
816      
817      /**
818       * Returns <i>true</i> if any of the given conditions are outside
819       * of this set of constraints.
820       * See {@link #getBlockingConstraints(LinearVelocity, Angle, Angle, Angle,
821       * boolean, boolean)} for a description of the parameters.
822       * <p>
823       * This method is logically equivalent to calling
824       * <tt>getBlockingConstraints(...).size() > 0</tt>, but will often be
825       * faster because it stops testing on the first failure.</p>
826       *  
827       * @return
828       *   <i>true</i> if any of the given conditions are outside
829       *   of this set of constraints.
830       */
831      //See TODO for getBlockingConstraints
832      public boolean hasBlockingConstraints(LinearVelocity windSpeed,
833                                            Angle          api,
834                                            Angle          ionosphericPhase,
835                                            Angle          solarSeparation,
836                                            boolean        nearSunrise,
837                                            boolean        nearSunset)
838      {
839        return testConstraints(true,
840                               windSpeed, api, ionosphericPhase,
841                               solarSeparation, nearSunrise, nearSunset).size() > 0;
842      }
843      
844      /**
845       * Does the work for both
846       * getBlockingConstraints(...) and hasBlockingConstraints(...).
847       */
848      //See TODO for getBlockingConstraints
849      private List<SchedulingConstraint> testConstraints(boolean failFast,
850                                                         LinearVelocity windSpeed,
851                                                         Angle api,
852                                                         Angle ionosphericPhase,
853                                                         Angle solarSeparation,
854                                                         boolean nearSunrise,
855                                                         boolean nearSunset)
856      {
857        List<SchedulingConstraint> blocks = new ArrayList<SchedulingConstraint>();
858        
859        if (nearSunrise && avoidSunrise)
860        {
861          blocks.add(SchedulingConstraint.NEAR_SUNRISE);
862          if (failFast)
863            return blocks;
864        }
865        if (nearSunset && avoidSunset)
866        {
867          blocks.add(SchedulingConstraint.NEAR_SUNSET);
868          if (failFast)
869            return blocks;
870        }
871        if (windSpeed != null && windSpeed.compareTo(windSpeedMax) > 0)
872        {
873          blocks.add(SchedulingConstraint.WIND_SPEED);
874          if (failFast)
875            return blocks;
876        }
877        if (api != null && api.compareTo(tropospherePhaseMax) > 0)
878        {
879          blocks.add(SchedulingConstraint.ATMOSPHERIC_PHASE);
880          if (failFast)
881            return blocks;
882        }
883        if (ionosphericPhase != null &&
884            ionosphericPhase.compareTo(ionospherePhaseMax) > 0)
885        {
886          blocks.add(SchedulingConstraint.IONOSPHERIC_PHASE);
887          if (failFast)
888            return blocks;
889        }
890        if (solarSeparation != null &&
891            solarSeparation.compareTo(solarSepMin) < 0)
892        {
893          blocks.add(SchedulingConstraint.SOLAR_SEPARATION);
894          if (failFast)
895            return blocks;
896        }
897    
898        return blocks;
899      }
900    
901      //============================================================================
902      // 
903      //============================================================================
904      
905      /**
906       * Returns a copy of these constraints.
907       * <p>
908       * If anything goes wrong during the cloning procedure,
909       * a {@code RuntimeException} will be thrown.</p>
910       */
911      @Override
912      public EnvironmentalConstraints clone()
913      {
914        EnvironmentalConstraints clone = null;
915        
916        try
917        {
918          //This line takes care of the primitive & immutable fields properly
919          clone = (EnvironmentalConstraints)super.clone();
920          
921          clone.windSpeedMax        = this.windSpeedMax.clone();
922          clone.tropospherePhaseMax = this.tropospherePhaseMax.clone();
923          clone.ionospherePhaseMax  = this.ionospherePhaseMax.clone();
924          clone.solarSepMin         = this.solarSepMin.clone();
925          clone.elevationMin        = this.elevationMin.clone();
926        }
927        catch (Exception ex)
928        {
929          throw new RuntimeException(ex);
930        }
931        
932        return clone;
933      }
934      
935      /** Returns <i>true</i> if {@code o} is equal to these constraints. */
936      @Override
937      public boolean equals(Object o)
938      {
939        //Quick exit if o is this
940        if (o == this)
941          return true;
942        
943        //Quick exit if o is null
944        if (o == null)
945          return false;
946        
947        //Quick exit if classes are different
948        if (!o.getClass().equals(this.getClass()))
949          return false;
950        
951        EnvironmentalConstraints other = (EnvironmentalConstraints)o;
952        
953        return
954          other.avoidSunrise == this.avoidSunrise &&
955          other.avoidSunset  == this.avoidSunset  &&
956          
957          other.opacityMax.compareTo(this.opacityMax) == 0 &&
958          
959          other.windSpeedMax.equals(this.windSpeedMax)               &&
960          other.tropospherePhaseMax.equals(this.tropospherePhaseMax) &&
961          other.ionospherePhaseMax.equals(this.ionospherePhaseMax)   &&
962          other.solarSepMin.equals(this.solarSepMin)                 &&
963          other.elevationMin.equals(this.elevationMin);
964      }
965      
966      /** Returns a hash code value for these constraints. */
967      @Override
968      public int hashCode()
969      {
970        //Taken from the Effective Java book by Joshua Bloch.
971        //The constants 17 & 37 are arbitrary & carry no meaning.
972        int result = 17;
973        
974        result = 37 * result + (avoidSunrise ? 17 : 3);
975        result = 37 * result + (avoidSunset ? 17 : 3);
976        result = 37 * result + opacityMax.hashCode();
977        result = 37 * result + windSpeedMax.hashCode();
978        result = 37 * result + tropospherePhaseMax.hashCode();
979        result = 37 * result + ionospherePhaseMax.hashCode();
980        result = 37 * result + solarSepMin.hashCode();
981        result = 37 * result + elevationMin.hashCode();
982        
983        return result;
984      }
985      /*
986      public static void main(String... args) throws Exception
987      {
988        System.out.println(getSuggestedMinimumSolarSeparation(ReceiverBand.EVLA_X, TelescopeConfiguration.VLA_A));
989      }
990      */
991    }