DMS.java 9.09 KB
/*
 * $Id: DMS.java,v 1.4 2009/04/21 13:31:17 abrighto Exp $
 */

package jsky.coords;

import jsky.util.StringUtil;

import java.io.Serializable;
import java.text.NumberFormat;
import java.util.Locale;
import java.util.StringTokenizer;

/**
 * Class representing a value of the form "deg:min:sec".
 *
 * @author Allan Brighton
 * @version $Revision: 1.4 $
 */
public class DMS implements Serializable {

    private static final long serialVersionUID = 1L;

    /**
     * On the handling of -0: from the javadoc for Double.equals():
     * "If d1 represents +0.0 while d2 represents -0.0, or vice versa,
     * the equal test has the value false, even though +0.0==-0.0 has the
     * value true."
     * The test for 0.0 != -0.0 only works with Double.equals(minusZero).
     * This case shows up in DMS values with zero degrees and negative values,
     * such as "-00 24 32"
     */
    private static final Double MINUS_ZERO = -0.0;

    // Number formats for 2 digit degrees and minutes
    private static final NumberFormat NF = NumberFormat.getInstance(Locale.US);

    // Number formats for seconds
    private static final NumberFormat NF_SEC = NumberFormat.getInstance(Locale.US);

    static {
        NF.setMinimumIntegerDigits(2);
        NF.setMaximumIntegerDigits(2);
        NF.setMaximumFractionDigits(0);

        NF_SEC.setMinimumIntegerDigits(2);
        NF_SEC.setMaximumIntegerDigits(2);
        NF_SEC.setMinimumFractionDigits(2);
        NF_SEC.setMaximumFractionDigits(2);
    }

    /**
     * number of degrees
     */
    private int degrees;

    /**
     * number of minutes
     */
    private int min;

    /**
     * number of seconds
     */
    private double sec;

    /**
     * value converted to decimal
     */
    private double val;

    /**
     * set to 1 or -1
     */
    private byte sign = 1;

    /* true if value has been initialized */
    private boolean initialized = false;


    /**
     * Default constructor: initialize to null values
     */
    public DMS() {
    }

    /**
     * Initialize with the given degrees, minutes and seconds.
     */
    public DMS(double degrees, int min, double sec) {
        set(degrees, min, sec);
    }

    /**
     * Initialize from a decimal value and calculate H:M:S.sss.
     */
    public DMS(double val) {
        setVal(val);
    }

    /**
     * Copy constructor
     */
    public DMS(DMS hms) {
        setVal(hms.val);
    }

    /**
     * Initialize from a string value, in format H:M:S.sss, hh, d.ddd, or
     * H M S.
     */
    public DMS(String s) {
        s = StringUtil.replace(s, ",", "."); // Treat ',' like '.', by request
        double[] vals = {0.0, 0.0, 0.0};
        StringTokenizer tok = new StringTokenizer(s, ": ");
        int n = 0;
        while (n < 3 && tok.hasMoreTokens()) {
            vals[n++] = Double.valueOf(tok.nextToken());
        }

        if (n >= 2) {
            set(vals[0], (int) vals[1], vals[2]);
        } else if (n == 1) {
            setVal(vals[0]);
        } else {
            throw new RuntimeException("Expected a string of the form hh:mm:ss.sss, but got: '" + s + "'");
        }
    }


    /**
     * Set the degrees, minutes and seconds.
     */
    public void set(double degrees, int min, double sec) {
        this.degrees = (int) degrees;
        this.min = min;
        this.sec = sec;

        val = (sec / 60.0 + min) / 60.0;

        if (degrees < 0.0 || new Double(degrees).equals(MINUS_ZERO)) {
            val = degrees - val;
            this.degrees = -this.degrees;
            sign = -1;
        } else {
            val = this.degrees + val;
            sign = 1;
        }
        initialized = true;
    }

    /**
     * Set from a decimal value and calculate H:M:S.sss.
     */
    public void setVal(double val) {
        this.val = val;

        double v = val; // check also for neg zero
        if (v < 0.0 || new Double(v).equals(MINUS_ZERO)) {
            sign = -1;
            v = -v;
        } else {
            sign = 1;
        }

        double dd = v + 0.0000000001;
        degrees = (int) dd;
        double md = (dd - degrees) * 60.;
        min = (int) md;
        sec = (md - min) * 60.;
        initialized = true;
    }

