/*
 ****************************************************************************
    JavaScript Sun/Moon Calculator common functions and methods
    Version 4.2  28 December 2009
 
    Copyright 1998-2009 Jeff Conrad
 ****************************************************************************
*/

// ************* Number and String methods **********

// set periodic variable to within 0-y
Number.prototype.setRange = function(y)
{
    var x = this;

    x %= y;
    if (x < 0)
	x += y;
    return x;
};

// convert feet to meters
Number.prototype.FTtoM = function()
{
    return this * 0.3048;
};

// convert meters to feet
Number.prototype.MtoFT = function()
{
    return this / 0.3048;
};

// convert decimal degrees to radians
Number.prototype.DtoR = function()
{
    return this * Math.PI / 180;
};

// convert radians to decimal degrees
Number.prototype.RtoD = function()
{
    return this * 180 / Math.PI;
};

// convert decimal hours to radians
Number.prototype.HtoR = function()
{
    return this * 15 * Math.PI / 180;
};

// convert radians to decimal hours
Number.prototype.RtoH = function()
{
    return this / 15 * 180 / Math.PI;
};

// sidereal time to solar time
Number.prototype.STtoUT = function()
{
    var ST_to_UT = 0.9972695663;
    return this * ST_to_UT;
};

// solar time to sidereal time
Number.prototype.UTtoST = function()
{
    var ST_to_UT = 0.9972695663;	// solar to sidereal time
    return this / ST_to_UT;
};
 
// days to msec for Date objects
Number.prototype.DayToMsec = function()
{
    return this * 24 * 3600 * 1e3;
};
 
// hours to msec for Date objects
Number.prototype.HtoMsec = function()
{
    return this * 3600 * 1e3;
};
 
// msec to hours for Date objects
Number.prototype.MsecToH = function()
{
    return this / (3600 * 1e3);
};

// msec to days for Date objects
Number.prototype.MsecToDay = function()
{
    return this / (24 * 3600 * 1e3);
};

// trim leading and trailing white space
function trim()
{
    var str;
    str = this.replace(/^\s*/, "");
    return str.replace(/\s*$/, "");
}

// width is places to the left of the radix
// FIXME? Should width be the same as printf() field width?
Number.prototype.toFixedLeftPadded = function(places, width, flag)
{
    var
	DIGITSPACE,	// approximate digit width
	NumStr,
	ndigits, pad;

    if (arguments.length > 2)
	DIGITSPACE = " ";	// for use with write_db()
    else
	DIGITSPACE = SP + " ";	// for HTML

    NumStr = this.toFixed(places);
    ndigits = NumStr.indexOf(".");
    if ((pad = width - ndigits) > 0) {
	while (pad--)
	    NumStr = DIGITSPACE + NumStr;
    }

    return NumStr;
};

// add digit-grouping separators to number of 5 or more integer digits
function addCommas(num)
{
    var
	group,				// start of 3-digit group
	numStr,
	rdx,				// radix position
	RADIX = ".",			// radix
	SEP = ",",			// digit-group separator
	sign,
	str = "";

    if (Math.abs(num) < 10e3)
	return num.toString();

    if (num < 0) {
	num = -num;
	sign = -1;
    }
    else
	sign = 1;

    numStr = num.toString();

    if ((rdx = numStr.indexOf(RADIX)) == -1)
	rdx = numStr.length;

    group = rdx - 3;
    str = numStr.slice(group);
    numStr = numStr.slice(0, group);
    group -= 3;

    while (numStr.length > 3) {
	str = numStr.slice(group) + SEP + str;
	numStr = numStr.slice(0, group);
	group -= 3;
    }
    if (numStr.length > 0)
	str = numStr + SEP + str;
    if (sign == -1)
	str = "-" + str;

    return str;
}

// remove commas from valid number
function rmCommas(value)
{
    var valid = true;

    if (value.search(/,/) != -1) {
	// get rid of leading and trailing whitespace
	value = value.replace(/^\s+(\S+)\s+$/, "$1");

	// non-numeric character
	if (value.search(/[^-+.,\dEe]/) != -1)
	    valid = false;
	// no digits before a comma
	else if (value.search(/^,/) != -1)
	    valid = false;
	// too many digits before a comma
	else if (value.search(/\d{4,},/) != -1)
	    valid = false;
	// too many digits after a comma
	else if (value.search(/,\d{4,}/) != -1)
	    valid = false;
	// too few digits after a comma
	else if (value.search(/,\d{0,2}$/) != -1)
	    valid = false;

	if (valid === true)
	    value = value.replace(/,(\d{3})/g, "$1");
    }

    return value;
}

