/* Copyright (c) 2008 Spokane Software Systems, Inc.  All rights reserved. */

/* These are general JavaScript tools that come in handy in multiple places */

/* Make a synchronous call to a page that does nothing but sleep for a specified amount of time */
function sleep( ms ) {
    if( typeof( ms ) != 'number' || ms < 0 ) return;

    var xmlHttp = getXmlHttpObject();

    xmlHttp.open( 'get', '/tools/sleep.html?ms=' + ms + '&cacheavoidance=' + (new Date()).getTime(), false );

    xmlHttp.send(null);
}

/* Given an associative array with a key, this function returns
   a boolean for whether or not the key is defined */
function exists( variable ) {
    return typeof(variable) != 'undefined';
}

/* Determine if an object passed is an array */
function isArray( obj ) {
    try {
        return obj.constructor == Array;
    }
    catch( e ) {
        return false;
    }
}

/* Given a string, determine if it represents a valid ISO date */
function isISODate( str ) {
    if( typeof( str ) != 'string' ) return false;
    if( str.length != 10 ) return false;
    if( !str.match( /^\d\d\d\d-\d\d-\d\d$/ ) ) return false;

    var comp = str.split('-');

    // Remove leading zeros because parseInt seems to think 08 and 09 are base 8 and 9 numbers...(stupid parseInt)
    comp[0] = comp[0].replace( /^0+/, '' );
    comp[1] = comp[1].replace( /^0+/, '' );
    comp[2] = comp[2].replace( /^0+/, '' );

    return isValidDate( parseInt(comp[0], 10), parseInt(comp[1], 10), parseInt(comp[2], 10) );
}

/* Given three numbers (hopefully) determine if they represent a valid year */
function isValidDate( year, month, day ) {
    if( typeof( year ) != 'number' ||
        typeof( month ) != 'number' ||
        typeof( day ) != 'number' ) return false;

    // Make sure the year is in the range valid for the Gregorian calendar
    if( year < 1582 || year > 4099 ) return false;

    // There are only twelve months.
    if( month < 1 || month > 12 ) return false;

    var days = new Array( 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 );
    if( (year % 100 == 0) ? (year % 400 == 0) : (year % 4 == 0) )
        days[1]++;

    if( day < 1 || day > days[month-1] ) return false;

    return true;
}

/* Given a number, this function formats it as a string to a specific
   number of decimal places, optionally with commas (included by default) */
function format( expr, decplaces, use_comma ) {
    // Only interested in numbers.  If it's anything else, simply return it
    if( isNaN( parseFloat(expr) ) ) return expr;

    // Force the expression into a standard number format
    expr = parseFloat(expr);

    // Store the sign of the number and then remove it.
    var sign = '';
    if( expr < 0 ) {
        sign = '-';
        expr *= -1;
    }

    // Build the formatted string using the number
    var str = '' + Math.round( expr * Math.pow( 10, decplaces ) );
    while( str.length <= decplaces ) str = '0' + str;
    var decpoint = str.length - decplaces;
    if( decplaces > 0 ) str = str.substring( 0, decpoint ) + "." + str.substr( decpoint, str.length )
    if( typeof(use_comma) == 'undefined' || use_comma ) for( var i = decpoint-3; i > 0; i-=3 ) str = str.substr(0, i) + "," + str.substr(i, str.length);
    return sign + str;
}

function trim( str ) {
    if( typeof( str ) != 'string' ) return str;

    return str.replace(/^\s+|\s+$/g, "");
}

function ltrim( str ) {
    if( typeof( str ) != 'string' ) return str;

    return str.replace(/^\s+/, "");
}

function rtrim( str ) {
    if( typeof( str ) != 'string' ) return str;

    return str.replace(/\s+$/, "");
}

function escapeHTML( str ) {
    if( typeof( str ) != 'string' ) return str;

    return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&#39;');
}