    /**
     * Return the value as a String in the form hh:mm:ss.sss.
     * Seconds are formatted with leading zero if needed.
     * The seconds are formatted with 2 digits precission.
     */
    public String toString() {

        // paulbalm6 - Feb 2011: If sec is 59.9999, it will be formatted here as
        // 60.0, e.g. +00:59:60.
        // If this happens, we need to roll the minutes (and possibly the
        // degrees) to the next value. The correct representation of the above
        // is +01:00:00 (and not +00:60:00!).
        String secStr = NF_SEC.format(sec);
        int min_copy = min;
        int degrees_copy = degrees;

        if (NF_SEC.format(60).equals(secStr)) {
            secStr = NF_SEC.format(0);
            // Roll minutes, possibly degrees.
            // Note that the roll here is independent of the sign (always +1).
            min_copy++;
            if (min_copy == 60) {
                min_copy = 0;
                degrees_copy++;
            }

        }

        // sign
        String signStr;
        if (sign == -1) {
            signStr = "-";
        } else {
            signStr = "+";
        }

        return signStr
                + NF.format(degrees_copy)
                + ":"
                + NF.format(min_copy)
                + ":"
                + secStr;
    }

    /**
     * Return the value as a String in the form dd:mm:ss.sss,
     * or if showSeconds is false, dd:mm.
     */
    public String toString(boolean showSeconds) {
        if (showSeconds) {
            return toString();
        }

        // sign
        String signStr;
        if (sign == -1) {
            signStr = "-";
        } else {
            signStr = "+";
        }

        return signStr
                + NF.format(degrees)
                + ":"
                + NF.format(min);
    }

    /**
     * Return true if this object has been initialized with a valid value
     */
    public boolean isInitialized() {
        return initialized;
    }

    /**
     * Return the number of degrees (not including minutes or seconds)
     */
    public int getDegrees() {
        return degrees;
    }

    /**
     * Return the number of minutes (not including degrees or seconds)
     */
    public int getMin() {
        return min;
    }

    /**
     * Return the number of seconds (not including degrees and minutes)
     */
    public double getSec() {
        return sec;
    }

    /**
     * Return the value (fractional number of degrees) as a double
     */
    public double getVal() {
        return val;
    }

    /**
     * Return the sign of the value
     */
    public byte getSign() {
        return sign;
    }

    /**
     * Define equality based on the value
     */
    public boolean equals(Object obj) {
        return (obj instanceof DMS && val == ((DMS) obj).val);
    }


    /**
     * Test cases
     */
    public static void main(String[] args) {

        DMS d = new DMS(3, 19, 48.23);
        System.out.println("DMS(3, 19, 48.23) == " + d + " == " + d.getVal());

        if (!(d.equals(new DMS(d.getVal())))) {
            System.out.println("Equality test failed: " + d + " != " + new DMS(d.getVal()));
        }

        d = new DMS(41, 30, 42.2);
        System.out.println("41 30 42.2 = " + d + " = " + d.getVal() + " = " + new DMS(d.getVal()));

        d = new DMS(-41, 30, 2.2);
        System.out.println("-41 30 2.2 = " + d + " = " + d.getVal() + " = " + new DMS(d.getVal()));

        d = new DMS("-41 30 42.2");
        System.out.println("-41 30 42.2 = " + d + " = " + d.getVal() + " = " + new DMS(d.getVal()));

        d = new DMS("1:01:02.34567");
        System.out.println("1:01:02.34567 = " + d + " = " + d.getVal() + " = " + new DMS(d.getVal()));

        d = new DMS("1:01:02.34567");
        System.out.println("1:01:02.34567 = " + d + " = " + d.getVal() + " = " + new DMS(d.getVal()));

        d = new DMS(-0., 15, 33.3333);
        System.out.println("-0 15 33.3333 = " + d + " = " + d.getVal() + " = " + new DMS(d.getVal()));

        d = new DMS(-0.0001);
        System.out.println("-0.0001 = " + d + " = " + d.getVal() + " = " + new DMS(d.getVal()));

        d = new DMS(121.39583332 / 15.);
        System.out.println("121.39583332/15. = " + d + " = " + d.getVal() + " = " + new DMS(d.getVal()));

        d = new DMS(121.09583332 / 15.);
        System.out.println("121.09583332/15. = " + d + " = " + d.getVal() + " = " + new DMS(d.getVal()));

        d = new DMS(-121.39583332 / 15.);
        System.out.println("-121.39583332/15. = " + d + " = " + d.getVal() + " = " + new DMS(d.getVal()));

        d = new DMS(-121.09583332 / 15.);
        System.out.println("-121.09583332/15. = " + d + " = " + d.getVal() + " = " + new DMS(d.getVal()));
        
        // paulbalm6 - Feb 2010: Check rounding + roll to next minute or degree
        double x = 5.499999980927;
        d = new DMS(x);
        System.out.println(x + " deg  = " + d);
        
        x = 5.99999999;
        d = new DMS(x);
        System.out.println(x + " deg  = " + d);
    }
}