/* support/js/mr-lib323.js - Martin Rinehart's library, version 3,
Copyright 2011, Martin Rinehart */

/* Table of Contents
 143 I. Data 
 145     A. MRlib323 - The global. 
 148     B. MRlib323.MILLIS_PER_FRAME - Morphing 
 156     C. MRlib323.fader_data - Fader 
 160     D. MRlib323.inits - Initial Values 
 163     E. MRlib323.messages - Messages 
 168 II. X-Browser Functions 
 170     A. MRlib323.center() Return [left, top] to center a rectangle. 
 181     B. MRlib323.delete_or_null() Attempt to delete an object reference. 
 189     C. MRlib323.get_elements_by_class_name() Get elements by class name.
 223     D. MRlib323.remove_children() Cross-browser element emptier. 
 234     E. MRlib323.set_opacity() Cross-browser opacity setter.
 250     F. MRlib323.window_size() Guesses the browser window's size. 
 269 III. General 
 271     A. Library DOM 
 273         1. MRlib323.create_attached_element() Create/attach DOM elem. 
 282         2. MRlib323.create_element() Create a new DOM element. 
 297         3. MRlib323.delete_element() Includes kids, grandkids, etc. 
 305         4. MRlib323.delete_element_by_id() Delete (if it exists) by id. 
 313         5. MRlib323.handle_event() Assign event handler, given id. 
 324         6. MRlib323.insert_after() Insert after existing. 
 338         7. MRlib323.invert_object() Return name:value -> value:name. 
 347         8. MRlib323.nodelist2array() Create an array given a NodeList. 
 355         9. MRlib323.style_an_element() Style given styles object(s).
 385         10. MRlib323.style_elements() Style elements with one style set. 
 396     B. Colors 
 398         1. MRlib323.color2rgb() '#rrggbb' (hex) to [r, g, b] (decimal). 
 406         2. MRlib323.rgb2color() [r, g, b] (decimal) to '#rrggbb' (hex). 
 419     C. String 
 421         1. MRlib323.find_first() Find leftmost char of [chars]. 
 435         2. MRlib323.trim() Trim leading, trailing blanks from return. 
 447         3. MRlib323.triml() Trim leading blanks from returned string. 
 457         4. MRlib323.trimr() Trim trailing blanks from returned string. 
 467     D. Other 
 469         1. MRlib323.array_copy() One-dimensional shallow copy. 
 476         2. MRlib323.array_2d_copy() Two-dimensional shallow copy. 
 483         3. MRlib323.array_fill() Fill new array. 
 490         4. MRlib323.array_index_of() Find index in array. 
 500         5. MRlib323.array_indexes_of() Find indexes in array. 
 511         6. MRlib323.array_subscript() Array from array. 
 527         7. MRlib323.array_2d_subscript() Array_2d from array_2d. 
 538         8. MRlib323.compare_numbers() Compare numbers. 
 543         9. MRlib323.compare_nocase() Compare case-insensitive. 
 555         10. MRlib323.compare_numbers_and_strings() Compare mixed. 
 566         11. MRlib323.compare_reverse() Compare descending. 
 571         12. MRlib323.date2dd_Mon_yyyy() Format date as dd-Mon-yyyy. 
 587         13. MRlib323.fader() Display a message, briefly. 
 609         14. MRlib323.get_column() Get a column from a 2D array. 
 618         15. MRlib323.flatten() Arrays to items within array. 
 630         16. MRlib323.number_from_style_px() 300 from '300px'. 
 646         17. MRlib323.object_attach_props() Add properties to an object. 
 665         18. MRlib323.object_props_to_array() Return values only. 
 672         19. MRlib323.to_string() Instant object.toString(). 
 715         20. MRlib323.toString() Function should expire, early 2012. 
 722 IV. Morph 
 724     A. Element Morphs 
 729         1. MRlib323.dance() Perform a 'dance'.
 788         2. MRlib323.morph_background_color() Morph background color. 
 815         3. MRlib323.morph_border_color() Morph border color. 
 842         4. MRlib323.morph_border_radii() Morph horz & vert. 
 869         5. MRlib323.morph_change_specs() Morph based on 'change_specs'.
 894         6. MRlib323.morph_opacity() Morph from one opacity to another. 
 906         7. MRlib323.morph_position() Morph position. 
 924         8. MRlib323.morph_px_style() Morph a pixel-measured style. 
 939         9. MRlib323.morph_size() Morph size. 
 957         10. MRlib323.morph_size_loc() Morph size and location. 
 981     B. Morph Support 
 983         1. MRlib323.morph_from_change() 'change_spec' to 'morph_spec'.
1150         2. MRlib323.morph_steps() Convert 3, 9, 4 into [3, 5, 7, 9]. 
1169         3. MRlib323.morph_styles() Morph styles. 
1203         4. MRlib323.spread_colors() Start/end colors to morphing array. 
1226 V. Effects 
1228     A. Flash 
1230         1. MRlib323.show_div() Show div briefly. 
1236     B. Glimmer 
1238         1. MRlib323.glimmer() Show element dreamily. 
1248     C. Hover 
1250         1. MRlib323.hover() Show element while over a trigger. 
1263     D. Popup 
1265         1. MRlib323.popup() Show element with its own close button(s). 
1294     E. Shake 
1296         1. MRlib323.shake() Shake an element.
1310     F. Swell 
1312         1. MRlib323.swell() Swell, then unswell, an element. 
1326 VI. Various Objects 
1328     A. QDiv Objects 
1330         1. MRlib323.QDiv() Create and attach a Quick Div. 
1350         2. MRlib323.QDiv.prototype.appendChild()  
1354         3. MRlib323.QDiv.prototype.toString() 
1366     B. Gradient Objects 
1368         1. Gradient Objects 
1370             a) MRlib323.Gradient() Trap 'new Gradient()' errors. 
1375             b) MRlib323.Gradient.init() Support H... and VGradient. 
1397             c) MRlib323.Gradient.prototype.paint() Master paint func. 
1460             d) MRlib323.Gradient.prototype.toString() 
1473         2. HGradient Objects 
1475             a) MRlib323.HGradient() Create an HGradient instance. 
1484             b) MRlib323.HGradient.init() Init HGradient instance. 
1497             c) MRlib323.HGradient.prototype.paint - Inherits Gradient. 
1500             d) MRlib323.HGradient.prototype.toString() 
1508         3. HGradOpposed Objects 
1510             a) MRlib323.HGradOpposed() Create an HGradOpposed instance. 
1519             b) MRlib323.HGradOpposed.init() Init instance. 
1533             c) MRlib323.HGradOpposed.prototype.paint - from Gradient. 
1536             d) MRlib323.HGradOpposed.prototype.toString() 
1545         4. VGradient Objects 
1547             a) MRlib323.VGradient() Create a VGradient instance. 
1556             b) MRlib323.VGradient.init() Init instance. 
1568             c) MRlib323.VGradient.prototype.paint - from Gradient. 
1571             d) MRlib323.VGradient.prototype.toString() 
1579         5. VGradOpposed Objects 
1581             a) MRlib323.VGradOpposed() Create a VGradOpposed instance. 
1590             b) MRlib323.VGradOpposed.init() Init instance. 
1604             c) MRlib323.VGradOpposed.prototype.paint - from Gradient. 
1607             d) MRlib323.VGradOpposed.prototype.toString() 
1616     C. Tree (and Subtree) Objects 
1618         1. Tree Instance Basics 
1620             a) MRlib323.Tree() 
1627             b) MRlib323.Tree.init() 
1654             c) MRlib323.Tree.prototype.toString() 
1663         2. Tree Instance Methods 
1665             a) MRlib323.Tree.prototype.show() Display Tree in DOM. 
1684 VII. Tables 
1686     A. Table Instance Basics 
1688         1. MRlib323.Table() Create a Table instance. 
1750         2. MRlib323.Table.init() Initialize a Table instance. 
1757         3. MRlib323.Table.prototype.toString() 
1774     B. Table Instance Methods 
1776         1. MRlib323.Table.prototype.get_object() Object from Table row. 
1789         2. MRlib323.Table.prototype.join() this.xxx_id = that.id. 
1820         3. MRlib323.Table.prototype.order_by() Sort order. 
1844         4. MRlib323.Table.prototype.select() SQL-like SELECT. 
1868         5. MRlib323.Table.prototype.to_objects() To object of objects. 
1900         6. MRlib323.Table.prototype.to_table() Build an HTML table. 
1935         7. MRlib323.Table.prototype.where() Select rows. 
2005     C. Table-Related Methods 
*/