// *********** Number and String prototypes *****

String.prototype.trim = trim;

// *************** Math extensions *****************

Math.PI2 = 2 * Math.PI;
Math.AU  = 149.597871e6;	// astronmical unit in km

// round to a specified number of decimal places
Math.rnd = function(x, places)
{
    var multiplier = Math.pow(10, places);

    return Math.round(x * multiplier) / multiplier;
};

Math.square = function(x)
{
    return x * x;
};

// integer part has same sign as x
Math.ipart = function(x)
{
    return x < 0 ? Math.ceil(x) : Math.floor(x);
};

// fractional part has same sign as x
Math.fpart = function (x)
{
    return x < 0 ? x - Math.ceil(x) : x - Math.floor(x);
};

// *************** angle and time methods ******************************

// convert decimal angle to dd:mm (or decimal hour to hh:mm) if no argument
// or to dd:mm:ss[.ss...] (or hh:mm:ss[.ss...]) if precision given
// if arg given and < 0, show seconds if they are > 0
// if arg >= 0, show seconds to a precision of: 0 = no decimals; 1 = 1 decimal; 2 = 2 decimals
function toDMS(arg)
{
    var 
	angle = Number(this),
	degrees,
	dms,
	minutes,
	seconds,
	precision = 0,
	show_seconds = false,
	sign = 1;

    if (arguments.length > 0) {
	show_seconds = true;
	if (arg >= 0)
	    precision = arg;
    }

    if (angle < 0.0) {
	angle = -angle;
	sign = -1;
    }
    degrees = Math.floor(angle);
    minutes = (angle - degrees) * 60.0;
    if (show_seconds === true) {
	seconds = Math.rnd((minutes - Math.floor(minutes)) * 60.0, precision);
	minutes = Math.floor(minutes);
    }
    else
	minutes = Math.round(minutes);

    if (show_seconds === true && seconds >= 60) {
	seconds = 0.0;
	minutes++;
    }

    if (minutes >= 59.5) {
	minutes = 0.0;
	degrees++;
    }

    dms = (sign == -1 ? "-" : "")
	    + degrees + ":" + (minutes < 10 ? "0" : "") + minutes;
    //FIXME
    //if (show_seconds === true && seconds > 0)
    if (show_seconds === true) {
	if  (arg >= 0)				// always show seconds
	    dms += ":" + (seconds < 10 ? "0" : "") + seconds;
	else if (seconds > 0)
	    dms += ":" + (seconds < 10 ? "0" : "") + seconds;
    }

    return dms;
}

// parse time in HMS format, with optional AM or PM specifier
// returns decimal hours (0-24), or null for invalid value
function parseTime(tag, flag)
{
    var
	allow_am_pm,
	is_am = false,
	is_pm = false,
	MIN_12 = 1,
	MAX_12 = 13,
	MIN_24 = 0,
	MAX_24 = 24,
	msg,
	am_pattern = /([.:\d]) *(am?|AM?) *$/,
	pm_pattern = /([.:\d]) *(pm?|PM?) *$/,
	timeStr,
	result;

    if (arguments.length > 0)
	msg = "Invalid " + tag + " time";
    else
	msg = "Invalid time";
    msg += " (" + this + ")" + LF;

    // always allow am/pm notation when initializing form
    // otherwise disallow am/pm when showing UT
    if (flags.show_ut === false || (arguments.length > 1 && flag == "init"))
	allow_am_pm = true;
    else
	allow_am_pm = false;
    
    // AM
    if (allow_am_pm === true && this.search(am_pattern) != -1) {
	timeStr = this.replace(am_pattern, "$1");
	is_am = true;
    }
    // PM
    else if (allow_am_pm === true && this.search(pm_pattern) != -1) {
	timeStr = this.replace(pm_pattern, "$1");
	is_pm = true;
    }
    // 24 hour
    else
	timeStr = this;

    if (isNaN(result = timeStr.HMto())) {		// invalid format
	if (flags.show_ut === true)
	    msg += UT_FORMAT_MSG;
	else
	    msg += HM_FORMAT_MSG;
	alert(msg);
	result = null;
    }
    else if (is_am === true || is_pm === true) {		// AM/PM format
	if (result < MIN_12 || result >= MAX_12) {	// out of range
	    msg += "Range is " + MIN_12 + " to < " + MAX_12;
	    alert(msg);
	    result = null;
	}
	else {
	    // round to nearest minute to preclude 13 am/pm
	    result = Math.round(result * 60) / 60;
	    if (is_am === true && result >= 12)
		result -= 12;
	    else if (is_pm === true && result < 12)
		result += 12;
	}
    }
    else {						// 24-hour format
	if (result < MIN_24 || result > MAX_24) {	// out of range
	    msg += "Range is " + MIN_24 + " to " + MAX_24;
	    alert(msg);
	    result = null;
	}
	else
	    // round to nearest minute to preclude 13 am/pm
	    result = Math.round(result * 60) / 60;
    }

    return result;
}

