001    package edu.nrao.sss.measure;
002    
003    import java.math.BigDecimal;
004    import java.math.RoundingMode;
005    
006    import static edu.nrao.sss.math.MathUtil.MC_FINAL_CALC;
007    import static edu.nrao.sss.math.MathUtil.MC_INTERM_CALCS;
008    
009    import edu.nrao.sss.util.EnumerationUtility;
010    import edu.nrao.sss.util.Symbolic;
011    
012    /**
013     * Units of arc.
014     * <p>
015     * <b><u>Table of Units</u></b></p>
016     * <p style="margin-left:1.5em">
017     * <table border="1" cellspacing="0">
018     *   <tr BGCOLOR="#EEEEFF"><th>Element</th>  <th>Name<sup><i>1</i></sup></th>
019     *       <th>Symbol<sup><i>1</i></sup></th> <th>HTML Symbol<sup><i>2</i></sup></th>
020     *       <th>Units per Circle</th></tr>
021     *   <tr><td>DEGREE</td><td>DEGREE</td>
022     *       <td align="center">d <sup><i>3</i></sup></td>
023     *       <td align="center">&amp;#x00B0; (&#x00B0;)</td>
024     *       <td align="right">360.0</td></tr>
025     *   <tr><td>RADIAN</td><td>RADIAN</td>
026     *       <td align="center">rad</td>
027     *       <td align="center">rad</td>
028     *       <td align="right">2&#x03C0;</td></tr>
029     *   <tr><td>PERCENT</td><td>PERCENT</td>
030     *       <td align="center">%</td>
031     *       <td align="center">&amp#x0025; (&#x0025;)</td>
032     *       <td align="right">100.0</td></tr>
033     *   <tr><td>HOUR</td><td>HOUR</td>
034     *       <td align="center">h</td>
035     *       <td align="center">h</td>
036     *       <td align="right">24.0</td></tr>
037     *   <tr><td>MINUTE</td><td>MINUTE</td>
038     *       <td align="center">m</td>
039     *       <td align="center">m</td>
040     *       <td align="right">1,440.0</td></tr>
041     *   <tr><td>SECOND</td><td>SECOND</td>
042     *       <td align="center">s</td>
043     *       <td align="center">s</td>
044     *       <td align="right">86,400.0</td></tr>
045     *   <tr><td>ARC_MINUTE</td><td>ARC_MINUTE</td>
046     *       <td align="center">'</td>
047     *       <td align="center">&amp;#x0027; (&#x0027;)</td>
048     *       <td align="right">21,600.0</td></tr>
049     *   <tr><td>ARC_SECOND</td><td>ARC_SECOND</td>
050     *       <td align="center">"</td>
051     *       <td align="center">&amp;quot; (&quot;)</td>
052     *       <td align="right">1,296,000.0</td></tr>
053     *   <tr><td>MILLI_ARC_SECOND</td><td>MILLI_ARC_SECOND</td>
054     *       <td align="center">mas</td>
055     *       <td align="center">mas</td>
056     *       <td align="right">1,296,000,000.0</td></tr>
057     * </table>
058     * <sup><b>1</b></sup>The names in this column may be sent to
059     * {@link #fromString(String)}.  Note that the names are
060     * not case sensitive.<br/>
061     * <sup><b>2</b></sup>The symbols for arc min and arc sec are sometimes
062     * troublesome in HTML (and in other arenas, for that matter).  These
063     * particular symbols are placed in ampersand form.<br/>
064     * <sup><b>3</b></sup>The original plan was to use '&#x00B0;' as the symbol,
065     * but 'd' is an easier identifier for users to furnish.</p>
066     * <p>
067     * <b><u>Table of Conversion Factors</u><sup>4</sup></b></p>
068     * <p style="margin-left:1.5em">
069     * <table border="1" cellspacing="0">
070     * <tr BGCOLOR="#EEEEFF"><th>Element</th><th>d</th><th>rad</th><th>%</th><th>h</th><th>m</th><th>s</th><th>'</th><th>"</th><th>mas</th></tr>
071     * <tr><td>DEGREE</td><td align="right">1.0</td><td align="right">0.01745329251994329444444444444444444</td><td align="right">0.2777777777777777777777777777777778</td><td align="right">0.06666666666666666666666666666666667</td><td align="right">4.0</td><td align="right">240.0</td><td align="right">60.0</td><td align="right">3600.0</td><td align="right">3600000.0</td></tr>
072     * <tr><td>RADIAN</td><td align="right">57.29577951308232522583526558752712</td><td align="right">1.0</td><td align="right">15.91549430918953478495424044097976</td><td align="right">3.819718634205488348389017705835142</td><td align="right">229.1831180523293009033410623501085</td><td align="right">13750.98708313975805420046374100651</td><td align="right">3437.746770784939513550115935251627</td><td align="right">206264.8062470963708130069561150976</td><td align="right">206264806.2470963708130069561150976</td></tr>
073     * <tr><td>PERCENT</td><td align="right">3.6</td><td align="right">0.06283185307179586</td><td align="right">1.0</td><td align="right">0.24</td><td align="right">14.4</td><td align="right">864.0</td><td align="right">216.0</td><td align="right">12960.0</td><td align="right">12960000.0</td></tr>
074     * <tr><td>HOUR</td><td align="right">15.0</td><td align="right">0.2617993877991494166666666666666667</td><td align="right">4.166666666666666666666666666666667</td><td align="right">1.0</td><td align="right">60.0</td><td align="right">3600.0</td><td align="right">900.0</td><td align="right">54000.0</td><td align="right">54000000.0</td></tr>
075     * <tr><td>MINUTE</td><td align="right">0.25</td><td align="right">0.004363323129985823611111111111111111</td><td align="right">0.06944444444444444444444444444444444</td><td align="right">0.01666666666666666666666666666666667</td><td align="right">1.0</td><td align="right">60.0</td><td align="right">15.0</td><td align="right">900.0</td><td align="right">900000.0</td></tr>
076     * <tr><td>SECOND</td><td align="right">0.004166666666666666666666666666666667</td><td align="right">0.00007272205216643039351851851851851852</td><td align="right">0.001157407407407407407407407407407407</td><td align="right">0.0002777777777777777777777777777777778</td><td align="right">0.01666666666666666666666666666666667</td><td align="right">1.0</td><td align="right">0.25</td><td align="right">15.0</td><td align="right">15000.0</td></tr>
077     * <tr><td>ARC_MINUTE</td><td align="right">0.01666666666666666666666666666666667</td><td align="right">0.0002908882086657215740740740740740741</td><td align="right">0.004629629629629629629629629629629630</td><td align="right">0.001111111111111111111111111111111111</td><td align="right">0.06666666666666666666666666666666667</td><td align="right">4.0</td><td align="right">1.0</td><td align="right">60.0</td><td align="right">60000.0</td></tr>
078     * <tr><td>ARC_SECOND</td><td align="right">0.0002777777777777777777777777777777778</td><td align="right">0.000004848136811095359567901234567901235</td><td align="right">0.00007716049382716049382716049382716049</td><td align="right">0.00001851851851851851851851851851851852</td><td align="right">0.001111111111111111111111111111111111</td><td align="right">0.06666666666666666666666666666666667</td><td align="right">0.01666666666666666666666666666666667</td><td align="right">1.0</td><td align="right">1000.0</td></tr>
079     * <tr><td>MILLI_ARC_SECOND</td><td align="right">2.777777777777777777777777777777778E-7</td><td align="right">4.848136811095359567901234567901235E-9</td><td align="right">7.716049382716049382716049382716049E-8</td><td align="right">1.851851851851851851851851851851852E-8</td><td align="right">0.000001111111111111111111111111111111111</td><td align="right">0.00006666666666666666666666666666666667</td><td align="right">0.00001666666666666666666666666666666667</td><td align="right">0.001</td><td align="right">1.0</td></tr>
080     * </table>
081     * <sup><b>4</b></sup>This table was generated from the conversion logic of this class.<br/>
082     * </p>
083     * <p>
084     * <b>Version Info:</b>
085     * <table style="margin-left:2em">
086     *   <tr><td>$Revision: 1586 $</td></tr>
087     *   <tr><td>$Date: 2008-10-01 10:38:49 -0600 (Wed, 01 Oct 2008) $</td></tr>
088     *   <tr><td>$Author: dharland $</td></tr>
089     * </table></p>
090     *  
091     * @author David M. Harland
092     * @since 2006-04-12
093     */
094    public enum ArcUnits
095      implements Symbolic
096    {
097      /**
098       * A degree.  There are 360 degrees in a full circle.
099       */
100      DEGREE("360.0", "d", "&#x00B0;"),
101      
102      /**
103       * A radian.  There are 2&#x03C0; radians in a full circle.
104       */
105      RADIAN("6.2831853071795864769252867665590057683943387987502", "rad", "rad"),
106    
107      /**
108       * Percent of a full circle.  A full circle is 100%.
109       */
110      PERCENT("100.0", "%", "&#x0025;"),
111      
112      /**
113       * An hour angle.  There are 24 hours in a full circle.
114       */
115      HOUR("24.0", "h", "h"),
116      
117      /**
118       * One sixtieth of an hour angle.
119       * Contrast this unit of measure with {@link #ARC_MINUTE}.
120       */
121      MINUTE("1440.0", "m", "m"),
122      
123      /**
124       * One sixtieth of a minute.
125       * Contrast this unit of measure with {@link #ARC_SECOND}.
126       */
127      SECOND("86400.0", "s", "s"),
128      
129      /**
130       * One sixtieth of a degree.
131       * Contrast this unit of measure with {@link #MINUTE}.
132       */
133      ARC_MINUTE("21600.0", "'", "&#x0027;"),
134      
135      /**
136       * One sixtieth of an arc minute.
137       * Contrast this unit of measure with {@link #SECOND}.
138       */
139      ARC_SECOND("1296000.0", "\"", "&quot;"),
140      
141      /**
142       * One thousandth of an arc second.
143       */
144      MILLI_ARC_SECOND("1296000000.0", "mas", "mas");
145      
146      private static final int PRECISION = MC_FINAL_CALC.getPrecision(); 
147    
148      private BigDecimal completeCircle;
149      private String     symbol;
150      private String     htmlSymbol;
151      
152      private ArcUnits(String completeCircle, String symbol, String htmlSymbol)
153      {
154        this.symbol         = symbol;
155        this.htmlSymbol     = htmlSymbol;
156        this.completeCircle = new BigDecimal(completeCircle);
157        
158        //It turns out that the PRECISION constant is zero when we're
159        //in this constructor, so we make the call directly.
160        //(See http://forums.java.net/jive/thread.jspa?threadID=40585&tstart=0)
161        this.completeCircle =
162          this.completeCircle.setScale(MC_FINAL_CALC.getPrecision(),
163                                       RoundingMode.HALF_UP);
164      }
165      
166      /** Returns <i>false</i> -- these symbols are <i>not</i> case sensitive. */
167      public boolean symbolsAreCaseSensitive()  { return false; }
168    
169      /**
170       * Returns the symbol for this unit.
171       * For example, the symbol for {@code DEGREE} is <i>d</i>.
172       * 
173       * @return the symbol for this unit.
174       */
175      public String getSymbol()
176      {
177        return symbol;
178      }
179      
180      /**
181       * Returns a symbol for this unit that may be more appropriate
182       * for use in HTML than the main symbol.  This is particularly
183       * true for {@link #ARC_MINUTE} and {@link #ARC_SECOND}.  For
184       * most units, the HTML symbol is the same as the main symbol.
185       * 
186       * @return a symbol for this unit that is HTML-friendly.
187       */
188      public String getHtmlSymbol()
189      {
190        return htmlSymbol;
191      }
192      
193      /**
194       * Returns the number of these units in a full circle.
195       * 
196       * @return the number of these units in a full circle.
197       */
198      public BigDecimal toFullCircle()
199      {
200        return completeCircle;
201      }
202      
203      /**
204       * Returns the number of these units in one half of a circle.
205       * This is useful for calculating supplementarty angles.
206       * 
207       * @return the number of these units in one half of a circle.
208       */
209      public BigDecimal toHalfCircle()
210      {
211        final BigDecimal oneHalf = new BigDecimal("0.5"); 
212        return completeCircle.multiply(oneHalf);
213      }
214      
215      /**
216       * Returns the number of these units in one quarter a circle.
217       * This is useful for calculating complementarty angles.
218       * 
219       * @return the number of these units in one quarter a circle.
220       */
221      public BigDecimal toQuarterCircle()
222      {
223        final BigDecimal oneQtr = new BigDecimal("0.25"); 
224        return completeCircle.multiply(oneQtr);
225      }
226    
227      /**
228       * Returns a factor for converting from this unit to {@code otherUnits}.
229       * 
230       * @param otherUnits the unit to which conversion is desired.
231       * 
232       * @return a factor for converting from this unit to {@code otherUnits}.
233       */
234      public BigDecimal toUnits(ArcUnits otherUnits)
235      {
236        return convertTo(otherUnits, BigDecimal.ONE);
237      }
238      
239      /**
240       * Non-public method for use by this and other classes in pkg.
241       * Tries to keep all calcs in BigDecimal form, but if that fails,
242       * reverts to java primitives.
243       */
244      BigDecimal convertTo(ArcUnits otherUnits, BigDecimal value)
245      {
246        BigDecimal answer = value;
247        
248        //Convert only if we have different units
249        if (!otherUnits.equals(this))
250        {
251          try  //to use BigDecimal, for better accuracy, but...
252          {
253            if (value.scale() < PRECISION)
254              value = value.setScale(PRECISION);
255            
256            BigDecimal ratio =
257              otherUnits.completeCircle.divide(this.completeCircle,
258                                               MC_INTERM_CALCS);
259            answer = value.multiply(ratio, MC_FINAL_CALC);
260          }
261          catch (ArithmeticException ex)
262          {
263            //...if it fails, use java primitives
264            double ratio = otherUnits.completeCircle.doubleValue() /
265                                 this.completeCircle.doubleValue();
266            
267            answer = BigDecimal.valueOf(value.doubleValue() * ratio);
268          }
269        }
270        
271        return answer.round(MC_FINAL_CALC);
272      }
273      
274      /**
275       * Converts {@code value}, expressed in this unit, to degrees-minutes-seconds.
276       * 
277       * @param value the quantity, in this unit, to be converted to DMS.
278       * 
279       * @return an array of size three in this order:
280       *         <ol start="0">
281       *           <li>An integral number of degrees.</li>
282       *           <li>An integral number of arc minutes.</li>
283       *           <li>A real number of arc seconds.</li>
284       *         </ol>
285       */
286      public Number[] convertToDms(BigDecimal value)
287      {
288        Number[] arc = new Number[3];
289        
290        //Calculate total seconds of arc
291        BigDecimal arcSeconds = convertTo(ARC_SECOND, value);
292        
293        //Work with positive numbers until last step
294        boolean negate = false;
295        if (arcSeconds.signum() < 0)
296        {
297          negate = true;
298          arcSeconds = arcSeconds.negate();
299        }
300    
301        //Get the normalized arc seconds (eg, range [0.0-60.0))
302        BigDecimal factor = ARC_MINUTE.toUnits(ARC_SECOND);
303        BigDecimal[] intAndRem = arcSeconds.divideAndRemainder(factor,
304                                                               MC_FINAL_CALC);
305        arc[2] = intAndRem[1];
306    
307        //Get the normalized integral arc minutes (eg, range [0-59])
308        factor = DEGREE.toUnits(ARC_MINUTE);
309        intAndRem = intAndRem[0].divideAndRemainder(factor, MC_FINAL_CALC);
310        arc[1] = intAndRem[1];
311        
312        //Get the integral arc degrees
313        arc[0] = intAndRem[0];
314        
315        //Handle negation
316        if (negate)
317          negateForConvertToXms(arc);
318    
319        return arc;
320      }
321      
322      /**
323       * Converts {@code value}, expressed in this unit, to hours-minutes-seconds.
324       * 
325       * @param value the quantity, in this unit, to be converted to DMS.
326       * 
327       * @return an array of size three in this order:
328       *         <ol start="0">
329       *           <li>An integral number of degrees.</li>
330       *           <li>An integral number of minutes.</li>
331       *           <li>A real number of seconds.</li>
332       *         </ol>
333       */
334      public Number[] convertToHms(BigDecimal value)
335      {
336        Number[] arc = new Number[3];
337        
338        //Calculate total angle-seconds
339        BigDecimal seconds = convertTo(SECOND, value);
340    
341        //Work with positive numbers until last step
342        boolean negate = false;
343        if (seconds.signum() < 0)
344        {
345          negate = true;
346          seconds = seconds.negate();
347        }
348    
349        //Get the normalized angle-seconds (eg, range [0.0-60.0))
350        BigDecimal factor = MINUTE.toUnits(SECOND);
351        BigDecimal[] intAndRem = seconds.divideAndRemainder(factor,
352                                                            MC_FINAL_CALC);
353        arc[2] = intAndRem[1];
354    
355        //Get the normalized integral angle-minutes (eg, range [0-59])
356        factor = HOUR.toUnits(MINUTE);
357        intAndRem = intAndRem[0].divideAndRemainder(factor, MC_FINAL_CALC);
358        arc[1] = intAndRem[1];
359        
360        //Get the integral angle-hours
361        arc[0] = intAndRem[0];
362        
363        //Handle negation
364        if (negate)
365          negateForConvertToXms(arc);
366    
367        return arc;
368      }
369    
370      /** Handles negation of H/D, M, S for convertToH/Dms methods. */
371      private void negateForConvertToXms(Number[] arc)
372      {
373        //Only one of H/D, M, S can be negative
374        if (arc[0].intValue() > 0) {
375          arc[0] = new Integer(-arc[0].intValue());
376        }
377        else if (arc[1].intValue() > 0) {
378          arc[1] = new Integer(-arc[1].intValue());
379        }
380        else {
381          arc[2] = new BigDecimal(arc[2].toString()).negate();
382        }
383      }
384      
385      /**
386       * Converts from degrees-minutes-seconds to {@code otherUnits}.
387       * <p>
388       * At most, only one of {@code degrees}, {@code minutes}, or {@code seconds}
389       * may be negative.  Further more, if one of these is negative, the higher
390       * units must all be zero.  E.g., in order for {@code seconds} to be negative
391       * both {@code degrees} and {@code minutes} must be zero.  If these conditions
392       * are not met, an {@code IllegalArgumentException} is thrown.</p>
393       * 
394       * @param otherUnits the units in which the value is returned.
395       * @param degrees the whole number of degrees of arc.
396       * @param minutes the whole number of minutes of arc. The normal
397       *                range for this value is [0-59].
398       * @param seconds the whole and fraction number of seconds of arc.
399       *                The normal range for this value is [0.0-60.0).
400       * @return the value of {@code degrees, minutes, seconds} converted
401       *         to {@code otherUnits}.
402       * 
403       * @throws IllegalArgumentException if the rules about negative parameters
404       *           described above are violated.
405       */
406      public static BigDecimal convertDmsTo(ArcUnits otherUnits, int degrees,
407                                            int minutes, BigDecimal seconds)
408      {
409        //Determine if negation is needed and do some parameter validation
410        boolean negate =
411          convertXmsToUnitsNeedsNegation(degrees, minutes, seconds, "degrees");
412        
413        //Work with positive numbers until last step
414        if (negate)
415        {
416          if      (seconds.signum() < 0)  seconds =  seconds.negate();
417          else if (minutes          < 0)  minutes = -minutes;
418          else if (degrees          < 0)  degrees = -degrees;
419        }
420    
421        BigDecimal secondsM = ARC_MINUTE.convertTo(ARC_SECOND, new BigDecimal(minutes));
422        BigDecimal secondsD =     DEGREE.convertTo(ARC_SECOND, new BigDecimal(degrees));
423        
424        BigDecimal totalSeconds = secondsD.add(secondsM).add(seconds);
425        
426        BigDecimal result = ARC_SECOND.convertTo(otherUnits, totalSeconds);
427        
428        if (negate)
429          result = result.negate();
430        
431        return result;
432      }
433      
434      /**
435       * Converts from hours-minutes-seconds to {@code otherUnits}.
436       * <p>
437       * At most, only one of {@code hours}, {@code minutes}, or {@code seconds}
438       * may be negative.  Further more, if one of these is negative, the higher
439       * units must all be zero.  E.g., in order for {@code seconds} to be negative
440       * both {@code hours} and {@code minutes} must be zero.  If these conditions
441       * are not met, an {@code IllegalArgumentException} is thrown.</p>
442       * 
443       * @param otherUnits the units in which the value is returned.
444       * @param hours the whole number of angle hours.
445       * @param minutes the whole number of angle minutes. The normal
446       *                range for this value is [0-59].
447       * @param seconds the whole and fraction number of angle seconds.
448       *                The normal range for this value is [0.0-60.0).
449       * @return the value of {@code hours, minutes, seconds} converted
450       *         to {@code otherUnits}.
451       * 
452       * @throws IllegalArgumentException if the rules about negative parameters
453       *           described above are violated.
454       */
455      public static BigDecimal convertHmsTo(ArcUnits otherUnits, int hours,
456                                            int minutes, BigDecimal seconds)
457      {
458        //Determine if negation is needed and do some parameter validation
459        boolean negate =
460          convertXmsToUnitsNeedsNegation(hours, minutes, seconds, "hours");
461        
462        //Work with positive numbers until last step
463        if (negate)
464        {
465          if      (seconds.signum() < 0)  seconds =  seconds.negate();
466          else if (minutes          < 0)  minutes = -minutes;
467          else if (hours            < 0)  hours   = -hours;
468        }
469    
470        BigDecimal secondsM = MINUTE.convertTo(SECOND, new BigDecimal(minutes));
471        BigDecimal secondsH =   HOUR.convertTo(SECOND, new BigDecimal(hours));
472        
473        BigDecimal totalSeconds = secondsH.add(secondsM).add(seconds);
474    
475        BigDecimal result = SECOND.convertTo(otherUnits, totalSeconds);
476        
477        if (negate)
478          result = result.negate();
479        
480        return result;
481      }
482      
483      /** Returns true if x or m or s is negative. Peforms some validation. */
484      private static boolean convertXmsToUnitsNeedsNegation(int x, int m, BigDecimal s,
485                                                            String type)
486      {
487        boolean negate = false;
488        
489        //If seconds are negative, minutes and hours/degrees must be zero
490        if (s.signum() < 0)
491        {
492          if ((x != 0) || (m != 0))
493            throw new IllegalArgumentException(
494              "If seconds < 0.0, " + type + " and minutes must be 0.");
495          
496          negate  = true;
497        }
498        //If minutes are negative, hours/degrees must be zero
499        else if (m < 0)
500        {
501          if (x != 0)
502            throw new IllegalArgumentException(
503              "If minutes < 0.0, " + type + " must be 0.");
504          
505          negate  = true;
506        }
507        else if (x < 0)
508        {
509          negate = true;
510        }
511        
512        return negate;
513      }
514      
515      /**
516       * Returns a default unit of arc.
517       * @return a default unit of arc.
518       */
519      public static ArcUnits getDefault()
520      {
521        return DEGREE;
522      }
523      
524      /**
525       * Returns a text representation of this enumeration constant.
526       * @return a text representation of this enumeration constant.
527       */
528      public String toString()
529      {
530        return EnumerationUtility.getSharedInstance().enumToString(this);
531      }
532      
533      /**
534       * Returns the arc units represented by {@code text}.
535       * <p>
536       * For details about the transformation, see
537       * {@link EnumerationUtility#enumFromString(Class, String)}.</p>
538       * 
539       * @param text a text representation of a unit of arc.
540       * 
541       * @return the arc units represented by {@code text}.
542       */
543      public static ArcUnits fromString(String text)
544      {
545        return EnumerationUtility.getSharedInstance()
546                                 .enumFromString(ArcUnits.class, text);
547      }
548    
549      //Here for testing only; builds HTML table for use in javadoc class comments.
550      /*
551      private static String toHtmlTable()
552      {
553        StringBuilder table = new StringBuilder();
554        
555        //Column headers
556        table.append(" * <table border=\"1\" cellspacing=\"0\">\n");
557        table.append(" * <tr BGCOLOR=\"#EEEEFF\"><th>Element</th>");
558        for (ArcUnits u : ArcUnits.values())
559          table.append("<th>").append(u.getSymbol()).append("</th>");
560        table.append("</tr>\n");
561    
562        //Data rows
563        for (ArcUnits from : ArcUnits.values())
564        {
565          table.append(" * <tr><td>").append(from.name()).append("</td>");
566          
567          for (ArcUnits to : ArcUnits.values())
568            table.append("<td align=\"right\">").append(from.convertTo(to)).append("</td>");
569          
570          table.append("</tr>\n");
571        }
572        
573        //End table
574        table.append(" * </table>");
575        
576        return table.toString();
577      }
578      
579      public static void main(String[] args)
580      {
581        String htmlTable = ArcUnits.toHtmlTable();
582        
583        System.out.println(htmlTable);
584      }
585      */
586    }