function unescapeHTML( str ) {
    if( typeof( str ) != 'string' ) return str;

    return str.replace(/&lt;/g, '<').replace(/&gt;/g, '>').replace(/&#39;/g, "'").replace(/&apos;/g, "'").replace(/&quot;/g, '"').replace(/&amp;/g, '&');
}

/* Attempts to return which browser the user is using based on the user agent string.
   Possible return codes are:
    'opera'   - Opera
    'msie'    - Internet Explorer
    'safari'  - Safari
    'firefox' - FireFox
    'mozilla' - Mozilla
    ''        - Unknown Browser
*/
function getBrowserName() {
    var browserName = "";

    var ua = navigator.userAgent.toLowerCase();

    if( ua.indexOf( "opera" ) != -1 )
        browserName = "opera";
    else if( ua.indexOf( "msie" ) != -1 )
        browserName = "msie";
    else if( ua.indexOf( "safari" ) != -1 )
        browserName = "safari";
    else if( ua.indexOf( "mozilla" ) != -1 ) {
        if ( ua.indexOf( "firefox" ) != -1 )
            browserName = "firefox";
        else
            browserName = "mozilla";
    }

    return browserName;
}

/* Find the dimensions of the browser window. */
function getBrowserDimensions() {
    var docWidth = 0, docHeight = 0;
    if( typeof( window.innerWidth ) == 'number' ) {
        //Non-IE
        docWidth = window.innerWidth;
        docHeight = window.innerHeight;
    } else if( document.documentElement && ( document.documentElement.clientWidth || document.documentElement.clientHeight ) )  {
        //IE 6+ in 'standards compliant mode'
        docWidth = document.documentElement.clientWidth;
        docHeight = document.documentElement.clientHeight;
    } else if( document.body && ( document.body.clientWidth || document.body.clientHeight ) ) {
        //IE 4 compatible
        docWidth = document.body.clientWidth;
        docHeight = document.body.clientHeight;
    }

    // Find how far the window has been scrolled (if at all)
    var scrollLeft = (document.documentElement.scrollLeft ? document.documentElement.scrollLeft : document.body.scrollLeft);
    var scrollTop = (document.documentElement.scrollTop  ? document.documentElement.scrollTop  : document.body.scrollTop);


    return { width:docWidth, height:docHeight, scrollLeft:scrollLeft, scrollTop:scrollTop };
}

/* Return an XMLHTTP object for AJAX use. */
function getXmlHttpObject() {
    var xmlHttp = null;

    try {
        // Firefox, Opera 8.0+, Safari
        xmlHttp = new XMLHttpRequest();
    }
    catch( e ) {
        // Internet Exploder...err...I mean "Explorer"
        try {
            xmlHttp = new ActiveXObject( "Msxml2.XMLHTTP" );
        }
        catch( e ) {
            xmlHttp = new ActiveXObject( "Microsoft.XMLHTTP" );
        }
    }

    return xmlHttp;
}

/* Given an XML node, index and optional default value, return the value of the node */
function getXmlNodeValue( node, index, dfltValue ) {
    var value = null;

    if( node && node.length && node.length > index && node[index].childNodes && node[index].childNodes.length > 0 )
        value = node[index].childNodes[0].nodeValue;

    return ( (value == null && typeof(dfltValue) != 'undefined') ? dfltValue : value );
}

/* Return an element's position (x, y, width, height) */
function getElementPosition( elementID ) {
    var e = document.getElementById( elementID );

    // If the element can't be found, return default "invalid" coordinates
    if( !e ) return new Array( -1, -1 );

    // Lookup the browser for some corrections
    var curBrowser = getBrowserName();

    // Copy the reference to the element so we can walk the DOM without losing the link to the original
    var obj = e;

    var list = new Array();
    for( var i in obj ) { list.push( i ); }

    // Find the x-position by either walking the DOM and adding up offsets to parents or by simply
    // getting the absolute x-position (depending on what the browser makes available to us)
    var curLeft = 0;
    if( obj.offsetParent ) {
        while( obj.offsetParent ) {
            var borderLeft = parseInt( getStyleProperty( obj, 'border-left-width' ), 10 );
            if( isNaN(borderLeft) ) borderLeft = 0;
            curLeft += parseInt( obj.offsetLeft, 10 ) + borderLeft;
            obj = obj.offsetParent;
        }
        // Take into account the padding and margins for the BODY and HTML tags
        if( curBrowser == 'msie' && document.body ) {
            var padding = parseInt( getStyleProperty( document.body, 'padding-left' ), 10 );
            if( isNaN(padding) ) padding = 0;
            var margin  = parseInt( getStyleProperty( document.body, 'margin-left' ), 10 );
            if( isNaN(margin) ) margin = 0;
            curLeft += padding + margin;

            if( document.body.parentNode ) {
                padding = parseInt( getStyleProperty( document.body.parentNode, 'padding-left' ), 10 );
                if( isNaN(padding) ) padding = 0;
                margin  = parseInt( getStyleProperty( document.body.parentNode, 'margin-left' ), 10 );
                if( isNaN(margin) ) margin = 0;
                curLeft += padding + margin;
            }
        }
    }
    else if( obj.x ) {
        var x = parseInt( obj.x, 10 );
        if( isNaN(x) ) x = 0;
        curLeft += x;
    }

    // Reset the temporary reference to the original element
    var obj = e;

    // Find the y-position similarly by either walking the DOM and adding up offsets to parents or by simply
    // getting the absolute y-position (depending on what the browser makes available to us)
    var curTop = 0;
    if( obj.offsetParent ) {
        while( obj.offsetParent ) {
            var borderTop = parseInt( getStyleProperty( obj, 'border-top-width' ), 10 );
            if( isNaN(borderTop) ) borderTop = 0;
            curTop += parseInt( obj.offsetTop, 10 ) + borderTop;
            obj = obj.offsetParent;
        }
        // Take into account the padding and margins for the BODY and HTML tags
        if( curBrowser == 'msie' && document.body ) {
            var padding = parseInt( getStyleProperty( document.body, 'padding-top' ), 10 );
            if( isNaN(padding) ) padding = 0;
            var margin  = parseInt( getStyleProperty( document.body, 'margin-top' ), 10 );
            if( isNaN(margin) ) margin = 0;
            curTop += padding + margin;

            if( document.body.parentNode ) {
                padding = parseInt( getStyleProperty( document.body.parentNode, 'padding-top' ), 10 );
                if( isNaN(padding) ) padding = 0;
                margin  = parseInt( getStyleProperty( document.body.parentNode, 'margin-top' ), 10 );
                if( isNaN(margin) ) margin = 0;
                curTop += padding + margin;
            }
        }
    }
    else if( obj.y ) {
        var y = parseInt( obj.y, 10 );
        if( isNaN(y) ) y = 0;
        curTop += y;
    }

    // Return the array of new coordinates
    return { x:curLeft, y:curTop, width:e.offsetWidth, height:e.offsetHeight };
}

/* Given an object and a style name, return the calculated value */
function getStyleProperty( obj, property )
{
    if( !obj ) return '';

    var CSSStyleProp = property;
    var IEStyleProp = property.replace( /-(.)/g, function (str, p1) { return p1.toUpperCase(); } );

    if (obj.currentStyle) //IE
        return obj.currentStyle[IEStyleProp];
    else if (window.getComputedStyle && obj.style) //W3C
        return window.getComputedStyle(obj, "").getPropertyValue(CSSStyleProp);

    return '';
}

/* Given an element ID (or array of elements), fades between two specified values in the given amount of time */
function fade( eleArr, startAlpha, endAlpha, time, timePerFrame ) {
    // Generate a new starting time for this fade
    var startTime = (new Date()).getTime();

    // If the element passed was just a single element, put it into an array
    if( !isArray( eleArr ) ) {
        eleArr = new Array( eleArr );
    }

    // If the parameters passed were not numbers, don't bother trying to interpret them
    if( typeof( startAlpha ) != 'number' ||
        typeof( endAlpha )   != 'number' ||
        typeof( time )       != 'number'  ) return;

    // This can be omitted and will default to a nice, smooth fade (on more modern machines)
    if( typeof( timePerFrame ) != 'number' || timePerFrame == null || timePerFrame < 1 ) timePerFrame = 50;

    // Bounds-check some of the inputs
    if( startAlpha < 0 ) startAlpha = 0;
    if( startAlpha > 100 ) startAlpha = 100;
    startAlpha = Math.round( startAlpha );

    if( endAlpha < 0 ) endAlpha = 0;
    if( endAlpha > 100 ) endAlpha = 100;
    endAlpha = Math.round( endAlpha );

    if( time < 0 ) time = 0;

    // Given a fixed amount of time per frame, calculate how many steps are necessary
    // and how much opacity is added (or subtracted) per step
    var iterations = Math.round( 1.0 * time / timePerFrame );
    var alphaPerFrame = 1.0 * (endAlpha - startAlpha) / iterations;

    for( var i = 0; i < eleArr.length; i++ ) {
        var eleID = eleArr[i];

        // If the element ID isn't even a string, the element can be retrieved, so go on
        if( typeof( eleID ) != 'string' ) continue;

        // If the element can't be found, then go on
        var ele = document.getElementById( eleID );
        if( !ele ) continue;

        // At this point we will definitely be able to continue, so halt any fade that may be in progress
        if( exists( ele.fadeData ) && ele.fadeData != null ) {
            clearTimeout( ele.fadeData.timeout );
        }
        ele.fadeData = new Object();

        if( time == 0 ) {
            // Simply snap to the target opacity with no fuss.
            setOpacity( eleID, endAlpha );
        }
        else {
            // Retrieve the current opacity of the element
            var curAlpha = Math.round( parseFloat(getStyleProperty(ele, 'opacity') * 100) );

            // Generate the plan for fading and assign it to the element
            var steps = new Array();
            // The steps are in reverse order since they'll be popped off the array
            // Also, note that the iterations will start one "frame" after the starting alpha (unlike pulse)
            // This assumes that the element is starting at the "startAlpha" opacity.
            steps.push( new Array(startTime + time, endAlpha) );
            for( var j = iterations - 1; j > 0; j-- ) {
                if( startAlpha < endAlpha && Math.round(startAlpha + alphaPerFrame*j) < curAlpha ||
                    startAlpha > endAlpha && Math.round(startAlpha + alphaPerFrame*j) > curAlpha )
                    break;
                steps.push( new Array( startTime + j*timePerFrame, Math.round(startAlpha + alphaPerFrame*j) ) );
            }

            ele.fadeData.steps = steps;
            ele.fadeData.timePerFrame = timePerFrame;

            walkFadeSteps( eleID );
        }
    }
}

function walkFadeSteps( eleID ) {
    var ele = document.getElementById( eleID );
    if( !ele || !exists(ele.fadeData) || ele.fadeData == null ) return;

    var rightNow = (new Date()).getTime();

    var curStep;
    if( ele.fadeData.steps.length == 1 ) {
        setOpacity( eleID, ele.fadeData.steps[0][1] );

        try {
            delete ele.fadeData;
        }
        catch( e ) {
            ele.fadeData = null;
        }
    }
    else {
        // Start popping through the steps until either we reach the end of we find one that is scheduled for about this time
        do {
            curStep = ele.fadeData.steps.pop();
        }while( ele.fadeData.steps.length > 0 && rightNow > (curStep[0] + 0.5*ele.fadeData.timePerFrame) );

        setOpacity( eleID, curStep[1] );

        if( ele.fadeData.steps.length > 0 ) {
            ele.fadeData.timeout = setTimeout( new Function( "walkFadeSteps( '" + eleID + "' )" ), ele.fadeData.timePerFrame );
        }
        else {
            try {
                delete ele.fadeData;
            }
            catch( e ) {
                ele.fadeData = null;
            }
        }
    }

}



/* Given an element (or array of elements), sets the opacity to the given amount */
function setOpacity( eleArr, alpha ) {
    if( !isArray( eleArr ) ) {
        eleArr = new Array( eleArr );
    }

    for( var i = 0; i < eleArr.length; i++ ) {
        var eleID = eleArr[i];

        // If the element ID isn't even a string, the element can't be retrieved, so quit
        if( typeof( eleID ) != 'string' ) continue;

        // If the element can't be found, then quit
        var ele = document.getElementById( eleID );
        if( !ele ) continue;

        // The instant we know we can go on, cancel any other fades that may be in progress for this element
        if( exists( ele.fadeData ) && ele.fadeData != null ) {
            clearTimeout( ele.fadeData.timeout );
        }

        // Run a real quick bounds check on the opacity
        if( alpha < 0 ) alpha = 0;
        if( alpha > 100 ) alpha = 100;
        alpha = Math.round( alpha );

        ele.style.filter = 'alpha(opacity=' + alpha + ')';
        ele.style.opacity = '' + (alpha / 100.0);
    }
}

function rgbToHex( red, grn, blu ) {
    if( typeof( red ) != 'number' || red < 0 || red > 255 ) return null;
    if( typeof( grn ) != 'number' || grn < 0 || grn > 255 ) return null;
    if( typeof( blu ) != 'number' || blu < 0 || blu > 255 ) return null;

    return '#' + (red < 16 ? '0' : '') + red.toString(16).toUpperCase() +
                 (grn < 16 ? '0' : '') + grn.toString(16).toUpperCase() +
                 (blu < 16 ? '0' : '') + blu.toString(16).toUpperCase();
}

/* These are just convenience functions for the pulseColor function */
function pulseFGColor( eleArr, color, duration, timePerFrame, defaultColor ) {
    pulseColor( eleArr, color, duration, true, true, timePerFrame, defaultColor );
}

function pulseBGColor( eleArr, color, duration, timePerFrame, defaultColor ) {
    pulseColor( eleArr, color, duration, false, true, timePerFrame, defaultColor );
}

function flashFGColor( eleArr, color, duration, timePerFrame, defaultColor ) {
    pulseColor( eleArr, color, duration, true, false, timePerFrame, defaultColor );
}

function flashBGColor( eleArr, color, duration, timePerFrame, defaultColor ) {
    pulseColor( eleArr, color, duration, false, false, timePerFrame, defaultColor );
}

/* Pulses (or flashes) an element a given color and fades back to the original color */
function pulseColor( eleArr, color, duration, foreground, pulse, timePerFrame, defaultColor ) {
    // Generate a new starting time for this pulse
    var startTime = (new Date()).getTime();

    // If the element passed was just a single element, put it into an array
    if( !isArray( eleArr ) ) {
        eleArr = new Array( eleArr );
    }

    // VERIFY THE COMMON PARAMETERS
    // If the color does not conform to strict formatting, then quit
    color = color.toUpperCase();
    if( !color.match( /^#?[0-9A-F]{6}$/ ) ) return;
    if( color.length == 7 ) color = color.substr( 1 );

    // The duration must be for at least SOME amount of time
    if( duration < 1 ) return;

    // These are meant to be boolean...get it right or go away :-)
    if( typeof( foreground ) != 'boolean' || typeof( pulse ) != 'boolean' ) return;
    // FINISHED VERIFYING THE COMMON PARAMETERS

    // This can be omitted and will default to a nice, smooth fade (on more modern machines)
    if( typeof( timePerFrame ) != 'number' || timePerFrame == null || timePerFrame < 1 ) timePerFrame = 50;

    // This is the default color that is used if transparency is encountered all the way up the DOM hierarchy
    if( typeof( defaultColor ) != 'string' || !defaultColor.match( /^#?[0-9A-F]{6}$/i ) ) defaultColor = 'FFFFFF';

    // Construct the pulse plan for each element and begin walking through it
    for( var i = 0; i < eleArr.length; i++ ) {
        var eleID = eleArr[i];

        // If the element ID isn't even a string, the element can't be retrieved, so go on
        if( typeof( eleID ) != 'string' ) continue;

        // If the element can't be found, then go on
        var ele = document.getElementById( eleID );
        if( !ele ) continue;

        // If this is a situation that cannot be color pulsed, then just continue
        if( ele.type && ele.type.toUpperCase() == 'BUTTON' && !foreground ) continue;

        // Retrieve the original color either from a pre-existing pulseData or from the element itself
        var orig_fg_color = ( exists( ele.fgPulseData ) && ele.fgPulseData != null ) ? ele.fgPulseData.origColor : getStyleProperty( ele, 'color' );
        var orig_bg_color = ( exists( ele.bgPulseData ) && ele.bgPulseData != null ) ? ele.bgPulseData.origColor : getStyleProperty( ele, 'background-color' );

        // If an original color could not be retrieved, there's nothing that can be done.  Simply continue.
        if( orig_fg_color == null || orig_bg_color == null ) continue;

        // At this point we will definitely be able to continue, so halt any pulse that may be in progress
        if( foreground && exists( ele.fgPulseData ) && ele.fgPulseData != null ) {
            clearTimeout( ele.fgPulseData.timeout );
        }
        if( !foreground && exists( ele.bgPulseData ) && ele.bgPulseData != null ) {
            clearTimeout( ele.bgPulseData.timeout );
        }
        if( foreground )
            ele.fgPulseData = new Object();
        else
            ele.bgPulseData = new Object();

        // If the original color is in non-hex form, convert it.
        orig_fg_color = orig_fg_color.toUpperCase().replace( /\s/g, '' );
        orig_bg_color = orig_bg_color.toUpperCase().replace( /\s/g, '' );
        if( orig_fg_color.match( /^RGB\(\d+,\d+,\d+\)$/ ) ) {
            orig_fg_color = orig_fg_color.replace( 'RGB', '' ).replace( /[\(\)]/g, '' ).split( ',' );
            orig_fg_color = rgbToHex( parseInt(orig_fg_color[0], 10), parseInt(orig_fg_color[1], 10), parseInt(orig_fg_color[2], 10) );
        }
        if( orig_bg_color.match( /^RGB\(\d+,\d+,\d+\)$/ ) ) {
            orig_bg_color = orig_bg_color.replace( 'RGB', '' ).replace( /[\(\)]/g, '' ).split( ',' );
            orig_bg_color = rgbToHex( parseInt(orig_bg_color[0], 10), parseInt(orig_bg_color[1], 10), parseInt(orig_bg_color[2], 10) );
        }

        // Make a backup of the original color for the final step of the pulse.
        if( foreground )
            ele.fgPulseData.origColor = orig_fg_color;
        else
            ele.bgPulseData.origColor = orig_bg_color;

        var orig_color = foreground ? orig_fg_color : orig_bg_color;

        // Parse the starting color
        var start_red = parseInt( color.substr( 0, 2 ), 16 );
        var start_grn = parseInt( color.substr( 2, 2 ), 16 );
        var start_blu = parseInt( color.substr( 4, 2 ), 16 );
        if( !pulse ) {
            // If this is just a simple flash, then simply change the color to the start color
            // and set a timeout to change it back after the duration
            if( foreground ) {
                ele.style.color = rgbToHex( start_red, start_grn, start_blu );
                ele.fgPulseData.timeout = setTimeout( new Function("document.getElementById('" + eleID + "').style.color = '" + orig_color + "';"), duration );
            }
            else {
                ele.style.backgroundColor = rgbToHex( start_red, start_grn, start_blu );
                ele.bgPulseData.timeout = setTimeout( new Function("document.getElementById('" + eleID + "').style.backgroundColor = '" + orig_color + "';"), duration );
            }
        }
        else {
            // Calculate what the ending color should be (usually the original color, but not always)
            var end_red = '';
            var end_grn = '';
            var end_blu = '';
            var effective_color = orig_color.toUpperCase().replace( /\s/g, '' );
            if( effective_color.match( /^#?[0-9A-F]{6}$/ ) ) {
                if( effective_color.length == 7 ) effective_color = effective_color.substr( 1 );
                end_red = parseInt( effective_color.substr( 0, 2 ), 16 );
                end_grn = parseInt( effective_color.substr( 2, 2 ), 16 );
                end_blu = parseInt( effective_color.substr( 4, 2 ), 16 );
            }
            else if( effective_color.match( /^RGB\(\d+,\d+,\d+\)$/ ) ) {
                effective_color = effective_color.replace( 'RGB', '' ).replace( /[\(\)]/g, '' ).split( ',' );
                end_red = effective_color[0];
                end_grn = effective_color[1];
                end_blu = effective_color[2];
            }
            else if( effective_color == 'TRANSPARENT' ) {
                // Support for this may be removed later...but it's worth trying for now.
                var obj = ele;

                // Continue searching up the parent nodes for a non-transparent color
                while( obj.parentNode ) {
                    obj = obj.parentNode;
                    var c = getStyleProperty( obj, (foreground ? 'color' : 'background-color') );
                    if( c == null) continue;
                    c = c.toUpperCase().replace( /\s/g, '' );
                    if( c.match( /^#?[0-9A-F]{6}$/ ) ) {
                        if( c.length == 7 ) c = c.substr( 1 );
                        end_red = parseInt( c.substr( 0, 2 ), 16 );
                        end_grn = parseInt( c.substr( 2, 2 ), 16 );
                        end_blu = parseInt( c.substr( 4, 2 ), 16 );
                        break;
                    }
                    else if( c.match( /^RGB\(\d+,\d+,\d+\)$/ ) ) {
                        c = c.replace( 'RGB', '' ).replace( /[\(\)]/g, '' ).split( ',' );
                        end_red = c[0];
                        end_grn = c[1];
                        end_blu = c[2];
                        break;
                    }
                }

                // If, after checking parent nodes, nothing but transparency was encountered, use the default
                if( end_red == '' || end_grn == '' || end_blu == '' ) {
                    if( defaultColor.length == 7 ) defaultColor = defaultColor.substr( 1 );
                    end_red = parseInt( defaultColor.substr( 0, 2 ), 16 );
                    end_grn = parseInt( defaultColor.substr( 2, 2 ), 16 );
                    end_blu = parseInt( defaultColor.substr( 4, 2 ), 16 );
                }

            }
            else
                continue;   // If the effective color was found but simply cannot be interpreted, just go on

            // Given a fixed amount of time per frame, calculate how many steps are necessary
            // and how much color is added (or subtracted) per step.
            var iterations = Math.round( 1.0 * duration / timePerFrame );
            var inc_red = 1.0 * ( end_red - start_red ) / iterations;
            var inc_grn = 1.0 * ( end_grn - start_grn ) / iterations;
            var inc_blu = 1.0 * ( end_blu - start_blu ) / iterations;

            // Generate the plan for pulsing the colors and assign it to the element
            var steps = new Array();
            // The final step is reversion to the original color
            steps.push( new Array(startTime + duration, orig_color) );
            for( var j = iterations - 2; j >= 0; j-- ) {
                steps.push( new Array( startTime + j*timePerFrame, rgbToHex(Math.round(start_red + inc_red*j), Math.round(start_grn + inc_grn*j), Math.round(start_blu + inc_blu*j)) ) );
            }

            if( foreground ) {
                ele.fgPulseData.steps = steps;
                ele.fgPulseData.timePerFrame = timePerFrame;
                ele.fgPulseData.foreground = foreground;
            }
            else {
                ele.bgPulseData.steps = steps;
                ele.bgPulseData.timePerFrame = timePerFrame;
                ele.bgPulseData.foreground = foreground;
            }

            walkPulseSteps( eleID, foreground );
        } // End Pulse
    }
}

function walkPulseSteps( eleID, foreground ) {
    var ele = document.getElementById( eleID );
    if( !ele || foreground && (!exists(ele.fgPulseData) || ele.fgPulseData == null) ||
               !foreground && (!exists(ele.bgPulseData) || ele.bgPulseData == null)) return;

    var rightNow = (new Date()).getTime();

    var curStep;
    if( foreground && ele.fgPulseData.steps.length == 1 || !foreground && ele.bgPulseData.steps.length == 1 ) {
        if( foreground ) {
            ele.style.color = ele.fgPulseData.origColor;
            try {
                delete ele.fgPulseData;
            }
            catch( e ) {
                ele.fgPulseData = null;
            }
        }
        else {
            ele.style.backgroundColor = ele.bgPulseData.origColor;
            try {
                delete ele.bgPulseData;
            }
            catch( e ) {
                ele.bgPulseData = null;
            }
        }


    }
    else {
        // Start popping through the steps until either we reach the end of we find one that is scheduled for about this time
        do {
            if( foreground )
                curStep = ele.fgPulseData.steps.pop();
            else
                curStep = ele.bgPulseData.steps.pop();
        }while( foreground && ele.fgPulseData.steps.length > 0 && rightNow > (curStep[0] + 0.5*ele.fgPulseData.timePerFrame) ||
               !foreground && ele.bgPulseData.steps.length > 0 && rightNow > (curStep[0] + 0.5*ele.bgPulseData.timePerFrame));

        if( foreground ) {
            ele.style.color = curStep[1];
            if( ele.fgPulseData.steps.length > 0 ) {
                ele.fgPulseData.timeout = setTimeout( new Function( "walkPulseSteps( '" + eleID + "', " + foreground + " )" ), ele.fgPulseData.timePerFrame );
            }
            else {
                try {
                    delete ele.fgPulseData;
                }
                catch( e ) {
                    ele.fgPulseData = null;
                }
            }
        }
        else {
            ele.style.backgroundColor = curStep[1];
            if( ele.bgPulseData.steps.length > 0 ) {
                ele.bgPulseData.timeout = setTimeout( new Function( "walkPulseSteps( '" + eleID + "', " + foreground + " )" ), ele.bgPulseData.timePerFrame );
            }
            else {
                try {
                    delete ele.bgPulseData;
                }
                catch( e ) {
                    ele.bgPulseData = null;
                }
            }
        }
    }
}
