001    package edu.nrao.sss.model.resource;
002    
003    import java.util.HashSet;
004    import java.util.Set;
005    import javax.xml.bind.annotation.XmlType;
006    import javax.xml.bind.annotation.XmlElement;
007    import javax.xml.bind.annotation.XmlTransient;
008    
009    /**
010     * A selection of antennas.
011     * <p>
012     * A selection of antennas can be more than just a collection of
013     * antennas.  It can be used to make requests such as:
014     * "give me at least 5 antennas, preferrably those that result
015     * in the longest possible baselines".</p>
016     * <p>
017     * <b>Version Info:</b>
018     * <table style="margin-left:2em">
019     *   <tr><td>$Revision: 1710 $</td></tr>
020     *   <tr><td>$Date: 2008-11-14 11:54:07 -0700 (Fri, 14 Nov 2008) $</td></tr>
021     *   <tr><td>$Author: dharland $</td></tr>
022     * </table></p>
023     * 
024     * @author David M. Harland
025     * @since 2007-03-07
026     */
027    @XmlType(
028      propOrder={
029        "idType", "method", "minimumNumber",
030        "persistentUserPicks", "requiredReceiver"
031      }
032    )
033    public class AntennaSelection implements Cloneable
034    {
035      //private static int nextId = 1;
036    
037      //private final int debugId = nextId++;
038    
039      //private static final org.apache.log4j.Logger log = org.apache.log4j.Logger.getLogger(AntennaSelection.class);
040    
041      private static final AntennaSelectionMethod DEFAULT_METHOD =
042        AntennaSelectionMethod.ALL;
043      
044      private static final AntennaSpecifier DEFAULT_ID_TYPE =
045        AntennaSpecifier.ANTENNA_ID;
046      
047      //User input
048      private AntennaSelectionMethod method;
049      private AntennaSpecifier       idType;
050            private AntennaProvider        provider;
051      private int                    minimumNumber;
052      private Set<String>            userPicks;
053            private ReceiverBand           receiverBand;
054      
055      //Not user modifiable & not persisted
056      private boolean     antennaSetIsStale;
057      private Set<String> antennas;
058    
059      
060            /** Creates a new instance using antennaProvider as the source of available
061              * antennas to select from. */
062      public AntennaSelection(AntennaProvider antennaProvider)
063      {
064                    this.provider = (antennaProvider != null)? antennaProvider : new DefaultAntennaProvider();
065    
066        this.userPicks = new HashSet<String>();
067    
068        this.antennas  = new HashSet<String>();
069    
070        init();
071      }
072      
073            /** Creates a new instance using a default AntennaProvider that returns an
074              * empty set of Antennas. */
075      public AntennaSelection()
076            {
077                    this(null);
078            }
079    
080            private static class DefaultAntennaProvider implements AntennaProvider
081            {
082                    public Set<Antenna> getAvailableAntennas() { return new HashSet<Antenna>(); }
083            }
084    
085      /** Used by constructor and reset(). */
086      private void init()
087      {
088        method            = DEFAULT_METHOD;
089        idType            = DEFAULT_ID_TYPE;
090        minimumNumber     = 0;
091        antennaSetIsStale = true;
092                    receiverBand      = null;
093      }
094      
095      /**
096       * Puts this selection back in its initial state.
097       */
098      public void reset()
099      {
100        init();
101        
102        userPicks.clear();
103        antennas.clear();
104      }
105      
106      //============================================================================
107      // 
108      //============================================================================
109      
110      /**
111       * Sets the manner in which antennas will be selected.
112       * <p>
113       * Depending on the value of {@code newMethod}, previously set properties
114       * may be cleared.</p>
115       * 
116       * @param newMethod the manner in which antennas will be selected.
117       *                  If this value is <i>null</i>, the default method
118       *                  (see {@link #getMethod()}) will be used.
119       */
120      public void setMethod(AntennaSelectionMethod newMethod)
121      {
122        if (!method.equals(newMethod))
123        {
124          method = (newMethod == null) ? DEFAULT_METHOD : newMethod;
125          
126          if (!method.requiresMinimumNumberSpecification())
127            minimumNumber = 0;
128          
129          if (!method.requiresManualIdentification())
130          {
131            userPicks.clear();
132          }
133          
134          antennaSetIsStale = true;
135        }
136      }
137    
138      /**
139       * Returns the manner in which antennas will be selected.
140       * The default value for this property is
141       * {@code AntennaSelectionMethod.ALL}.
142       * 
143       * @return the manner in which antennas will be selected.
144       */
145      @XmlElement public AntennaSelectionMethod getMethod()
146      {
147        return method;
148      }
149    
150      /**
151       * Set the type of identifiers returned by {@link #getMatchingAntennas()}.
152       * 
153       * @param newType the type of identifiers returned by {@link #getMatchingAntennas()}.
154       *                If this value is <i>null</i>, the default type
155       *                (see {@link #getIdType()}) will be used.
156       */
157      public void setIdType(AntennaSpecifier newType)
158      {
159        if (!idType.equals(newType))
160        {
161          idType = (newType == null) ? DEFAULT_ID_TYPE : newType;
162    
163          antennaSetIsStale = true;
164        }
165      }
166      
167      /**
168       * Returns the type of identifiers returned by {@link #getMatchingAntennas()}.
169       * The default value for this property is
170       * {@code AntennaSpecifier.ANTENNA_ID}.
171       * <p>
172       * <b>Caveat:</b> If this selection holds a collection of user-specified
173       * antennas, there is no guarantee that the user-specified IDs are of the
174       * type returned here.</p>
175       * 
176       * @return the type of identifiers returned by {@link #getMatchingAntennas()}.
177       */
178      @XmlElement public AntennaSpecifier getIdType()
179      {
180        return idType;
181      }
182      
183      /**
184       * Sets the minimum number of antennas to select and the selection method to
185       * use.
186       * 
187       * @param newMin the minimum number of antennas to select.
188       * @param selectionMethod the manner in which antennas will be selected.
189       * 
190       * @throws IllegalArgumentException if {@code newMin} is less than one or if
191       *           {@code selectionMethod} does not require specification of a
192       *           minimum number of antennas.
193       */
194      public void setMinimumNumber(int                    newMin,
195                                   AntennaSelectionMethod selectionMethod)
196      {
197        if (newMin < 1)
198          throw new IllegalArgumentException(
199            "The lowest allowed value for the minimum number of antennas is one.");
200        
201        if (!selectionMethod.requiresMinimumNumberSpecification())
202          throw new IllegalArgumentException("Selection method " + selectionMethod +
203            " does not require specification of a minimum # of antennas.");
204    
205        setMethod(selectionMethod);
206        
207        minimumNumber = newMin;
208    
209        antennaSetIsStale = true;
210      }
211      
212      /**
213       * Returns the minimum number of antennas to select.
214       * 
215       * @return the minimum number of antennas to select.
216       */
217      @XmlElement public int getMinimumNumber()
218      {
219        //TODO: Count actual selections if user-list?
220        //      Return # of antennas if ALL?
221        
222        return minimumNumber;
223      }
224      
225      /**
226       * Adds the given antenna ID to this selection.
227       * The antenna selection method is also set to
228       * {@link AntennaSelectionMethod#LIST}.
229       * 
230       * @param antennaId the ID of a manually selected antenna.
231       */
232      public void addUserPick(String antennaId)
233      {
234        setMethod(AntennaSelectionMethod.LIST);
235        
236        userPicks.add(antennaId);
237        
238        antennaSetIsStale = true;
239      }
240      
241      /**
242       * Removes the given antenna ID from this selection.
243       * If the current selection method is not
244       * {@link AntennaSelectionMethod#LIST}, this method does nothing.
245       * 
246       * @param antennaId the ID of antenna to remove from the set of
247       *                  manually selected antennas.
248       */
249      public void removeUserPick(String antennaId)
250      {
251        if (getMethod().equals(AntennaSelectionMethod.LIST))
252        {
253          userPicks.remove(antennaId);
254    
255          antennaSetIsStale = true;
256        }
257      }
258      
259            /**
260             * Sets the AntennaProvider instance used to find the complete set of
261             * available Antennas.  If {@code p} is null, then the provider is set to an
262             * instance of DefaultAntennaProvider.  In either case the selection returned
263             * by getMatchingAntennas() is recalculated.
264             */
265            public void setAntennaProvider(AntennaProvider p)
266            {
267                    this.provider = (p != null)? p : new DefaultAntennaProvider();
268                    this.antennaSetIsStale = true;
269            }
270    
271            /**
272             * @return the AntennaProvider instance used to find the complete set of
273             * available Antennas.
274             */
275            @XmlTransient public AntennaProvider getAntennaProvider()
276            {
277                    return this.provider;
278            }
279    
280            /**
281             * Sets a receiver band that must be supported on all returned Antennas.
282             * Setting this to null implies that there is no restriction on supported
283             * receiver bands.
284             */
285            public void setRequiredReceiver(ReceiverBand b)
286            {
287                    this.receiverBand = b;
288            }
289    
290            /**
291             * Returns the receiver band that must be supported on all returned Antennas.
292             * If this returns null, that implies that there is no restriction on supported
293             * receiver bands.
294             */
295            @XmlElement public ReceiverBand getRequiredReceiver()
296            {
297                    return this.receiverBand;
298            }
299    
300      //============================================================================
301      // 
302      //============================================================================
303    
304      /**
305             * Returns a collection of antenna identifiers represented by this selection.
306             * Note that this method will only recalculate the matching antennas if 1 or
307             * more of the properties of this object have changed!
308       * <p>
309       * The returned set is <i>not</i> held internally by this selection, so any
310       * changes made to it will not be reflected in this object.
311       * The returned set is guaranteed to be non-null, but it may be empty.</p>
312       * 
313       * @return a collection of antenna identifiers represented by this selection.
314             * @see #recalculateMatchingAntennas
315       */
316      @XmlTransient public Set<String> getMatchingAntennas()
317      {
318        if (antennaSetIsStale)
319          recalculateMatchingAntennas();
320        
321        return new HashSet<String>(antennas);
322      }
323      
324      /**
325             * Selects antennas based on the other properties of this object.  Calling
326             * this method will recalculate the matching antennas even if none of the
327             * properties of this object have changed.  In contrast,
328             * getMatchingAntennas() will always return the same set, with no
329             * recalculation, if none of the properties have changed.
330             *
331             * <p>TODO: What do we do if the AntennaProvider can not meet the
332             * requirements laid out in this object?</p>
333       */
334      public void recalculateMatchingAntennas()
335      {
336        this.antennas.clear();
337    
338                    switch(this.method)
339                    {
340                            case LIST:
341                                    this.antennas.addAll(this.userPicks);
342                                    break;
343    
344                            case MINIMUM_NUMBER_SHORTEST_BASELINES:
345                                    break;
346    
347                            case MINIMUM_NUMBER_LONGEST_BASELINES:
348                                    break;
349    
350                            case MINIMUM_NUMBER_EVEN_DISTRIBUTION:
351                            case MINIMUM_NUMBER:
352                                    break;
353    
354                            case ALL:
355                            default:
356                                    for (Antenna a : this.provider.getAvailableAntennas())
357                                    {
358                                            this.antennas.add((getIdType().equals(AntennaSpecifier.ANTENNA_ID))? a.getId() : a.getPadId());
359                                    }
360                    }
361        
362        this.antennaSetIsStale = false;
363      }
364      
365      //============================================================================
366      // PERSISTENCE HELPERS 
367      //============================================================================
368      
369      @SuppressWarnings("unused")
370      @XmlElement private String getPersistentUserPicks()
371      {
372        StringBuilder buff = new StringBuilder();
373        
374        for (String id : userPicks)
375          buff.append(id).append(';');
376        
377        return buff.toString();
378      }
379      
380    
381      @SuppressWarnings("unused")
382      private void setPersistentUserPicks(String picks)
383      {
384        boolean picksAdded = false;
385        if (picks != null)
386        {
387          for (String id : picks.split(";"))
388          {
389            if (id.length() > 0)
390            {
391              userPicks.add(id);
392              this.antennaSetIsStale = true;
393              picksAdded = true;
394            }
395          }
396    
397          if (picksAdded)
398            setMethod(AntennaSelectionMethod.LIST);
399        }
400      }
401      
402      //============================================================================
403      // 
404      //============================================================================
405      
406      /**
407       * Returns a selection that is a copy of this one. 
408       * <p>
409       * If anything goes wrong during the cloning procedure,
410       * a {@code RuntimeException} will be thrown.</p>
411       */
412      @Override
413      public AntennaSelection clone()
414      {
415        AntennaSelection clone = null;
416        
417        try
418        {
419          //This line takes care of the primitive & immutable fields properly
420          clone = (AntennaSelection)super.clone();
421          
422          //Clone the sets, but not the immutable elements
423          clone.userPicks = new HashSet<String>(this.userPicks);
424          clone.antennas  = new HashSet<String>(this.antennas);  
425        }
426        catch (Exception ex)
427        {
428          throw new RuntimeException(ex);
429        }
430        
431        return clone;
432      }
433    
434      /** Returns <i>true</i> if {@code o} is equal to this antenna selection. */
435      @Override
436      public boolean equals(Object o)
437      {
438        //Quick exit if o is null
439        if (o == null)
440          return false;
441        
442        //Quick exit if o is this
443        if (o == this)
444          return true;
445        
446        //Quick exit if classes are different
447        if (!o.getClass().equals(this.getClass()))
448          return false;
449    
450        AntennaSelection other = (AntennaSelection)o;
451        
452        //TODO Right now we're comparing only the user input.
453        //     Once we know how we're going to programmatically select
454        //     antennas, we might want to look at either
455        //     A) the new input fields
456        //     B) the derived antenna selection
457        
458        
459        return other.method.equals(this.method) &&
460               other.idType.equals(this.idType) &&
461               other.minimumNumber == this.minimumNumber &&
462               other.userPicks.equals(this.userPicks);
463    
464        //Note the comparison of the userPicks Set is OK because
465        //the elements are immutable.
466     }  
467    
468      /** Returns a hash code value for this antenna selection. */
469      @Override
470      public int hashCode()
471      {
472        //You MUST keep this method in sync w/ the equals method
473    
474        //Taken from the Effective Java book by Joshua Bloch.
475        //The constants 17 & 37 are arbitrary & carry no meaning.
476        int result = 17;
477        
478        result = 37 * result + method.hashCode();
479        result = 37 * result + idType.hashCode();
480        result = 37 * result + minimumNumber;
481        result = 37 * result + userPicks.hashCode();
482        
483        return result;
484      }
485    }