001    package edu.nrao.sss.measure;
002    
003    import java.math.BigDecimal;
004    
005    import edu.nrao.sss.util.FormatString;
006    
007    /**
008     * An interval from one line of longitude to another.
009     * <p>
010     * This interval is half-open; it includes the starting point
011     * but does <i>not</i> include the ending point.</p>
012     * <p>
013     * The motivation for this class is the desire to test, for example,
014     * if a given point is between a longitude of 355 degrees and
015     * 10 degrees, which is different that testing if that point is
016     * between 10 degrees and 355 degrees.  Because there is a discontinuity
017     * where once we reach the maximum longitude we proceed to the minimum,
018     * we cannot use the typical range concept that allows us to test
019     * whether a point is at or above a minimum and below a maximum, as
020     * the example above shows.  Instead, we need to know if a point
021     * is on or after a <em>starting</em> point and before an
022     * <em>ending</em>, where the starting point could be numerically
023     * greater than the ending point.  The {@link TimeInterval} class
024     * encapsulates the same concept for the time of day.</p>
025     * <p>
026     * <b>Version Info:</b>
027     * <table style="margin-left:2em">
028     *   <tr><td>$Revision: 1707 $</td></tr>
029     *   <tr><td>$Date: 2008-11-14 10:23:59 -0700 (Fri, 14 Nov 2008) $</td></tr>
030     *   <tr><td>$Author: dharland $ (last person to modify)</td></tr>
031     * </table></p>
032     * 
033     * @author David M. Harland
034     * @since 2007-06-13
035     */
036    public class LongitudeInterval
037      implements Cloneable
038    {
039      private Longitude start;
040      private Longitude end;
041      
042      /** Creates a new interval that encompasses the entire circle. */
043      public LongitudeInterval()
044      {
045        start = new Longitude();
046        end   = new Longitude();
047        
048        end.setToFullCircle();
049      }
050      
051      /**
052       * Creates a new interval using the given longitudes.
053       * <p>
054       * The description of the {@link #set(Longitude, Longitude)} method applies
055       * to this constructor as well.</p>
056       * 
057       * @param from the starting point of this interval.  The starting
058       *             point is included in the interval.
059       *             
060       * @param to the ending point of this interval.  The ending
061       *           point is <i>not</i> included in the interval.
062       */
063      public LongitudeInterval(Longitude from, Longitude to)
064      {
065        setInterval(from, to);
066      }
067      
068      /**
069       *  Resets this interval to its default state.
070       *  <p>
071       *  A reset interval has the same state as one newly created by
072       *  the {@link #LongitudeInterval() no-argument constructor}.
073       *  Specifically, it is an interval that encompasses a full circle.</p> 
074       */
075      public void reset()
076      {
077        start.reset();
078        end.setToFullCircle();
079      }
080    
081      /**
082       * Sets the starting and ending points of this interval.
083       * <p>
084       * It is acceptable for the starting longitude to be
085       * later than the ending longitude.  The interval still
086       * starts at {@code from}; it is just that, in order to
087       * reach {@code to}, it must cross the "top" of the circle
088       * (0, or 360, degrees).</p>
089       * <p>
090       * If either parameter is <i>null</i>, an
091       * {@code IllegalArgumentException} will be thrown.</p>
092       * <p>
093       * This class will maintain references to {@code from} and
094       * {@code to}; it will not make copies.  This means that
095       * any changes made by clients to the parameter objects after
096       * calling this method will be reflected in this object.</p>
097       * 
098       * @param from the starting point of this interval.  The starting
099       *             point is included in the interval.
100       *             
101       * @param to the ending point of this interval.  The ending
102       *           point is <i>not</i> included in the interval.
103       */
104      public void set(Longitude from, Longitude to)
105      {
106        setInterval(from, to);
107      }
108    
109      /** Called from constructor & public set method. */
110      private void setInterval(Longitude startLon, Longitude endLon)
111      {
112        if ((startLon == null) || (endLon == null))
113          throw new IllegalArgumentException(
114            "Cannot configure LongitudeInterval with null Longitude.");
115        
116        //Since we don't care about the NUMERICAL ordering of these two
117        //elements, we do NOT need to make clones.  Contrast this to
118        //what we needed to do in the TimeInterval class.
119        start = startLon;
120        end   = endLon;
121      }
122      
123      /**
124       * Sets the endpoints of this interval based on {@code intervalText}.
125       * If {@code intervalText} is <i>null</i> or <tt>""</tt> (the empty string),
126       * the {@link #reset()} method is called.  Otherwise, the parsing is
127       * delegated to {@link #parse(String)}.  See that method
128       * for details related to parsing.
129       */
130      public void set(String intervalText)
131      {
132        if (intervalText == null || intervalText.equals(""))
133        {
134          reset();
135        }
136        else
137        {
138          try {
139            parseInterval(intervalText, FormatString.ENDPOINT_SEPARATOR);
140          }
141          catch (IllegalArgumentException ex) {
142            //If the parseInterval method threw an exception it never reached
143            //the point of updating this interval, so we don't need to restore
144            //to pre-parsing values.
145            throw ex;
146          }
147        }
148      }
149    
150      /**
151       * Exchanges the starting and ending points of this interval. 
152       */
153      public void switchEndpoints()
154      {
155        Longitude temp = start;
156        start = end;
157        end   = temp;
158      }
159      
160      /**
161       * Creates a new interval whose starting point is this interval's ending
162       * point and whose ending point is this interval's starting point.
163       * 
164       * @return a new interval with endpoints opposite to those of this interval.
165       */
166      public LongitudeInterval toComplement()
167      {
168        LongitudeInterval result = this.clone();
169    
170        result.switchEndpoints();
171    
172        return result;
173      }
174      
175      /**
176       * Returns two new intervals that were formed by splitting this one at the
177       * given point.  This interval is not altered by this method.
178       * <p>
179       * <b>Scenario One.</b> If this interval contains {@code pointOfSplit}, then
180       * the first interval in the array runs from this interval's starting point
181       * to {@code pointOfSplit}, and the second interval runs from 
182       * {@code pointOfSplit} to this interval's ending point.</p>
183       * <p>
184       * <b>Scenario Two.</b> If this interval does not contain
185       * {@code pointOfSplit}, then the first interval in the array will be equal
186       * to this one, and the second interval will be a zero length interval whose
187       * starting and ending endpoints are both {@code pointOfSplit}.</p>
188       * 
189       * @param pointOfSplit the point at which to split this interval in two.
190       * 
191       * @return an array of two intervals whose combined length equals this
192       *         interval's length.
193       */
194      public LongitudeInterval[] split(Longitude pointOfSplit)
195      {
196        LongitudeInterval[] result = new LongitudeInterval[2];
197        
198        result[0] = new LongitudeInterval();
199        result[1] = new LongitudeInterval();
200        
201        if (this.contains(pointOfSplit))
202        {
203          result[0].set(this.start, pointOfSplit.clone());
204          result[1].set(pointOfSplit.clone(), this.end);
205          
206          //Special logic for splitting at the top of the circle
207          BigDecimal splitVal = pointOfSplit.getValue();
208          if (splitVal.compareTo(BigDecimal.ZERO) == 0)
209          {
210            result[0].end.setToFullCircle();
211          }
212          else if (splitVal.compareTo(pointOfSplit.getUnits().toFullCircle()) == 0)
213          {
214            result[1].start.set("0.0", result[1].start.getUnits());
215          }
216        }
217        else //can't use point to split, as its not in this interval
218        {
219          result[0].set(this.start, this.end);
220          result[1].set(pointOfSplit.clone(), pointOfSplit.clone());
221        }
222        
223        return result;
224      }
225    
226      //============================================================================
227      // QUERIES
228      //============================================================================
229    
230      /**
231       * Returns this interval's starting longitude.
232       * Note that the starting longitude is included in this interval.
233       * <p>
234       * The returned longitude, which is guaranteed to be non-null,
235       * is the actual longitude held by this interval,
236       * so changes made to it will be reflected in this object.</p>
237       * 
238       * @return this interval's starting longitude.
239       * 
240       * @see #set(Longitude, Longitude)
241       */
242      public Longitude getStart()
243      {
244        return start;
245      }
246    
247      /**
248       * Returns this interval's ending longitude.
249       * Note that the end longitude is <i>not</i> included in this interval.
250       * <p>
251       * The returned longitude, which is guaranteed to be non-null,
252       * is the actual longitude held by this interval,
253       * so changes made to it will be reflected in this object.</p>
254       * 
255       * @return this interval's ending longitude.
256       * 
257       * @see #set(Longitude, Longitude)
258       */
259      public Longitude getEnd()
260      {
261        return end;
262      }
263      
264      /**
265       * Returns the longitude that is midway between the endpoints of this
266       * interval.
267       * <p>
268       * Understand that while the returned value will always be between
269       * the endpoints (or coincident with them, if they are identical),
270       * the center may be numerically smaller than the starting point
271       * of this interval.  For example, if this interval starts at
272       * 340 degrees and ends at 60 degrees, the length of this interval
273       * is 80 degrees, and the center is at 20 degrees, which is a smaller
274       * value than that of -- but is not "before" -- the starting point.</p>
275       *  
276       * @return the center of this interval.
277       */
278      public Longitude getCenter()
279      {
280        Longitude center = start.clone();
281        
282        Angle halfLength = getLength().divideBy("2.0");
283        
284        return center.add(halfLength);
285      }
286      
287      /**
288       * Returns the length of this this interval.
289       * @return the length of this this interval.
290       */
291      public Angle getLength()
292      {
293        Angle endPoint   = end.getAngle().clone();
294        Angle startPoint = start.getAngle();
295        
296        if (endPoint.compareTo(startPoint) < 0)
297          endPoint.add(endPoint.getUnits().toFullCircle());
298        
299        return endPoint.subtract(startPoint);
300      }
301    
302      /**
303       * Returns <i>true</i> if {@code longitude} is contained in this interval.
304       * <p>
305       * Note that this interval is half-open; it includes the starting point,
306       * but not the ending point.</p>
307       * 
308       * @param longitude the longitude to be tested for containment.
309       * 
310       * @return <i>true</i> if {@code longitude} is contained in this interval.
311       */
312      public boolean contains(Longitude longitude)
313      {
314        boolean result;
315        
316        int startComparedToEnd = start.compareTo(end);
317        
318        //"Normal" order: end > start
319        if (startComparedToEnd < 0)
320        {
321          result = (start.compareTo(longitude) <= 0) && (longitude.compareTo(end) < 0);
322        }
323        //Crossing top of circle: start > end
324        else if (startComparedToEnd > 0)
325        {
326          result = (start.compareTo(longitude) <= 0) || (longitude.compareTo(end) < 0);
327        }
328        //A half-open interval with identical endpoints contains nothing
329        else //start==end
330        {
331          result = false;
332        }
333        
334        return result;
335      }
336    
337      //See TimeOfDayInterval for more methods we could add.
338      
339      //============================================================================
340      // TRANSLATION TO & FROM TEXT
341      //============================================================================
342    
343      /**
344       * Returns a string representation of this interval.
345       * The separator of the endpoints is {@link FormatString#ENDPOINT_SEPARATOR}.
346       * 
347       * @return a string representation of this interval.
348       */
349      public String toString()
350      {
351        return toString(FormatString.ENDPOINT_SEPARATOR);
352      }
353      
354      /**
355       * Returns a string representation of this interval.
356       * See the {@link Longitude#toString() toString} method in
357       * {@code Longitude} for information about how the endpoints
358       * are formatted.  The {@code endPointSeparator} is used to
359       * separate the two endpoints in the returned string.
360       * 
361       * @param endPointSeparator text that separates one endpoint from another
362       *                          in the returned string.  Using a <i>null</i>
363       *                          or empty-string value here is a bad idea.
364       *                          
365       * @return a string representation of this interval.
366       */
367      public String toString(String endPointSeparator)
368      {
369        StringBuilder buff = new StringBuilder();
370        
371        buff.append(start.toString());
372        buff.append(endPointSeparator);
373        buff.append(end.toString());
374        
375        return buff.toString();
376      }
377      
378      /**
379       * Creates a new longitude interval by parsing {@code intervalText}.
380       * <p>
381       * This is a convenience method that is equivalent to:<pre>
382       *   parse(intervalText, FormatString.ENDPOINT_SEPARATOR);</pre>
383       * That is, the string separating the two endpoints is assumed to
384       * be {@link FormatString#ENDPOINT_SEPARATOR}.
385       * 
386       * @param intervalText
387       *   the text to be parsed and converted into a longitude
388       *   interval.  If this value is <i>null</i> or <tt>""</tt>
389       *   (the empty string), a new interval of length zero is returned.
390       *
391       * @return
392       *   a new time interval based on {@code intervalText}.\
393       *   If {@code intervalText} is <i>null</i> or <tt>""</tt>
394       *   (the empty string), a new interval of length zero is returned.
395       * 
396       * @throws IllegalArgumentException if {@code intervalText} cannot be parsed.
397       */
398      public static LongitudeInterval parse(String intervalText)
399      {
400        return LongitudeInterval.parse(intervalText, FormatString.ENDPOINT_SEPARATOR);
401      }
402    
403      /**
404       * Creates a new longitude interval by parsing {@code intervalText}.
405       * The general form of {@code intervalText} is
406       * <tt>StartTimeOfDaySeparatorEndTimeOfDay</tt>,
407       * with the particulars of the longitude format described by
408       * the {@link Longitude#parse(String)} method of {@code Longitude}.
409       * 
410       * @param intervalText
411       *   the text to be parsed and converted into a longitude interval.
412       *   If this value is <i>null</i> or <tt>""</tt> (the empty string),
413       *   a new interval of length zero is returned.
414       *                   
415       * @param endPointSeparator
416       *   the text that separates the starting longitude
417       *   from the ending longitude in {@code intervalText}.
418       *  
419       * @return
420       *   a new longitude interval based on {@code intervalText}.
421       * 
422       * @throws IllegalArgumentException
423       *   if the combination {@code intervalText} and {@code endPointSeparator}
424       *   cannot be parsed successfully.
425       */
426      public static LongitudeInterval parse(String intervalText,
427                                            String endPointSeparator)
428      {
429        LongitudeInterval newInterval = new LongitudeInterval();
430        
431        //null & "" are permissible
432        if ((intervalText != null) && !intervalText.equals(""))
433        {
434          try {
435            newInterval.parseInterval(intervalText, endPointSeparator);
436          }
437          catch (IllegalArgumentException ex) {
438            throw ex;
439          }
440        }
441        
442        return newInterval;
443      }
444    
445      /** Does the actual parsing. */
446      private void parseInterval(String intervalText, String endPointSeparator)
447      {
448        //Find the endpoints of the interval
449        String[] endPoints = intervalText.split(endPointSeparator, -1);
450    
451        if (endPoints.length == 2)
452        {
453          setInterval(Longitude.parse(endPoints[0]), 
454                      Longitude.parse(endPoints[1]));
455        }
456        else //bad # of endpoints
457        {
458          throw new IllegalArgumentException("Could not parse " + intervalText +
459            " using " + endPointSeparator + ".  Found  + endPoints.length +" +
460            " endpoints. There should be 2.");
461        }
462      }
463    
464      //============================================================================
465      // 
466      //============================================================================
467    
468      /**
469       *  Returns a copy of this interval.
470       *  <p>
471       *  If anything goes wrong during the cloning procedure,
472       *  a {@code RuntimeException} will be thrown.</p>
473       */
474      @Override
475      public LongitudeInterval clone()
476      {
477        LongitudeInterval clone = null;
478    
479        try
480        {
481          clone = (LongitudeInterval)super.clone();
482          
483          clone.start = this.start.clone();
484          clone.end   = this.end.clone();
485        }
486        catch (Exception ex)
487        {
488          throw new RuntimeException(ex);
489        }
490        
491        return clone;
492      }
493    
494      /** Returns <i>true</i> if {@code o} is equal to this interval. */
495      @Override
496      public boolean equals(Object o)
497      {
498        //Quick exit if o is this
499        if (o == this)
500          return true;
501        
502        //Quick exit if o is null
503        if (o == null)
504          return false;
505        
506        //Quick exit if classes are different
507        if (!o.getClass().equals(this.getClass()))
508          return false;
509        
510        LongitudeInterval otherTime = (LongitudeInterval)o;
511        
512        return otherTime.start.equals(this.start) &&
513               otherTime.end.equals(this.end);
514      }
515    
516      /** Returns a hash code value for this interval. */
517      @Override
518      public int hashCode()
519      {
520        //Taken from the Effective Java book by Joshua Bloch.
521        //The constants 17 & 37 are arbitrary & carry no meaning.
522        int result = 17;
523        
524        result = 37 * result + start.hashCode();
525        result = 37 * result + end.hashCode();
526        
527        return result;
528      }
529    }