001    package edu.nrao.sss.measure;
002    
003    import static java.math.BigDecimal.ONE;
004    
005    import java.math.BigDecimal;
006    import java.math.RoundingMode;
007    import java.util.ArrayList;
008    import java.util.List;
009    import java.util.SortedMap;
010    import java.util.TreeMap;
011    
012    import edu.nrao.sss.util.EnumerationUtility;
013    import edu.nrao.sss.util.Symbolic;
014    
015    import static edu.nrao.sss.math.MathUtil.MC_FINAL_CALC;
016    import static edu.nrao.sss.math.MathUtil.MC_INTERM_CALCS;
017    import static edu.nrao.sss.measure.Wave.LIGHT_SPEED_VACUUM_KM_PER_SEC;
018    
019    /**
020     * Units of linear velocity.
021     * <p>
022     * <b>Version Info:</b>
023     * <table style="margin-left:2em">
024     *   <tr><td>$Revision: 1586 $</td></tr>
025     *   <tr><td>$Date: 2008-10-01 10:38:49 -0600 (Wed, 01 Oct 2008) $</td></tr>
026     *   <tr><td>$Author: dharland $ (last person to modify)</td></tr>
027     * </table></p>
028     * 
029     * @author David M. Harland
030     * @since 2006-03-31
031     */
032    public class LinearVelocityUnits
033      implements Symbolic
034    {
035      private static final int PRECISION = MC_FINAL_CALC.getPrecision();
036      
037      private static final boolean IS_CASE_SENSITIVE;
038      static
039      {
040        IS_CASE_SENSITIVE =
041          DistanceUnits.getDefault().symbolsAreCaseSensitive() ||
042          TimeUnits.getDefault().symbolsAreCaseSensitive();
043      }
044    
045      private static final SortedMap<String, LinearVelocityUnits> UNITS =
046        new TreeMap<String, LinearVelocityUnits>();
047        
048      /**
049       * Returns linear velocity units for the given distance and time units.
050       * Successive calls to this method with the same parameters will return
051       * the same object, not new equal objects.
052       * 
053       * @param distanceUnits
054       *   units of distance, the numerator for the returned velocity units.
055       *   
056       * @param timeUnits
057       *   units of time, the denominator for the returned velocity units.
058       *   
059       * @return
060       *   linear velocity units of <tt>distanceUnits</tt> per <tt>timeUnits</tt>.
061       */
062      public static LinearVelocityUnits from(DistanceUnits distanceUnits,
063                                             TimeUnits     timeUnits)
064      {
065        String key = makeSymbol(distanceUnits, timeUnits);
066        
067        LinearVelocityUnits units = UNITS.get(key);
068        
069        if (units == null)
070        {
071          units = new LinearVelocityUnits(distanceUnits, timeUnits);
072          UNITS.put(key, units);
073        }
074        
075        return units;
076      }
077    
078      /**
079       * Returns a default unit of linear velocity.
080       * @return a default unit of linear velocity.
081       */
082      public static LinearVelocityUnits getDefault()
083      {
084        return KILOMETERS_PER_SECOND;
085      }
086    
087      /**
088       * This non-public method is here only to support unit testing and should
089       * not be employed for other purposes.  Calling this method erases this
090       * class's memory of which units have been created.  If called after clients
091       * have made velocity units, it will be possible for one client to have
092       * one m/s instance, and new clients to have a different m/s instance.
093       * Not a crisis, but wasteful, and in violation of comments in
094       * "from" method.
095       */
096      static void clearCache()  { UNITS.clear(); }
097      
098      //============================================================================
099      // FREQUENTLY USED UNITS
100      //============================================================================
101      
102      /** Meters per second. */
103      public static final LinearVelocityUnits METERS_PER_SECOND =
104        from(DistanceUnits.METER, TimeUnits.SECOND);
105      
106      /** Kilometers per second. */
107      public static final LinearVelocityUnits KILOMETERS_PER_SECOND =
108        from(DistanceUnits.KILOMETER, TimeUnits.SECOND);
109      
110      /**
111       * A redshift measurement related to velocity.
112       * <p>
113       * For more information on the relationship of <i>z</i>
114       * to velocity, see these references:
115       * <ul>
116       *   <li><a href="http://www.astro.ucla.edu/~wright/doppler.htm">
117       *                http://www.astro.ucla.edu/~wright/doppler.htm</a></li>
118       *   <li><a href="http://iram.fr/IRAMFR/ARN/may95/node4.html">
119       *                http://iram.fr/IRAMFR/ARN/may95/node4.html</a></li>
120       * </ul</p>
121       */
122      public static final LinearVelocityUnits Z = new LVU_Z();
123    
124      /**
125       * Returns a list of frequently used velocity units.
126       * This is a small subset of the total set of units that could
127       * be produced by all combinations of {@link DistanceUnits}
128       * and {@link TimeUnits}.
129       * 
130       * @return a list of frequently used velocity units.
131       */
132      public static List<LinearVelocityUnits> getFrequentlyUsedUnits()
133      {
134        List<LinearVelocityUnits> list = new ArrayList<LinearVelocityUnits>();
135        
136        list.add(METERS_PER_SECOND);
137        list.add(KILOMETERS_PER_SECOND);
138        list.add(Z);
139        
140        return list;
141      }
142      
143      //============================================================================
144      // INSTANCE VARIABLES, CONSTRUCTORS, & SIMPLE GETTERS
145      //============================================================================
146    
147      private DistanceUnits distUnits;
148      private TimeUnits     timeUnits;
149      private BigDecimal    multiplier;  //introduced in order to support "Z"
150      
151      //This no-arg constructor is here for mechanisms such as JAXB & Hibernate
152      private LinearVelocityUnits()
153      {
154        this(DistanceUnits.getDefault(), TimeUnits.getDefault(), BigDecimal.ONE);
155      }
156      
157      private LinearVelocityUnits(DistanceUnits distanceUnits, TimeUnits timeUnits)
158      {
159        this(distanceUnits, timeUnits, BigDecimal.ONE);
160      }
161    
162      private LinearVelocityUnits(DistanceUnits du, TimeUnits tu, BigDecimal mult)
163      {
164        distUnits  = du;
165        timeUnits  = tu;
166        multiplier = mult.setScale(PRECISION, RoundingMode.HALF_UP);
167      }
168      
169      /**
170       * Returns <i>true</i> if either the {@link DistanceUnits} or {@link TimeUnits}
171       * class has case-sensitive symbols.
172       */
173      public boolean symbolsAreCaseSensitive()
174      {
175        return IS_CASE_SENSITIVE;
176      }
177    
178      /**
179       * Returns the units of distance used by this unit of velocity.
180       * @return the units of distance used by this unit of velocity.
181       */
182      public DistanceUnits getDistanceUnits()
183      {
184        return distUnits;
185      }
186      
187      /**
188       * Returns the units of time used by this unit of velocity.
189       * @return the units of time used by this unit of velocity.
190       */
191      public TimeUnits getTimeUnits()
192      {
193        return timeUnits;
194      }
195    
196      //============================================================================
197      // CONVERSIONS TO OTHER UNITS
198      //============================================================================
199    
200      /**
201       * Returns a factor for converting from this unit to {@code otherUnits}.
202       * 
203       * @param otherUnits the unit to which conversion is desired.
204       * 
205       * @return a factor for converting from this unit to {@code otherUnits}.
206       */
207      public BigDecimal toUnits(LinearVelocityUnits otherUnits)
208      {
209        return convertTo(otherUnits, ONE);
210      }
211      
212      /**
213       * Non-public method for use by this and other classes in pkg.
214       * Does the actual conversion; keeps answer in BigDecimal form.
215       */
216      BigDecimal convertTo(LinearVelocityUnits otherUnits, BigDecimal value)
217      {
218        BigDecimal answer = value;
219    
220        if (!otherUnits.equals(this))
221        {
222          try  //to use BigDecimal, for better accuracy, but...
223          {
224            if (value.scale() < PRECISION)
225              value = value.setScale(PRECISION);
226            
227            //Strategy: use conversion methods of DistanceUnits and TimeUnits and
228            //then divide.
229            BigDecimal time = this.timeUnits.convertTo(otherUnits.timeUnits, ONE);
230            BigDecimal dist = this.distUnits.convertTo(otherUnits.distUnits, value);
231    
232            dist = dist.multiply(this.multiplier);
233            dist = dist.divide(otherUnits.multiplier, MC_INTERM_CALCS);
234            
235            answer = dist.divide(time, MC_FINAL_CALC);
236          }
237          catch (Exception ex)
238          {
239            //...if it fails, use java primitives
240            double distance =
241              this.distUnits.convertTo(otherUnits.distUnits, value).doubleValue();
242    
243            double time =
244              this.timeUnits.convertTo(otherUnits.timeUnits, ONE).doubleValue();
245            
246            double mult = this.multiplier.doubleValue() /
247                          otherUnits.multiplier.doubleValue();
248    
249            answer = BigDecimal.valueOf(mult * distance / time);
250          }
251        }
252        
253        return answer.round(MC_FINAL_CALC);
254      }
255    
256      //============================================================================
257      // TO / FROM TEXT
258      //============================================================================
259    
260      private static String makeSymbol(DistanceUnits du, TimeUnits tu)
261      {
262        return du.getSymbol() + '/' + tu.getSymbol();
263      }
264    
265      /**
266       * Returns the symbol for this unit.
267       * @return the symbol for this unit.
268       */
269      public String getSymbol()
270      {
271        return makeSymbol(distUnits, timeUnits);
272      }
273    
274      /**
275       * Returns the name of this unit.
276       * This method is here for historical reasons; it mimics the behavior
277       * this method had when this class had been an <tt>enum</tt> class.
278       * The {@link #toString()} is a better alternative.
279       * 
280       * @return the name of this unit.
281       */
282      public String name()
283      {
284        return distUnits.name() + "_PER_" + timeUnits.name();
285      }
286      
287      /**
288       * Returns a text representation of this unit of linear velocity.
289       * @return a text representation of this unit of linear velocity.
290       */
291      @Override
292      public String toString()
293      {
294        EnumerationUtility eu = EnumerationUtility.getSharedInstance();
295        
296        return eu.enumToString(distUnits) + " Per " + eu.enumToString(timeUnits);
297      }
298      
299      /**
300       * Returns the linear velocity units represented by {@code text}.
301       * <p>
302       * Two basic formats for <tt>text</tt> are employed, both based on the
303       * text representations of the component distance and time units.
304       * The first is of the form
305       * <tt><i>distUnits.symbol</i> / <i>timeUnits.symbol</i></tt>.
306       * The amount of space surrounding the "/" separator is not material;
307       * zero or more spaces are allowed on either side of the separator.
308       * The second is of the form
309       * <tt><i>distUnits.name</i> per <i>timeUnits.name</i></tt>.
310       * The case of the separator "per" is not material.  The same comment about
311       * whitespace given above applies here as well.
312       * The rules governing the format of the symbol or name of the component
313       * units are given in
314       * {@link EnumerationUtility#enumFromString(Class, String)}.</p>
315       * 
316       * @param text a text representation of a unit of linear velocity.
317       * 
318       * @return the linear velocity units represented by {@code text}.
319       * 
320       * @throws IllegalArgumentException
321       *   if <tt>text</tt> cannot be parsed to a valid units.
322       */
323      public static LinearVelocityUnits fromString(String text)
324      {
325        text = text.trim();
326        
327        LinearVelocityUnits units = null;
328        
329        //Special case: "Z"
330        if (text.toUpperCase().equals("Z"))
331        {
332          units = Z;
333        }
334        //See if we get lucky
335        else if (UNITS.containsKey(text))
336        {
337          units = UNITS.get(text);
338        }
339        //Parse text to create distance and time units
340        else
341        {
342          String separator = "/";
343    
344          if (!text.contains(separator)) //then try separator "per"
345          {
346            String upperText = text.toUpperCase();
347            int sepIndex = upperText.indexOf(" PER ");
348            if (sepIndex > -1)
349            {
350              separator = text.substring(sepIndex, sepIndex+" PER ".length());
351            }
352            else
353            {
354              sepIndex = upperText.indexOf("_PER_");
355              if (sepIndex > -1)
356              {
357                separator = text.substring(sepIndex, sepIndex+" PER ".length());
358              }
359              else
360              {
361                separator = null;
362              }
363            }
364          }
365          
366          if (separator != null)
367          {
368            String[] tokens = text.split(separator);
369    
370            EnumerationUtility eu = EnumerationUtility.getSharedInstance();
371    
372            TimeUnits     tu = eu.enumFromString(    TimeUnits.class, tokens[1]);
373            DistanceUnits du = eu.enumFromString(DistanceUnits.class, tokens[0]);
374            
375            if (du == null)
376            {
377              //Distance units c/b plural; make singular
378              int indexLast = tokens[0].length() - 1;
379              if (tokens[0].toUpperCase().charAt(indexLast) == 'S')
380              {
381                tokens[0] = tokens[0].substring(0, indexLast);
382                du = eu.enumFromString(DistanceUnits.class, tokens[0]);
383              }
384            }
385    
386            if (du == null || tu == null)
387              throwBadUnitsText(text, du, tu);
388            
389            units = from(du, tu);
390          }
391          else //no separator
392          {
393            throw new IllegalArgumentException("Text '" + text +
394              "' could not be recognized as a unit of velocity.");
395          }
396        }
397        
398        return units;
399      }
400    
401      public static void throwBadUnitsText(String text, DistanceUnits du, TimeUnits tu)
402        throws IllegalArgumentException
403      {
404        StringBuilder msg = new StringBuilder();
405        
406        msg.append("Text '").append(text);
407        msg.append("' could not be recognized as a unit of velocity.  ");
408        if (du != null)
409        {
410          msg.append("The distance units were recognized as '").append(du.name());
411          if (tu == null)
412          {
413            msg.append("', but the time units were not recognized.");
414          }
415          else //tu not null, du not null
416          {
417            msg.append("', and the time units were recognized as '").append(tu.name());
418            msg.append("', but the combination was not recognized.  ");
419            msg.append("This could be a PROGRAMMER ERROR.");
420          }
421        }
422        else //du is null
423        {
424          if (tu != null)
425          {
426            msg.append("The time units were recognized as '").append(tu.name());
427            msg.append("', but the distance units were not recognized.");
428          }
429          else //both null
430          {
431            msg.append("Neither the distance units nor the time units were recognized.");
432          }
433        }
434        
435        throw new IllegalArgumentException(msg.toString());
436      }
437      
438      //============================================================================
439      // 
440      //============================================================================
441    
442      /** Returns <i>true</i> if <tt>o</tt> is equal to this units. */
443      @Override
444      public boolean equals(Object o)
445      {
446        //Quick exit if o is this
447        if (o == this)
448          return true;
449        
450        //Quick exit if o is null
451        if (o == null)
452          return false;
453        
454        //Quick exit if classes are different
455        if (!o.getClass().equals(this.getClass()))
456          return false;
457    
458        LinearVelocityUnits other = (LinearVelocityUnits)o;
459        
460        return other.getSymbol().equals(this.getSymbol());
461      }
462      
463      /** Returns a hash code for this units. */
464      @Override
465      public int hashCode()
466      {
467        return getSymbol().hashCode();
468      }
469    
470      //============================================================================
471      // 
472      //============================================================================
473    
474      /** Special subclass for "Z"; overrides a few methods. */
475      private static class LVU_Z extends LinearVelocityUnits
476      {
477        private LVU_Z()
478        {
479          super(DistanceUnits.KILOMETER, TimeUnits.SECOND, LIGHT_SPEED_VACUUM_KM_PER_SEC);
480        }
481    
482        @Override public String getSymbol()  { return "Z"; }
483        @Override public String toString()   { return "Z"; }
484      }
485    
486      //============================================================================
487      // 
488      //============================================================================
489      /*
490      public static void main(String... args) throws Exception
491      {
492        LinearVelocityUnits lvu;
493    
494        //Build from good symbols
495        for (DistanceUnits du : DistanceUnits.values())
496        {
497          for (TimeUnits tu : TimeUnits.values())
498          {
499            String symbol = makeSymbol(du, tu);
500            lvu = fromString(symbol);
501            System.out.println(lvu.getSymbol() + ", " + lvu.toString());
502          }
503        }
504        lvu = fromString("Z");
505        System.out.println(lvu.getSymbol() + ", " + lvu.toString());
506        
507        //Clear memory of constructed units
508        LinearVelocityUnits.UNITS.clear();
509        System.out.println("--------------------------------------------------------");
510        
511        //Build from good names
512        for (DistanceUnits du : DistanceUnits.values())
513        {
514          for (TimeUnits tu : TimeUnits.values())
515          {
516            String name = du.toString() + " per " + tu.toString();
517            lvu = fromString(name);
518            System.out.println(lvu.getSymbol() + ", " + lvu.toString());
519          }
520        }
521      }
522      */
523    }