/**I Data */

/** The global. */
MRlib323 = {};

/** Morphing */
MRlib323.MILLIS_PER_FRAME = 40;  
MRlib323.FRAMES_PER_SECOND = 1000 / MRlib323.MILLIS_PER_FRAME;

/* Note: the more millis per frame, the smoother the morph. However, if you
are morphing multiple elements simultaneously, fewer millis may be better.
Test on low-powered devices. */

/** Fader */
MRlib323.fader_data = 
    { left: 130, top: 70, fade_millis: 8000, delay_millis: 2000 };
    
/** Initial Values */
MRlib323.inits = {};

/** Messages */
MRlib323.messages = {};
MRlib323.messages.gradient = 
        'Use new HGradient or new VGradient. Not new Gradient.';

/**I X-Browser Functions */

/** Return [left, top] to center a rectangle. */
MRlib323.center = function ( width, height ) {

    var size = MRlib323.window_size();
    return [
        Math.round( (size.width - width) / 2 ),
        Math.round( (size.height - height) / 2 )
    ];
    
} // end: center()

/** Attempt to delete an object reference. */
MRlib323.delete_or_null = function ( ref ) {
    
    try { delete ref; } // won't work in MSIE
    catch( e ) { ref = null; }
    
} // end: delete_or_null()

/** Get elements by class name.

Note: class_names may be multiple: 'foo bar' for all 'foo's and 'bar's in
the elements class attribute, but must be singular for the MSIE flavor of
this function.

Returns array, not nodelist. */
MRlib323.get_elements_by_class_name = function ( parent_elem, class_name ) {

    if ( ! parent_elem ) { return []; }

    if ( parent_elem.getElementsByClassName ) {
        return MRlib323.nodelist2array( 
                parent_elem.getElementsByClassName(class_name) );
    }
    
// MSIE, here down

    var all_elems = MRlib323.nodelist2array( 
            parent_elem.getElementsByTagName("*") ),
        elems = [],
        regex = new RegExp( '\\b' + class_name + '\\b' );

    for ( var i in all_elems ) {
        elem = all_elems[ i ];
        if ( elem.className && regex.test(elem.className) ) {
            elems.push( elem );
        }
    } // end: for ( i in all_elems )

    return elems;

} // end: get_elements_by_class_name()

/** Cross-browser element emptier. */
MRlib323.remove_children = function ( element ) {

    element.innerHTML = ''; // that should be all
    
    while ( element.firstChild ) { // but MSIE needs this
        element.removeChild( element.firstChild );
    }
    
} // end: remove_children()

/** Cross-browser opacity setter.
Konqueror had, then lost, opacity capability. 
MozOpacity and KhtmlOpacity are probably extinct. */
MRlib323.set_opacity = function ( elem, opacity ) {

    if ( opacity < 0 ) { opacity = 0.0; } // 0.0 === invisible
    if ( opacity > 1 ) { opacity = 1.0; } // 1.0 === opaque

    var s = elem.style;
        s.opacity = opacity; // most modern browsers
        // s.MozOpacity = opacity; // original Mozilla
        // s.KhtmlOpacity = opacity; // older Konqueror, Safari
        s.filter = "alpha(opacity=" + (100*opacity) + ")"; // guess who

} // end of set_opacity()

/** Guesses the browser window's size. */
MRlib323.window_size = function () {

    var w, h;
    if ( window.innerWidth ) {
        w = innerWidth;
        h = innerHeight;
    } else if ( document.documentElement.clientWidth !== 0 ) {
        w = document.documentElement.clientWidth;
        h = document.documentElement.clientHeight;
    } else {
        w = document.body.clientWidth;
        h = document.body.clientHeight;
    }
    
    return { width: w, height: h };
    
}  // end of window_size()

/**I General */

    /**A Library DOM */

/** Create/attach DOM elem. */
MRlib323.create_attached_element = function ( parent, type, id, style_specs, other_specs ) {
    
    var ret = MRlib323.create_element( type, id, style_specs, other_specs );
    parent.appendChild( ret );
    return ret;
    
} // end: create_attached_element()

/** Create a new DOM element. 

see: ??? */
MRlib323.create_element = function ( type, id, style_specs, other_specs ) {

    var elem = document.createElement( type );
        elem.id = id;
        MRlib323.style_an_element( elem, style_specs );
        for ( prop in other_specs ) {
            elem[ prop ] = other_specs[ prop ];
        }
    return elem;
    
} // end: create_element() 

/** Includes kids, grandkids, etc. */
MRlib323.delete_element = function ( elem ) {

    while( elem.firstChild ) { MRlib323.delete_element( elem.firstChild ); }
    elem.parentNode.removeChild( elem );
    
} // end: delete_element()

/** Delete (if it exists) by id. */
MRlib323.delete_element_by_id = function ( id ) {

    var elem = document.getElementById( id );
    if ( elem ) { MRlib323.delete_element( elem ); }

} // end: delete_element_by_id()

/** Assign event handler, given id. 
'event' is the name, minus 'on' ('click', 'mouseover', ...). */
MRlib323.handle_event = function( id, event, event_func ) {
    var ref = document.getElementById( id );
    if ( ! ref ) { throw new Error( 
            "In 'handle_event()', id '" + id + " not found." ); }
            
    ref[ 'on' + event ] = event_func;
    
} // end: handle_event()

/** Insert after existing. 

Existing element must be a child (or grandchild, or ...) of 'document.body'.*/
MRlib323.insert_after = function ( element_new, element_existing ) {
    
    var parent = element_existing.parentNode,
        sibling = element_existing.nextSibling;
    
    if ( sibling ) { 
        parent.insertBefore( element_new, sibling ); 
    } else { parent.appendChild( element_new ); }
    
} // end: insert_after()

/** Return name:value -> value:name. */
MRlib323.invert_object = function ( obj ) {

    var ret = {};
    for ( prop in obj ) { ret[ obj[prop] ] = prop; }
    return ret;
    
} // end: invert_object()

/** Create an array given a NodeList. */
MRlib323.nodelist2array = function ( nl ) {

    ret = [];
    for ( var i=0; i < nl.length; i++ ) { ret.push( nl[i] ); }
    return ret;
} // end: nodelist2array()

/** Style given styles object(s).
Sample styles object:
    { position:'relative', left:'100px', top: '50px', background: 'blue' }*/
MRlib323.style_an_element = function ( elem, styles ) {

    if ( ! (styles.length) ) { styles = [ styles ]; }
    
    for ( var i in styles ) {
        var style_obj = styles[ i ];
        for ( var prop in style_obj ) {
        
            if ( prop === 'opacity' ) {
                MRlib323.set_opacity( elem, style_obj.opacity ); 
                
            } else if ( prop === 'borderRadius' ) {
                elem.style[ 'borderRadius' ] = 
                        elem.style[ 'MozBorderRadius' ] = style_obj[ prop ];
                
            } else if ( prop === 'cssFloat' ) {
                elem.style[ 'cssFloat' ] = elem.style[ 'styleFloat' ] = 
                        style_obj[ prop ]; 
                        
            } else { 
// if ( prop === 'fontSize' ) { throw "Gotcha!"; }
                elem.style[ prop ] = style_obj[ prop ]; }
            
        } // end: for ( prop in style_obj
    } // end: for ( i in styles )
    
} // end: style_an_element()

/** Style elements with one style set. */
MRlib323.style_elements = function ( elems, styles ) {
    if ( elems.length === 0 ) { return; }
    for ( var i in elems ) { MRlib323.style_an_element( elems[i], styles ); }
}

    /**A Colors */

/** '#rrggbb' (hex) to [r, g, b] (decimal). */
MRlib323.color2rgb = function ( color ) {
    var red   = parseInt( color.substr(1, 2), 16 ),
        green = parseInt( color.substr(3, 2), 16 ),
        blue  = parseInt( color.substr(5, 2), 16 );
    return [ red, green, blue ];
} // end: color2rgb()

/** [r, g, b] (decimal) to '#rrggbb' (hex). */
MRlib323.rgb2color = function ( r, g, b ) {
    var color = '#' +
        hex2( r.toString(16) ) +
        hex2( g.toString(16) ) +
        hex2( b.toString(16) );
    return color;
    
    function hex2( hex ) {
        return hex.length === 2 ? hex : '0' + hex;
    }
} // end: rgb2color()

/**A String */