// format time for AM/PM or 24 hour
function formatTime(type)
{
    var
	time = Number(this),
	timeStr;

    if (flags.show_am_pm_time === true && flags.show_ut === false) {
	var
	    amStr,
	    pmStr;
	if (type == "T") {
	    amStr = "a";
	    pmStr = "p";
	}
	else {
	    amStr = " am";
	    pmStr = " pm";
	}

	// round to nearest minute to preclude 13 am/pm
	time = Math.round(time * 60) / 60;
	if (time == 24) {
	    time = 12;
	    timeStr = time.toDMS() + amStr;
	}
	else if (time >= 13) {
	    time -= 12;
	    timeStr = time.toDMS() + pmStr;
	}
	else if (time >= 12) {
	    timeStr = time.toDMS() + pmStr;
	}
	else {
	    if (time < 1)
		time += 12;
	    timeStr = time.toDMS() + amStr;
	}
    }
    else
	timeStr = time.toDMS();

    // left pad times on main form
    if (type == "F" && timeStr.search(/^\d:/) != -1)
	timeStr = "0" + timeStr;

    return timeStr;
}

// width is places to the left of the colon
Number.prototype.toDMSLeftPadded = function(width, flag)
{
    var
	DIGITSPACE,		// approximate digit width
	DMSStr,
	ndigits, pad;

    if (arguments.length > 1)
	DIGITSPACE = " ";	// for use with write_db()
    else
	DIGITSPACE = SP + " ";	// for HTML

    DMSStr = this.toDMS();
    ndigits = DMSStr.indexOf(":");
    if ((pad = width - ndigits) > 0) {
	while (pad--)
	    DMSStr = DIGITSPACE + DMSStr;
    }

    return DMSStr;
};

// convert colon-separated DMS value ([dd]d:mm:ss[.ss...] or [dd]d:m[.mm]...) to decimal
// decimal value ([dd]d[.dd...]) is returned unchanged
function DMSto()
{
    var
	msg,
	angle,
	d_angle,
	sign,
	temparr;

    angle = this.toString().trim();
    // get rid of most degree, minute, second indicators
    angle = angle.cleanDMS();
    if (angle.search(/:/) < 0)
	angle = angle.replace(/\s+(\S)/g, ":$1");	// convert spaced DMS to colon-separated
    temparr = angle.split(":");

    // decimal point (if any) must be in last DMS component
    if (angle.search(/\..*:/) != -1)
	return NaN;
    // empty DMS component not allowed; only three components allowed
    if (angle.search("::") != -1 || temparr.length > 3)
	return NaN;

    if (angle.indexOf("-") != -1)
	sign = -1;
    else
	sign = 1;

    if (sign == -1)
	temparr[0] *= -1;

    d_angle = Number(temparr[0]);
    if (temparr.length > 1)		// minutes given
	d_angle += Number(temparr[1]) / 60;

    if (temparr.length > 2)		// seconds given
	d_angle += Number(temparr[2]) / 3600;

    return sign * d_angle;
}

// get rid of most degree, minute, second indicators
function cleanDMS()
{
    var pat_deg, pat_min, pat_sec, pattern;

	// degree symbol (U+00b0, #176)
    pat_deg = "(\\d{1,2}(\\.\\d*)?)\\u00b0\\s*";
	// prime (U+0032) or ASCII apostrophe
    pat_min = "(\\d{1,2}(\\.\\d*)?)[\\u2032\']\\s*";
	// double prime (U+2033), ASCII double quote, or two ASCII apostrophes
    pat_sec = "(\\d{1,2}(\\.\\d*)?)([\\u2033\"]|\'{2})";

    pattern = RegExp(pat_deg + "(" + pat_min + "(" + pat_sec + ")?" + ")?");

    return this.replace(pattern, "$1 $4 $7");
}