/** Find leftmost char of [chars]. */
MRlib323.find_first = function ( chars, str ) {

    /* find first to occur of a char in chars or EOS */
    var first = str.length;
    for ( var i in chars ) {
        var loc = str.indexOf( chars[i] );
        if ( loc > -1 ) { first = Math.min( loc, first ); }
    }
    
    return first;
    
} // end: find_first()
    
/** Trim leading, trailing blanks from return. */
MRlib323.trim = function ( str ) {
    /*  ^       BOI
        /s*     zero or more whitespace
        (.*\S)  zero or more any chars, ending in non-white, $1
        /s*     zero or more whitespace
        $       EOI */
    if ( str.match(  /^\s*(.*\S)\s*$/  ) ) { return RegExp.$1; }
    return '';
    
} // end: trim()

/** Trim leading blanks from returned string. */
MRlib323.triml = function ( str ) {
    /*  ^       BOI
        \s*     zero or more whitespace
        (.*)    zero or more non-white, $1
        $       EOI */
    if ( str.match(  /^\s*(.*)$/  ) ) { return RegExp.$1; }
    return '';
} // end: triml()

/** Trim trailing blanks from returned string. */
MRlib323.trimr = function ( str ) {
    /*  ^       BOI
        (.*\S)  zero or more any char, ending in non-white, $1
        \s*     zero or more whitespace
        $       EOI */
    if ( str.match(  /^(.*\S)\s*$/  ) ) { return RegExp.$1; }
    return '';
} // end: trimr()

/**A Other */

/** One-dimensional shallow copy. */
MRlib323.array_copy = function ( arr ) {
    var ret = [];
    for ( var i in arr ) { ret[ i ] = arr[ i ]; }
    return ret;
}

/** Two-dimensional shallow copy. */
MRlib323.array_2d_copy = function ( arr ) {
    var ret = [];
    for ( var i in arr ) { ret[ i ] = MRlib323.array_copy( arr[i] ); }
    return ret;
}

/** Fill new array. */
MRlib323.array_fill = function ( value, count ) {
    var arr = [];
    for ( var i = 0; i < count; i += 1 ) { arr[ i ] = value; }
    return arr;
}

/** Find index in array. */
MRlib323.array_index_of = function ( array, value ) {

        for ( var i in array ) {
            if ( array[i] === value ) { return parseInt( i, 10 ); }
        }
        return -1;
        
} // end: array_index_of()

/** Find indexes in array. */
MRlib323.array_indexes_of = function ( array, values ) {

    var ret = [];
    for ( var i in values ) {
        ret[ i ] = MRlib323.array_index_of( array, values[i] ) ;
    }
    return ret;
    
} // end: array_indexes()

/** Array from array. */
MRlib323.array_subscript = function ( array, cols ) {

/*  array_subscript( [1, 2, 3], [0, 1] )     -> [1, 2]
    array_subscript( [1, 2, 3], [-1, 1, 3] ) -> [2] */

    var ret = [];
    for ( var i in cols ) {
        var col = cols[ i ],
            val = array[ col ];
        if ( val ) { ret.push( val ); }
    }
    return ret;

} // end: array_subscript()

/** Array_2d from array_2d. */
MRlib323.array_2d_subscript = function ( array, cols ) {

    var ret = [];
    for ( var i in array ) {
        ret[ i ] = MRlib323.array_subscript( array[i], cols );
    }
    return ret;
    
} // end: array_2d_subscript()

/** Compare numbers. */
MRlib323.compare_numbers = function ( a, b ) {
    return a - b;
}

/** Compare case-insensitive. */
MRlib323.compare_nocase = function ( a, b ) {

    var x = a.toUpperCase(),
        y = b.toUpperCase();
        
    if ( x > y ) { return 1; }
    if ( x === y ) { return 0; }
    return -1;
    
} // end: compare_nocase()

/** Compare mixed. 
Numbers, then strings, ascending. */
MRlib323.compare_numbers_and_strings = function ( a, b ) {
    if ( (typeof a === 'number') && (typeof b === 'number') ) {
            return MRlib323.compare_numbers( a, b ); }
    if ( (typeof a === 'string') && (typeof b === 'string') )
            return MRlib323.compare_nocase( a, b );
    if ( typeof a === 'number' ) { return -1; } // typeof b !== 'number'
    return 1;
}

/** Compare descending. 
Strings, then numbers, descending. */
MRlib323.compare_reverse = function ( a, b ) { 
        return -MRlib323.compare_numbers_and_strings( a, b ); }

/** Format date as dd-Mon-yyyy. */
MRlib323.date2dd_Mon_yyyy = function ( date ) {

    var months = [ 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
            'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec' ];

    var day = two_digit( '' + date.getDate() );
    var mon = months[ date.getMonth() ];
    var year = date.getFullYear();
    
    return day + '-' + mon + '-' + year;
    
    function two_digit( str ) { return str.length === 1 ? '0' + str : str; }

} // end: date2dd_Mon_yyyy()

/** Display a message, briefly. */
MRlib323.fader = function ( msg, left, top, fade_millis, delay_millis ) {

/* Note: this 'parent' stuff is a kludge because I found I needed it
but it wasn't there. Redesign! */

    var parent;
    if ( MRlib323.fader.parent !== undefined ) { 
        parent = MRlib323.fader.parent;
    } else {
        parent = document.body;
    }        
    
    if ( !left         ) { left         = MRlib323.fader_data.left; }
    if ( !top          ) { top          = MRlib323.fader_data.top; }
    if ( !fade_millis  ) { fade_millis  = MRlib323.fader_data.fade_millis; }
    if ( !delay_millis ) { delay_millis = MRlib323.fader_data.delay_millis; }
    
    var fade_elem = MRlib323.create_attached_element( 
        parent, 'div', '', {left: left + 'px', top: top + 'px',
                background: '#f8f4f0', border: '3px double black',
                display: 'inline-block', margin: '20px', padding: '20px', 
                position: 'fixed'} );
        MRlib323.fader.parent = undefined; 
        
    fade_elem.innerHTML = msg;
    MRlib323.morph_opacity( fade_elem, 1.0, 0.0, 
            fade_millis, delay_millis );
                
    setTimeout( cleanup, fade_millis + delay_millis + 1000 );
    
    function cleanup() { MRlib323.delete_element( fade_elem ); }
    
} // end of fader();
    
/** Get a column from a 2D array. */
MRlib323.get_column = function ( arr, index ) {

    var col = [];
    for ( var i in arr ) { col[ i ] = arr[ i ][ index ]; }
    return col;
    
} // end: get_column()

/** Arrays to items within array. */
MRlib323.flatten = function ( arr ) {

    var ret = [];
    for ( var i in arr ) {
        ret = ret.concat( arr[i] );
    }
    
    return ret;
    
} // end: flatten()    

/** 300 from '300px'. 

Returns 100 from '100px' or 5 from 'groove 5px #f0f0ff'.*/
MRlib323.number_from_style_px = function ( val ) {

    /*  \b      word boundary
        (\d+)   start $1, one or more decimal digits, end $1
        px      'px'

        \b      word boundary
    */
    val.match(  /\b(\d+)px\b/  );
    return parseInt( RegExp.$1, 10 );

} // end: number_from_style_px()

/** Add properties to an object. 

'props' is a property object:
    {name0: val0, name1: val1, . . .} 
    
or 'props' is an array of property objects, as above. */
MRlib323.object_attach_props = function ( obj, props ) {

    if ( !props.length ) { props = [ props ]; }
    
    for ( var i in props ) {
        var prop_obj = props[ i ];
        for ( prop in prop_obj ) { obj[ prop ] = prop_obj[ prop ]; }
    }
    
    return obj;
    
} // end: object_attach_props()

/** Return values only. */
MRlib323.object_props_to_array = function ( obj ) {
    var arr = [];
    for ( var i in obj ) { arr.push( obj[i] ); }
    return arr;
}

/** Instant object.toString(). 
Primitives other than 'string' are returned as strings.
Strings are enclosed in apostrophes. Embedded apostrophes, returns and 
linefeeds are escaped (visually).
Arrays are wrapped in '[' and ']'. Array contents are comma-separated values. 
Other Objects are wrapped in '{' and '}'. Object contents are comma-separated 
name:value pairs. 
Each value in an Array or other Object is to_string()ed recursively. */
MRlib323.to_string = function ( x ) {

    if ( typeof x === 'string' ) { return string( x ); }
    if ( x === null ) { return 'null'; }
    if ( typeof x !== 'object' ) { return '' + x; }

    if ( x instanceof String ) { return string( x ); }
    if ( x instanceof Array  ) { return array( x );  }
    
    var ret = [];
    for ( var prop in x ) {
        ret.push( maybe_quote(prop) + ':' + MRlib323.to_string(x[ prop ]) );
    }
    return '{' + ret.join( ',' ) + '}';
    
    function array( arr ) {
        ret = [];
        for ( var i in arr ) { ret[ i ] = MRlib323.to_string( arr[i] ); }
        return '[' + ret.join( ',' ) + ']';
    } // end: array()
    
    function maybe_quote( prop ) {    
        if ( /^\w+$/.test(prop) ) { return prop; }
        return string( prop );
    }
    
    function string( str ) {
        return "'" + 
                str.replace(  /'/g, "\\'"  ).
                    replace(  /\r/g, "\\r" ).
                    replace(  /\n/g, "\\n" ) + "'"; 
    } // end: string()
    
} // end: toString()

/** Function should expire, early 2012. */
MRlib323.toString = function ( obj ) {
    throw "Old toString() called."; // generate traceback
    alert( 'Martin, old "MRlib.toString()" called. SB "MRlib.to_string."' );
    return MRlib323.to_string( obj );
}    
    
/**I Morph */

    /**A Element Morphs */

/* Morph speed is 20 frames per second. Morph begins after
optional delay (default === 0 millis). */

/** Perform a 'dance'.

The 'dance_steps' are:
[   [time0, style_specs0],
    [time1, style_specs1],
    ... ]
    
The 'style_specs' are:
{ style_name0: value0, style_name1: value1, ... }

The dance begins at 'style_specs0', held for 'time0', continues
with simultaneous morphs to 'style_specs1' over 'time1', ... */
MRlib323.dance = function ( element, dance_steps ) {

    if ( element.is_dancing ) { return; }
    
    element.is_dancing = true;
    var elapsed_time = 0;
    var last_styles = {};
    
    for ( var i in dance_steps ) {
    
        var step = dance_steps[ i ];
        var time = step[ 0 ];
        var styles = step[ 1 ];
        
        if ( i === '0' ) {
            MRlib323.style_an_element( element, step );
        } else {
            MRlib323.morph_change_specs( element, changes(styles, last_styles),
                    time, elapsed_time );
        }
        
        MRlib323.object_attach_props( last_styles, styles );
        var prev_step = step;
        elapsed_time += time;
        
    } // end: for ( i in dance_steps )
    
    setTimeout( function () { element.is_dancing = false; }, 
            elapsed_time + 100 );
            
    /* if "styles.width = '100px'" and "last_styles.width = '50px'",
    "changes().width = '50px;100px'" */
    function changes( styles, last_styles ) {
    
        var chg = {};
        
        for ( var style in styles ) {
            var last = last_styles[ style ];
            if ( last ) { chg[ style ] = last + ';' + styles[ style ]; }
        }
        
        return chg;
        
    } // end changes()
    
} // end: dance()

/** Morph background color. */
MRlib323.morph_background_color = function ( element, start_color, end_color, 
        millis_morph, millis_delay ) {
        
    if ( millis_delay === undefined ) { millis_delay = 0; }
    
    var number_of_changes = millis_morph / MRlib323.MILLIS_PER_FRAME;
    var delta_time = millis_morph / number_of_changes;
    var colors = MRlib323.spread_colors( start_color, end_color, 
            number_of_changes );
    var es = element.style;
    var time = 0;
        
    for ( var i = 0; i < number_of_changes; i += 1 ) {
        setTimeout( get_morph_func(colors[ i ]), time + millis_delay );
        time += delta_time;
    }
    
    setTimeout( get_morph_func(end_color), millis_morph + millis_delay );
    
    function get_morph_func( color ) {
        var bgc = color;
        return function () { es.backgroundColor = bgc; }
    }

} // end: morph_background_color()

/** Morph border color. */
MRlib323.morph_border_color = function ( element, start_color, end_color, 
        millis_morph, millis_delay ) {
        
    if ( millis_delay === undefined ) { millis_delay = 0; }
    
    var number_of_changes = millis_morph / MRlib323.MILLIS_PER_FRAME;
    var delta_time = millis_morph / number_of_changes;
    var colors = MRlib323.spread_colors( start_color, end_color, 
            number_of_changes );
    var es = element.style;
    var time = 0;
        
    for ( var i = 0; i < number_of_changes; i += 1 ) {
        setTimeout( get_morph_func(colors[ i ]), time + millis_delay );
        time += delta_time;
    }
    
    setTimeout( get_morph_func(end_color), millis_morph + millis_delay );
    
    function get_morph_func( color ) {
        var bc = color;
        return function () { es.borderColor = bc; }
    }

} // end: morph_background_color()

/** Morph horz & vert. */
MRlib323.morph_border_radii = function ( element, 
        start_horz_radius, start_vert_radius, end_horz_radius, end_vert_radius,
        millis_morph, millis_delay ) {
        
    if ( millis_delay === undefined ) { millis_delay = 0; }
    
    var number_of_changes = millis_morph / MRlib323.MILLIS_PER_FRAME;
    var delta_time = millis_morph / number_of_changes;
    
    var horzes = MRlib323.morph_steps( start_horz_radius, end_horz_radius,
            number_of_changes );
    var verts = MRlib323.morph_steps( start_vert_radius, end_vert_radius,
            number_of_changes );
            
    var specs = [];
    for ( i in horzes ) {
        horzes[ i ] = horzes[ i ] + 'px';
        verts[ i ] = verts[ i ] + 'px';
        specs.push( horzes[i] + ' ' + verts[i] );
    }
    
    MRlib323.morph_styles( element, {borderRadius: specs},
            millis_morph, millis_delay );
    
} // end: morph_border_radii()

/** Morph based on 'change_specs'.

The 'change_specs' are an object: '{style0: change0, style1: change1, ... }'
The changes are 'start;end' strings: 'start_value;end_value'

The values supported are:
    number: 'opacity: "0.0;0.5"'
    pixels: 'width: "100px;200px"'
    colors: 'backgroundColor: "#a0a0ff;#f0f0ff"'
    two_px: 'borderRadius: "20px / 40px;30px / 50px"' */
MRlib323.morph_change_specs = function ( element, change_specs, 
        millis_morph, millis_delay ) {
        
    var morph_specs = {};
    var frames = millis_morph / MRlib323.MILLIS_PER_FRAME; 

    for ( prop in change_specs ) {
        morph_specs[ prop ] = MRlib323.morph_from_change( change_specs[prop],
                frames );
    }

    MRlib323.morph_styles( element, morph_specs, millis_morph, millis_delay );
        
} // end: morph_change_specs()
    
/** Morph from one opacity to another. */
MRlib323.morph_opacity = function ( element, start_opacity, end_opacity, 
        millis_morph, millis_delay ) {

    var frames = millis_morph / MRlib323.MILLIS_PER_FRAME;
    var opacities = MRlib323.morph_steps( start_opacity, end_opacity, frames );

    MRlib323.morph_styles( element, {opacity: opacities}, 
            millis_morph, millis_delay );
    
} // end: morph_opacity()

/** Morph position. */
MRlib323.morph_position = function ( element, start_left, start_top,
        end_left, end_top, millis_morph, millis_delay ) {
        
    var frames = millis_morph / MRlib323.MILLIS_PER_FRAME;
    var lefts = MRlib323.morph_steps( start_left, end_left, frames );
    var tops = MRlib323.morph_steps( start_top, end_top, frames );
    
    for ( var i in lefts ) {
        lefts[ i ] = lefts[ i ] + 'px';
        tops[ i ] = tops[ i ] + 'px';
    }
    
    MRlib323.morph_styles( element, {left: lefts, top: tops}, 
            millis_morph, millis_delay );
    
} // end: morph_position()

/** Morph a pixel-measured style. */
MRlib323.morph_px_style = function ( element, style_name, start, end, 
        millis_morph, millis_delay ) {
        
    var frames = millis_morph / MRlib323.MILLIS_PER_FRAME;
    var values = MRlib323.morph_steps( start, end, frames );
    
    for ( var i in values ) { values[ i ] = values[ i ] + 'px'; }
    
    var specs_obj = {};
        specs_obj[ style_name ] = values;
    MRlib323.morph_styles( element, specs_obj, millis_morph, millis_delay );
        
} // end: morph_px_style()

/** Morph size. */
MRlib323.morph_size = function ( element, start_width, start_height,
        end_width, end_height, millis_morph, millis_delay ) {
        
    var frames = millis_morph / MRlib323.MILLIS_PER_FRAME;
    var widths = MRlib323.morph_steps( start_width, end_width, frames );
    var heights = MRlib323.morph_steps( start_height, end_height, frames );
    
    for ( var i in widths ) {
        widths[ i ] = widths[ i ] + 'px';
        heights[ i ] = heights[ i ] + 'px';
    }
    
    MRlib323.morph_styles( element, {width: widths, height: heights}, 
            millis_morph, millis_delay );
    
} // end: morph_size()

/** Morph size and location. */
MRlib323.morph_size_loc = function ( element, start_width, start_height,
        start_left, start_top, end_width, end_height, end_left, end_top,
        millis_morph, millis_delay ) {
        
    var frames = millis_morph / MRlib323.MILLIS_PER_FRAME;
    
    var widths  = MRlib323.morph_steps( start_width, end_width, frames );
    var heights = MRlib323.morph_steps( start_height, end_height, frames );
    var lefts   = MRlib323.morph_steps( start_left, end_left, frames );
    var tops    = MRlib323.morph_steps( start_top, end_top, frames );
    
    for ( var i in widths ) {
        widths[ i ] = widths[ i ] + 'px';
        heights[ i ] = heights[ i ] + 'px';
        lefts[ i ] = lefts[ i ] + 'px';
        tops[ i ] = tops[ i ] + 'px';
    }
    
    MRlib323.morph_styles( element, {width: widths, height: heights,
            left: lefts, top: tops}, millis_morph, millis_delay );
            
} // end: morph_size_loc()

/**A Morph Support */

/** 'change_spec' to 'morph_spec'.

A 'change_spec' - width: '100px;200px'
A 'morph_spec'  - width: ['100px', '102px', '104px', ... ] */
MRlib323.morph_from_change = function ( change, num_frames ) {

    var start_stop = change.split( ';' );
    var start = start_stop[ 0 ];
    var stop  = start_stop[ 1 ];
    
    return spread( start, stop, num_frames );
    
    function spread( start, stop, num_frames ) {
    
        if ( is_a_number(start) ) { 
                return spread_a_number( start, stop, num_frames ); }
                
        if ( is_a_px(start) ) {
                return spread_a_px( start, stop, num_frames ); }
                
        if ( is_a_pct(start) ) {
                return spread_a_pct( start, stop, num_frames ); }
                
        if ( is_a_color(start) ) {
                return spread_a_color( start, stop, num_frames ); }
                
        if ( is_two_px(start) ) {
                return spread_two_px( start, stop, num_frames ); }
                
        throw "In 'morph_from_change()', 'spread()', could not spread '" +
                change + "'.";
    
    } // end: spread()
    
    function spread_a_number( start, stop, num_frames ) {
    
        if ( !is_a_number(stop) ) { throw '' +
                "In 'morph_from_change()', 'spread_a_number()', '" +
                stop + "' is not a valid stop spec."; }
                
        return MRlib323.morph_steps( parseFloat(start, 10),
                parseFloat(stop, 10), num_frames );
        
    } // end: spread_a_number()
    
    function spread_a_px( start, stop, num_frames ) {
    
        if ( !is_a_px(stop) ) { throw '' +
                "In 'morph_from_change()', 'spread_a_px()', '" +
                stop + "' is not a valid stop spec."; }
    
        var arr = MRlib323.morph_steps( parseInt(start, 10),
                parseInt(stop, 10), num_frames, true );
        for ( var i in arr ) { arr[ i ] += 'px'; }
        
        return arr;
        
    } // end: spread_a_px() 
    
    function spread_a_pct( start, stop, num_frames ) {
    
        if ( !is_a_pct(stop) ) { throw '' +
                "In 'morph_from_change()', 'spread_a_pct()', '" +
                stop + "' is not a valid stop spec."; }
    
        var arr = MRlib323.morph_steps( parseInt(start, 10),
                parseInt(stop, 10), num_frames, true );
        for ( var i in arr ) { arr[ i ] += '%'; }
        
        return arr;
        
    } // end: spread_a_pct() 
    
    function spread_a_color( start, stop, num_frames ) {
        
        if ( !is_a_color(stop) ) { throw '' +
                "In 'morph_from_change()', 'spread_a_color()', '" +
                stop + "' is not a valid stop spec."; }
                
        return MRlib323.spread_colors( start, stop, num_frames );
        
    } // end: spread_a_color()
    
    function spread_two_px( start, stop, num_frames ) {
    
        if ( !is_two_px(stop) ) { throw '' +
                "In 'morph_from_change()', 'spread_two_px()', '" +
                stop + "' is not a valid stop spec."; }

        var starts = start.split( '/' );
        var stops = stop.split( '/' );

        var pix0s = MRlib323.morph_steps( parseInt(starts[ 0 ], 10),
                parseInt(stops[ 0 ], 10), num_frames, true );
        
        var pix1s = MRlib323.morph_steps( parseInt(starts[ 1 ], 10),
                parseInt(stops[ 1 ], 10), num_frames, true );
                
        var arr = [];
        
        for ( var i in pix0s ) {
                arr.push( pix0s[i] + 'px/' + pix1s[i] + 'px' ); }

        return arr;
        
    } // end: spread_two_px() 
    
    /*
    ^           BOI
    \s*         zero or more whitespace
    \d+         one or more digits
    (\.         a period
    \d+         one or more digits
    )?          zero or one times
    \s*         zero or more whitespace
    $           EOI
    */
    function is_a_number( str ) { return /^\s*\d+(\.\d+)?\s*$/.test( str ); }
    
    /*
    ^           BOI
    \s*         zero or more whitespace
    \d+         one or more digits
    px          literal 'px'
    \s*         zero or more whitespace
    $           EOI
    */
    function is_a_px( str ) { return /^\s*\d+px\s*$/.test( str ); }
    
    /*
    ^           BOI
    \s*         zero or more whitespace
    \d+         one or more digits
    %          literal '%'
    \s*         zero or more whitespace
    $           EOI
    */
    function is_a_pct( str ) { return /^\s*\d+%\s*$/.test( str ); }
    
    /*
    ^           BOI
    \s*         zero or more whitespace
    #           the '#' character
    [0-9a-fA-F]{6}  six hex digits
    \s*         zero or more whitespace
    $           EOI
    */
    function is_a_color( str ) { return /^\s*#[0-9a-fA-F]{6}\s*$/.test( str ); }
    
    /*
    ^           BOI
    \s*         zero or more whitespace
    \d+         one or more digits
    px          literal 'px'
    \s*         zero or more whitespace
    \/          a forward slash
    \s*         zero or more whitespace
    \d+         one or more digits
    px          literal 'px'
    \s*         zero or more whitespace
    $           EOI
    */
    function is_two_px( str ) { return /^\s*\d+px\s*\/\s*\d+px\s*$/.test( 
            str ); }

} // end: morph_from_change()

/** Convert 3, 9, 4 into [3, 5, 7, 9]. */
MRlib323.morph_steps = function ( start, end, nsteps, round ) {

    // nsteps = # of values, including start and end
    var nvalues = end - start,
        values = [];
        
    values.push( start );
    for ( var i = 1; i < (nsteps - 1); i++ ) {
        var next = start + ( i * nvalues ) / ( nsteps - 1 );
        if ( round ) { next = Math.round( next ) };
        values.push( next );
    }
    values.push( end );
    
    return values;

} // end: morph_steps()

/** Morph styles. 

'morph_specs' are an object:
    { style0: [val0, val1, ... valn], style1: [val0, val1, ... valn], ... } */
MRlib323.morph_styles = function (
        element, morph_specs, millis_morph, millis_delay ) {
        
    if ( millis_delay === undefined ) { millis_delay = 0; }
    
    var number_of_changes = millis_morph / MRlib323.MILLIS_PER_FRAME;
    var delta_time = millis_morph / number_of_changes;
    
    var time = 0;
    
    for ( var i = 0; i < number_of_changes; i += 1 ) {
        setTimeout( get_morph_func(morph_specs, i), time + millis_delay );
        time += delta_time;
    } // end: for ( i = 0 to number_of_changes )
    
    function get_morph_func(specs, frame_num) {
    
        var style_specs = {};
        for ( var name in specs ) {
            var vals = specs[ name ];
            if ( vals[frame_num] ) { style_specs[ name ] = vals[ frame_num ]; }
        }
    
        return function () 
            { MRlib323.style_an_element( element, style_specs ); }
        
    } // end: get_morph_func()
    
} // end: morph_styles()

/** Start/end colors to morphing array. */
MRlib323.spread_colors = function ( start_color, end_color, ncolors ) {

    var red=0, green=1, blue=2;
    var colors = [],
        start = MRlib323.color2rgb( start_color ),
        end   = MRlib323.color2rgb( end_color ),
        reds, greens, blues;
        
    var round = true;
    
    reds = MRlib323.morph_steps( start[red], end[red], ncolors, round );
    greens = MRlib323.morph_steps( start[green], end[green], ncolors, round );
    blues = MRlib323.morph_steps( start[blue], end[blue], ncolors, round );
    
    for ( var i = 0; i < ncolors; i++ ) {
        colors[ i ] = MRlib323.rgb2color( reds[i], greens[i], blues[i] );
    }
    
    return colors;
    
} // end: spread_colors()

/**I Effects */

/**A Flash */

/** Show div briefly. */
MRlib323.show_div = function ( div, millis ) {
    div.style.display = 'block';
    setTimeout( function () { div.style.display = 'none'; }, millis );
}       
        
/**A Glimmer */

/** Show element dreamily. */
MRlib323.glimmer = function ( 
        elem, max_opacity, millis_up, millis_down, millis_delay ) {

    MRlib323.morph_opacity( elem, 0.0, max_opacity, millis_up, millis_delay );
    MRlib323.morph_opacity( elem, max_opacity, 0.0, 
            millis_down, millis_up + millis_delay );

} // end: glimmer()

/**A Hover */

/** Show element while over a trigger. */
MRlib323.hover = function ( trig_id, div_id ) {

    var trigger = document.getElementById( trig_id );
        if ( !trigger ) { throw "MRlib323.hover: trigger not found."; }
    var div = document.getElementById( div_id );
        if ( !div ) { throw "MRlib323.hover: display div not found."; }
        
    trigger.onmouseover = function () { div.style.display = 'block'; }
    trigger.onmouseout = function () { div.style.display = 'none'; }

} // end: hover()

/**A Popup */

/** Show element with its own close button(s). */
MRlib323.popup = function ( trig_id, div_id ) {

    var trigger = document.getElementById( trig_id );
        if ( !trigger ) { throw "MRlib323.popup: trigger not found."; }
    var div = document.getElementById( div_id );
        if ( !div ) { throw "MRlib323.popup: display div not found."; }
        
    var closer = MRlib323.create_attached_element( div, 'div', '',
        {background: 'red', color: 'white', fontSize: '14px',
        fontWeight: 'bold', height: '15px', paddingBottom: '2px',
        position: 'absolute', right: '5px', textAlign: 'center',
        top: '3px', width: '15px'},
        {innerHTML: 'X'} );
        closer.onmouseover = function () { 
            this.style.background = 'pink'; 
            this.style.cursor = 'pointer';
        };
        closer.onclick = 
                function () { this.parentNode.style.display = 'none'; }
        closer.onmouseout = function () { 
            this.style.background = 'red'; 
            this.style.cursor = 'default'
        };
        
    trigger.onmouseover = function () { div.style.display = 'block'; }

} // end: popup()

/**A Shake */

/** Shake an element.

Element must be 'position: relative;' at 0, 0. */
MRlib323.shake = function ( element, left, top, millis, millis_delay ) {

    if ( millis_delay === undefined ) { millis_delay = 0; }
    
    MRlib323.morph_position( element, 0, 0, left, top, 
            millis / 2, millis_delay );
    MRlib323.morph_position( element, left, top, 0, 0,
            millis / 2, (millis / 2) + millis_delay );
            
} // end: shake()

/**A Swell */

/** Swell, then unswell, an element. */
MRlib323.swell = function ( element, width_start, height_start, 
        width_end, height_end, millis, millis_delay ) {
        
    if ( millis_delay === undefined ) { millis_delay = 0; }
    
    MRlib323.morph_size( element,  width_start, height_start, 
            width_end, height_end, millis / 2, millis_delay );
    MRlib323.morph_size( element,  width_end, height_end, 
            width_start, height_start, 
            millis / 2, (millis / 2) + millis_delay );
            
} // end: swell()

/**I Various Objects */

/**A QDiv Objects */

/** Create and attach a Quick Div. 
    new QDiv( parent, left, top[, width, height[, background]] ); */
MRlib323.QDiv = function ( parent, left, top, width, height, background ) {

    this.div = document.createElement( 'div' );
        var style = this.div.style;
            style.left = left + 'px';
            style.position = 'absolute';
            style.top  = top + 'px';
            if ( arguments.length > 3 ) {
                style.width  = width + 'px';
                style.height = height + 'px';
                if ( arguments.length > 5 ) { 
                    style.background = background;
                }
            }
    parent.appendChild( this.div );
    
} // end: QDiv()

/**  */
MRlib323.QDiv.prototype.appendChild = function( kid ) {
        this.div.appendChild( kid ); }

/** */
MRlib323.QDiv.prototype.toString = function () {
    return 'QDiv{' +
        'div='     + this.div +
        ',left='   + parseInt( this.div.style.left, 10 ) +
        ',top='    + parseInt( this.div.style.top, 10 ) +
        ',width='  + parseInt( this.div.style.width, 10 ) +
        ',height=' + parseInt( this.div.style.height, 10 ) +
        ',color='  + this.div.style.backgroundColor +
    '}';
} // end: QDiv.toString()

/**A Gradient Objects */

/**1 Gradient Objects */

/** Trap 'new Gradient()' errors. */
MRlib323.Gradient = function () {
    throw new Error( MRlib323.messages.gradient );
}

/** Support H... and VGradient. */
MRlib323.Gradient.init = function ( instance_ref, parent, 
            left, top, width, height, start_color, end_color, is_span ) {

    instance_ref.parent = parent;
    instance_ref.left = left;
    instance_ref.top = top;
    instance_ref.width = width;
    instance_ref.height = height;
    instance_ref.start_color = start_color;
    instance_ref.end_color = end_color;
    instance_ref.type = is_span ? 'span' : 'div';
    
    instance_ref.background = MRlib323.create_attached_element( parent, 
            instance_ref.type, '', {position: 'absolute', 
            left: left + 'px', top: top + 'px', 
            width: width + 'px', height: height + 'px'} );
            
    instance_ref.wash = [];
    
} // end: Gradient()

/** Master paint func. */
MRlib323.Gradient.prototype.paint = function () {

    // delete old wash
    for ( var i in this.wash ) {
        this.background_div.removeChild( this.wash[i].div );
    }
    this.wash = [];
    
    var num_grads = this.horizontal ? this.width : this.height;
    this.colors = spread_colors( 
            this.start_color, this.end_color, num_grads, this.opposed );
    
    for ( var i = 0; i < num_grads; i += 1 ) {
 
        var left_loc, top_loc, width_size, height_size;
        
        if ( this.horizontal ) {
            left_loc = i;
            top_loc = 0;
            width_size = 1;
            height_size = this.height;
        } else {
            left_loc = 0;
            top_loc = i;
            width_size = this.width;
            height_size = 1;
        }
        
        var color_band_styles = { left: left_loc + 'px', top: top_loc + 'px', 
                width: width_size + 'px', height: height_size + 'px',
                backgroundColor: this.colors[i], display: 'block',
                position: 'absolute'};
                
        var parent = this.background;
        var color_band = MRlib323.create_attached_element(
                parent, 'span', '', color_band_styles ); 
        
        this.wash.push( color_band );
        
    } // end: for ( 0 to num_grads loop )
    
    function spread_colors( start, end, num, opposed ) {
        
        var ret;
        
        if ( opposed ) {
            var num_up = Math.round( num_grads / 2 ),
                num_down = num_grads - num_up;
                
            ret = MRlib323.spread_colors( start, end, num_up );
            ret = ret.concat( MRlib323.spread_colors(end, start, num_down) );
            
        } else {
            ret = MRlib323.spread_colors( start, end, num_grads );
        }
        
        return ret;
        
    } // end: spread_colors()
    
} // end: Gradient.paint()

/** */
MRlib323.Gradient.prototype.toString = function () {
    return 'Gradient{' +
        'parent=' + this.parent +
        ', left=' + this.left +
        ',top=' + this.top +
        ', width=' + this.width + 
        ',height=' + this.height +
        ', start_color=' + this.start_color +
        ',end_color=' + this.end_color + 
    '}';
} // end: Gradient.toString()

/**1 HGradient Objects */

/** Create an HGradient instance. */
MRlib323.HGradient = function ( parent, 
        left, top, width, height, start_color, end_color, is_span ) {

    MRlib323.HGradient.init( this, parent, 
            left, top, width, height, start_color, end_color, is_span );
            
} // end: HGradient()

/** Init HGradient instance. */
MRlib323.HGradient.init = function( instance_ref, parent, 
        left, top, width, height, start_color, end_color, is_span ) {
            
    MRlib323.Gradient.init(  instance_ref, parent, 
            left, top, width, height, start_color, end_color, is_span );
    instance_ref.horizontal = true;
    instance_ref.opposed = false;
            
    instance_ref.paint();
            
} // end: HGradient.init()

/** Inherits Gradient. */
MRlib323.HGradient.prototype.paint = MRlib323.Gradient.prototype.paint;

/** */
MRlib323.HGradient.prototype.toString = function () {
    return 'HGradient{' +
        MRlib323.Gradient.prototype.toString.call( this ) +
        ',horizontal=' + this.horizontal + 
    '}';
} // end HGradient.toString()

/**1 HGradOpposed Objects */

/** Create an HGradOpposed instance. */
MRlib323.HGradOpposed = function ( parent, 
        left, top, width, height, start_color, end_color, is_span ) {
        
    MRlib323.HGradOpposed.init( this, parent, 
        left, top, width, height, start_color, end_color, is_span );
        
} // end: HGradOpposed()

/** Init instance. */
MRlib323.HGradOpposed.init = function ( instance_ref, parent, 
        left, top, width, height, start_color, end_color, is_span ) {

    MRlib323.Gradient.init( instance_ref, parent, 
        left, top, width, height, start_color, end_color, is_span );
    
    instance_ref.horizontal = true;
    instance_ref.opposed = true;
    
    instance_ref.paint();
        
} // end: HGradOpposed.init()

/** from Gradient. */
MRlib323.HGradOpposed.prototype.paint = MRlib323.Gradient.prototype.paint;

/** */
MRlib323.HGradOpposed.prototype.toString = function () {

    return 'HGradOpposed{' +
        Gradient.toString.call( this ) +
    '}';

} // end: HGradOpposed.toString()

/**1 VGradient Objects */

/** Create a VGradient instance. */
MRlib323.VGradient = function ( parent, 
        left, top, width, height, start_color, end_color, is_span ) {

    MRlib323.VGradient.init( this, parent, 
            left, top, width, height, start_color, end_color, is_span );
            
} // end: VGradient()

/** Init instance. */
MRlib323.VGradient.init = function( instance_ref, parent, 
            left, top, width, height, start_color, end_color, is_span ) {
            
    MRlib323.Gradient.init(  instance_ref, parent, 
            left, top, width, height, start_color, end_color, is_span );
    instance_ref.horizontal = false;
    
    instance_ref.paint();
            
} // end: VGradient.init()

/** from Gradient. */
MRlib323.VGradient.prototype.paint = MRlib323.Gradient.prototype.paint;

/** */
MRlib323.VGradient.prototype.toString = function () {
    return 'VGradient{' +
        MRlib323.Gradient.prototype.toString.call( this ) +
        ',horizontal=' + this.horizontal + 
    '}';
} // end VGradient.toString()

/**1 VGradOpposed Objects */

/** Create a VGradOpposed instance. */
MRlib323.VGradOpposed = function ( parent, 
        left, top, width, height, start_color, end_color, is_span ) {
        
    MRlib323.VGradOpposed.init( this, parent, 
        left, top, width, height, start_color, end_color, is_span );
        
} // end: VGradOpposed()

/** Init instance. */
MRlib323.VGradOpposed.init = function ( instance_ref, parent, 
        left, top, width, height, start_color, end_color, is_span ) {

    MRlib323.Gradient.init( instance_ref, parent, 
        left, top, width, height, start_color, end_color, is_span );
    
    instance_ref.horizontal = false;
    instance_ref.opposed = true;
    
    instance_ref.paint();
        
} // end: VGradOpposed.init()

/** from Gradient. */
MRlib323.VGradOpposed.prototype.paint = MRlib323.Gradient.prototype.paint;

/** */
MRlib323.VGradOpposed.prototype.toString = function () {

    return 'VGradOpposed{' +
        Gradient.toString.call( this ) +
    '}';

} // end: VGradOpposed.toString()

/**A Tree (and Subtree) Objects */

    /**1 Tree Instance Basics */
    
/** */    
MRlib323.Tree = function ( parent, name ) {

    MRlib323.Tree.init( this, parent, name );
    
} // end: Tree()

/** */
MRlib323.Tree.init = function ( instance_ref, parent, name ) {

    instance_ref.parent = parent; // may be 'undefined'
    if ( (parent === undefined) && (name === undefined) ) { name = 'root'; }
    
    var name_of_parent = '',
        name_from_parent = '';
        
    if ( parent !== undefined ) {
        if ( parent.name ) {
            name_of_parent = parent.name;
        }
        if ( parent.children ) { 
            name_from_parent = parent.children.length;
            parent.children.push( instance_ref ); 
        }
    } 
    instance_ref.name = 
        ( name_of_parent !== '' ? (name_of_parent + ' - ') : '' ) +
        ( name !== undefined ? name : name_from_parent );
    if ( instance_ref.name === '' ) { instance_ref.name = '0'; }
// alert( instance_ref.name );
    instance_ref.children = [];
    
} // end: Tree.init()

/** */
MRlib323.Tree.prototype.toString = function () {
    return 'Tree{' +
        'name=' + this.name +
        ',parent=' + ( this.parent ? this.parent.name : '(none)' ) +
        ',children=' + MRlib323.to_string( this.children ) +
    '}'
} // end: Tree.toString()

    /**1 Tree Instance Methods */

/** Display Tree in DOM. */    
MRlib323.Tree.prototype.show = function ( container ) {

    if ( container === undefined ) { container = this.parent.delem; }
    
    this.delem = MRlib323.create_attached_element( container, 'div', this.name,
            {background: '#fff0f0', border: '1px solid green', 
             display: 'inline-block', margin: '1px', padding: '1px',
             verticalAlign: 'top'}, {innerHTML: this.name} );
            
    if ( this.children.length > 0 ) { this.delem.innerHTML += '<br>'; }

    for ( var i in this.children ) { 
        var kid = this.children[ i ];
        kid.show(); 
    }
    
} // end: Tree.show()

/**I Tables */

/**A Table Instance Basics */

/** Create a Table instance. 
Alternate arguments are:
new Table( other_table );
new Table( name, objects_list ); */
MRlib323.Table = function ( name, col_names, data ) {
    
    if ( arguments.length === 1 ) { // copy a Table
        var old_table = arguments[ 0 ],
            name = old_table.name,
            col_names = MRlib323.array_copy( old_table.col_names ),
            data = MRlib323.array_2d_copy( old_table.data );
    } else if ( arguments.length === 2 ) { // Table from object of instances
    
/* The two-arg form converts an array of instances of a single class to
a table. Given an array of Prisoners, [p0, p1, ...] where each Prisoner
is {name: prisoner_name, rank: p_rank: sno: p_serial_no} it returns a
table, column names: ['id', 'name', 'rank', 'sno'] and data: 
[ 
    ['0', p0.name, p0.rank, p0.sno], 
    ['1', p1.name, p1.rank, p1.sno], 
    ... 
]
*/
        var name = arguments[ 0 ],
            objects_list = arguments[ 1 ],
            col_names = ['id'].concat( prop_names(objects_list) ),
            data = data_arr( objects_list );            
    }
    
    MRlib323.Table.init( this, name, col_names, data ); 
    
    function prop_names( obj ) {
        var props = [];
        for ( var prop in obj ) { // get names from first object
            var first_obj = obj[ prop ];
            for ( var name in first_obj ) {
                props.push( name );
            }
            break;
        }
        return props;
    } // end: prop_names
    
    function data_arr( obj ) {
    
        var ret = [];
        
        for ( prop in obj ) {
        
            var row = [ prop ];
            for ( var name in obj[prop] ) {
                row.push( obj[prop][name] );
            }
            ret.push( row );
            
        } // end: for ( name in obj[prop] )
        return ret;
        
    } // end: for ( prop in obj )
    
} // end: Table()

/** Initialize a Table instance. */
MRlib323.Table.init = function ( instance_ref, name, col_names, data ) {
    instance_ref.name = name;
    instance_ref.col_names = col_names;
    instance_ref.data = data;
} // end: Table.init() 

/** */
MRlib323.Table.prototype.toString = function () {

    var ret = 'Table{' +
        'name=' + this.name +
        ',col_names=' + arr_wrap( this.col_names ) +
        ',data=[';
    for ( var i in this.data ) {
        if ( i !== '0' ) { ret += ','; }
        ret += arr_wrap( this.data[i] );
    }
    return ret + ']}';
    
    function arr_wrap( array ) { return '[' + array + ']'; }

} // end: Table.toString()

/**A Table Instance Methods */

/** Object from Table row. 
Column names become property names. Data become property values. */
MRlib323.Table.prototype.get_object = function ( row_num ) {

    var ret = {};
    for ( var i in this.data[row_num] ) {
        ret[ col_names[i] ] = this.data[ row_num ][ i ];
    }

    return ret;
    
} // end: Table.get_object()

/** this.xxx_id = that.id. */
MRlib323.Table.prototype.join = function ( detail_table, id_col_name ) {

    var cnames = MRlib323.array_copy( this.col_names );
    for ( var i in detail_table.col_names ) {
        if ( i === '0' ) { continue; }
        cnames.push( detail_table.name + '.' + detail_table.col_names[i] );
    }
    
    var data = [];
    var join_col = MRlib323.array_index_of( this.col_names, id_col_name );
    var join_data = detail_table.to_objects();
    var no_join_filler = MRlib323.array_fill( 
            -1, detail_table.col_names.length - 1 );
        
    for ( i in this.data ) {
        var row = this.data[ i ];
        var key = row[ join_col ];
        var join_with = join_data[ key ];
        if ( join_with ) { 
                join_with = MRlib323.object_props_to_array( join_with ); }
        else { // or fill with N/A values
                join_with = MRlib323.array_copy( no_join_filler ); }
        data[ i ] = this.data[ i ].concat( join_with );
    }
    
    return new MRlib323.Table( this.name + '.' + detail_table.name,
        cnames, data );
    
} // end: Table.join()

/** Sort order. */
MRlib323.Table.prototype.order_by = function ( col_name, ascending ) {

    if ( ascending === undefined ) { ascending = true; }
    
    var col_index = MRlib323.array_index_of( this.col_names, col_name );
    if ( col_index < 0 ) { 
            throw "In Table.order_by(), invalid column name."; }
            
    var cnames = MRlib323.array_copy( this.col_names );
    var data = MRlib323.array_2d_copy( this.data );
    
    data.sort( comp_func );
    
    return new MRlib323.Table( this.name, cnames, data );
    
    function comp_func( a, b ) {
        ret = MRlib323.compare_numbers_and_strings( 
                a[col_index], b[col_index] );
        return ascending ? ret : -ret;
    }
    
} // end: of Table.order_by()

/** SQL-like SELECT. */
MRlib323.Table.prototype.select = function ( col_names ) {

/* 'col_names' may be '*" (all columns) or a single name or an array
or 'arguments' may be a list of names. */

    if ( col_names === '*' ) { return new MRlib323.Table( this ); }
    
    var cnames = [];
    if ( (typeof col_names === 'string') && (arguments.length === 1) ) {
        cnames[ 0 ] = col_names; }
    else if ( col_names instanceof Array ) { cnames = col_names; }
    else if ( arguments.length > 1 ) {
        for ( var i = 0, len = arguments.length; i < len; i += 1 ) {
                cnames[ i ] = arguments[ i ]; }
    } else { throw "In Table.select(), unknown 'col_names' argument."; }

    var cols = MRlib323.array_indexes_of( this.col_names, cnames );
    var data = MRlib323.array_2d_subscript( this.data, cols );

    return new MRlib323.Table( this.name, cnames, data );

} // end: Table.select()

/** To object of objects. */
MRlib323.Table.prototype.to_objects = function () {

/* The whole Table becomes an object. Each first-column value becomes the
name of a property. The value of each property is an object, as follows:

cval0: { cname1: cval1, cname2: cval2, ... } 

If there are more column names than data values, the extra names become
undefined properties. If there are more data values than column names,
the extra data values are discarded. */

    var ret = {};
    for ( i in this.data ) {
    
        var obj = {};
        
        for ( j in this.col_names ) {
            if ( j === '0' ) { continue; }
            if ( this.data[i] ) {
                obj[ this.col_names[j] ] = this.data[ i ][ j ];
            } else { obj[ this.col_names[j] ] = undefined; }
        }
        
        ret[ this.data[i][0] ] = obj;
    
    } // end: ( for i in data )
    
    return ret;

} // end: Table.to_objects

/** Build an HTML table. */
MRlib323.Table.prototype.to_table = function ( parent ) {

    if ( parent === undefined ) { parent = document.body; }
    
    var tbl = MRlib323.create_attached_element( parent, 'table', '',
            {background: '#f0fff0', border: '3px solid green'} );
        tbl.border=1;
        var cap = tbl.createCaption();
            cap.innerHTML = '<b>' + this.name + '</b>';
            
        var r0 = tbl.insertRow( 0 );
        
        for ( var i in this.col_names ) {
            cell = r0.insertCell( i );
                cell.style.paddingLeft = cell.style.paddingRight = '5px';
                cell.style.textAlign = 'center';
                cell.style.fontWeight = 'bold';
            cell.innerHTML = this.col_names[ i ];
        }
    
    for ( var j in this.data ) {
        row = tbl.insertRow( (+j) + 1 );
        for ( i in this.data[j] ) {
            cell = row.insertCell( i );
            var datum = this.data[ j ][ i ]
            cell.innerHTML = datum;
            if ( typeof datum === 'number' ) { cell.align = 'right'; }
        }
    }
    
    return this;
        
} // end: Table.to_table()

/** Select rows. */
MRlib323.Table.prototype.where = function ( comp_str ) {

/* Returns table where comparison is true (like a SQL WHERE clause). 

comp_str = 'col_name op value' where 
    col_name - the name of a column in the table
    op       - one of '<', '<=', '=', '>=', '>', '!=' or '<>'
    value    - a number or string */
    
    var ops = [ '<', '<=', '=', '>=', '>', '!=' ];
    var funcs = [
        function (cell, value) { return cell <   value; },
        function (cell, value) { return cell <=  value; },
        function (cell, value) { return cell ==  value; },
        function (cell, value) { return cell >=  value; },
        function (cell, value) { return cell >   value; },
        function (cell, value) { return cell !=  value; }
    ];
    
    var comp_results = compile( this, comp_str );
    var col = comp_results[0],
        op  = comp_results[1],
        val = comp_results[2];

    var cnames = MRlib323.array_copy( this.col_names );
    var data = [];
    
    for ( var i in this.data ) {
        var cell = this.data[ i ][ col ];
        if ( typeof cell === 'string' ) { cell = MRlib323.trim( cell ); }
        if ( funcs[op](cell, val) ) {
            data.push( this.data[i] );
        }
    } // end: for( i in this.data )
    
    return new MRlib323.Table( this.name, cnames, data );
    
    function compile( table, comp_str ) {
    
        var op = get_op( comp_str );
        if ( ! op ) { throw "In Table.where(), comp_str has no operator."; }
        
        var name_value = comp_str.split( ops[op] );
        var name = MRlib323.trim( name_value[0] );
        var col = MRlib323.array_index_of( table.col_names, name );
        if ( col < 0 ) { throw "In Table.where(), no such column name."; }

        var value = MRlib323.trim( name_value[1] );
        if ( value.length < 1 ) { throw "In Table.where(), no value."; }
        
        return [ col, op, value ];
        
        function get_op( comp_str ) {
        
            if ( comp_str.indexOf('<=') > -1 ) { return 1; }
            if ( comp_str.indexOf('>=') > -1 ) { return 3; }
            if ( comp_str.indexOf('!=') > -1 ) { return 5; }
            if ( comp_str.indexOf('<>') > -1 ) { return 5; }
            if ( comp_str.indexOf('<')  > -1 ) { return 0; }
            if ( comp_str.indexOf('>')  > -1 ) { return 4; }
            if ( comp_str.indexOf('=')  > -1 ) { return 2; }
            return undefined;
            
        }
        
    } // end: compile()

} // end: Table.where()

/**A Table-Related Methods */

// alert( 'mr-lib323.js loaded' );

/* end of support/js/mr-lib323.js */