//FIXME
// convert DMS value with N|S|E|W hemisphere flag to signed value
function getHemisphere(type)
{
    var
	angle = this.toString().trim(),
	sign = 1;

    if (angle.search(/[NESW]/i) < 0)	// no hemisphere indicator
	return angle;
    // negative value not allowed w/hemisphere indicator; don't change
    // anything, and let DMSto() generate an error
    else if (angle.search(/^-/) >= 0)
	return angle;

    else if (type == "lat") {
	if (angle.search(/^[NS]/i) >= 0) {		// prepended N|S
	    if (angle.search(/^S/i) >= 0)
		sign = -1;
	    angle = angle.replace(/^[NS]\s*/i, "");
	}
	else if (angle.search(/[NS]$/i) >= 0) {		// appended N|S
	    if (angle.search(/S$/i) >= 0)
		sign = -1;
	    angle = angle.replace(/\s*[NS]$/i, "");
	}
    }
    else if (type == "lon") {
	if (angle.search(/^[EW]/i) >= 0) {		// prepended E|W
	    if (angle.search(/^W/i) >= 0)
		sign = -1;
	    angle = angle.replace(/^[EW]\s*/i, "");
	}
	else if (angle.search(/[EW]$/i) >= 0) {		// appended E|W
	    if (angle.search(/W$/i) >= 0)
		sign = -1;
	    angle = angle.replace(/\s*[EW]$/i, "");
	}
    }
    if (sign < 0)
	angle = "-" + angle;

    return angle;
}

//FIXME??
// see if angle is packed DMS ([dd]dmmss[.ss...])
function isPackedDMS(type)
{
    var angle, is_packed_dms;

    // ignore the sign for this test
    angle = this.toString().trim().replace(/^-/, "");

    if (arguments.length < 1)
	type = "unspecified";

    switch (type) {
	case "lat":
					    // [d]dmmss[.ss...]
	    if (angle.search(/^\d{5,6}(\.\d*)?$/) >= 0)
		is_packed_dms = true;
	    else
		is_packed_dms = false;
	    break;
	case "lon":
	case "unspecified":
					    // [dd]dmmss[.ss...]
	    if (angle.search(/^\d{5,7}(\.\d*)?/i) >= 0)
		is_packed_dms = true;
	    else
		is_packed_dms = false;
	    break;
	default:
	    is_packed_dms = false;
	    alert("invalid type: " + type);
	    break;
    }

    return is_packed_dms;
}

// convert packed DMS to colon-separated DMS
function packedDMSto()
{
    var angle = this.toString().trim();

    return angle.replace(/(\d{2})(\d{2})(\.|$)/, ":" + "$1" + ":" + "$2" + "$3");
}

// convert hh:mm value to decimal
function HMto()
{
    var
	msg,
	time = this.toString(),
	d_time,
	sign,
	temparr = time.split(":");

    // decimal point (if any) must be in last HM component
    if (time.search(/\..*:/) != -1)
	return NaN;
    // empty HM component not allowed; only two components allowed
    if (time.search("::") != -1 || temparr.length > 2)
	return NaN;

    if (time.indexOf("-") != -1)
	sign = -1;
    else
	sign = 1;

    if (sign == -1)
	temparr[0] *= -1;

    d_time = Number(temparr[0]);
    if (temparr.length > 1)		// minutes given
	d_time += Number(temparr[1]) / 60;

    return sign * d_time;
}

// *********** Number and String prototypes *****

Number.prototype.DMSto = DMSto;
Number.prototype.HMto = HMto;
Number.prototype.formatTime = formatTime;
Number.prototype.parseTime = parseTime;
Number.prototype.toDMS = toDMS;

String.prototype.DMSto = DMSto;
String.prototype.HMto = HMto;
String.prototype.cleanDMS = cleanDMS;
String.prototype.formatTime = formatTime;
String.prototype.getHemisphere = getHemisphere;
String.prototype.isPackedDMS = isPackedDMS;
String.prototype.packedDMSto = packedDMSto;
String.prototype.parseTime = parseTime;
String.prototype.toDMS = toDMS;

// *************** data validation function *********************

// Partial workaround for Mozilla focus() bug
function fixField(field, msg, value)
{
    if (arguments.length > 1 && msg !== null && msg !== "")
	alert(msg);

    if (flags.isMozilla === false) {
	field.focus();			// if possible, make the user fix it
	field.select();
    }
    else if (arguments.length > 2)
	field.value = value;		// otherwise, set a default value
}
