/**
 * Pelmorex Base object
 *
 * @fileoverview
 * This is the base object that provides a "namespace" to minimize collisions
 * with global code from other libraries.
 *
 * @author Jim Ing (jing@pelmorex.com)
 */


Pelm =
{
    'lang'  : 'en',
    'debug' : false
};

/**
 * Pelmorex Console object
 *
 * @fileoverview
 * This object emulates the logging functionality from Firebug. This is useful
 * for debugging an application in non-Firefox browsers like Internet Explorer,
 * Safari and Opera.
 *
 * @author Jim Ing (jing@pelmorex.com)
 */

Pelm.Console =
{
    'id'        : 'console',
    'window_id' : 'console_window',
    'state_id'  : 'console_state',
    'line_num'  : 0
};

/**
 * @description
 * Emulates Firebug's console.debug method.
 */
Pelm.Console.debug = function( msg )
{
    if( Pelm.debug === true )
    {
        if( document.getElementById( Pelm.Console.window_id ) && document.getElementById( Pelm.Console.state_id ) && document.getElementById( Pelm.Console.state_id ).checked === true )
        {
            Pelm.Console.line_num++;
            document.getElementById( Pelm.Console.window_id ).innerHTML += Pelm.Console.line_num + '. ' + msg + '<br />';
            document.getElementById( Pelm.Console.window_id ).scrollTop = document.getElementById( Pelm.Console.window_id ).scrollHeight;
        }

        if( navigator.userAgent.indexOf( 'Firefox' ) != -1 && window.console )
        {
            console.debug( msg );
        }
    }
};

/**
 * @description
 * Emulates Firebug's console.info method.
 */
Pelm.Console.info = function( msg )
{
    if( Pelm.debug === true )
    {
        if( document.getElementById( Pelm.Console.window_id ) && document.getElementById( Pelm.Console.state_id ) && document.getElementById( Pelm.Console.state_id ).checked === true )
        {
            Pelm.Console.line_num++;
            document.getElementById( Pelm.Console.window_id ).innerHTML += '<span style="color: #0000FF;">' + Pelm.Console.line_num + '. ' + msg + '</span><br />';
            document.getElementById( Pelm.Console.window_id ).scrollTop = document.getElementById( Pelm.Console.window_id ).scrollHeight;
        }

        if( navigator.userAgent.indexOf( 'Firefox' ) != -1 && window.console  )
        {
            console.info( msg );
        }
    }
};

/**
 * @description
 * Emulates Firebug's console.warn method.
 */
Pelm.Console.warn = function( msg )
{
    if( Pelm.debug === true )
    {
        if( document.getElementById( Pelm.Console.window_id ) && document.getElementById( Pelm.Console.state_id ) && document.getElementById( Pelm.Console.state_id ).checked === true )
        {
            Pelm.Console.line_num++;
            document.getElementById( Pelm.Console.window_id ).innerHTML += '<span style="color: #FF8000;">' + Pelm.Console.line_num + '. ' + msg + '</span><br />';
            document.getElementById( Pelm.Console.window_id ).scrollTop = document.getElementById( Pelm.Console.window_id ).scrollHeight;
        }

        if( navigator.userAgent.indexOf( 'Firefox' ) != -1 && window.console  )
        {
            console.warn( msg );
        }
    }
};

/**
 * @description
 * Emulates Firebug's console.error method.
 */
Pelm.Console.error = function( msg )
{
    if( Pelm.debug === true )
    {
        if( document.getElementById( Pelm.Console.window_id ) && document.getElementById( Pelm.Console.state_id ) && document.getElementById( Pelm.Console.state_id ).checked === true )
        {
            Pelm.Console.line_num++;
            document.getElementById( Pelm.Console.window_id ).innerHTML += '<span style="color: #FF0000; font-weight: bold;">' + Pelm.Console.line_num + '. ' + msg + '</span><br />';
            document.getElementById( Pelm.Console.window_id ).scrollTop = document.getElementById( Pelm.Console.window_id ).scrollHeight;
        }

        if( navigator.userAgent.indexOf( 'Firefox' ) != -1 && window.console  )
        {
            console.error( msg );
        }
    }
};

/**
 * @description
 * Highlights a line.
 */
Pelm.Console.highlight = function( msg )
{
    if( Pelm.debug === true )
    {
        if( document.getElementById( Pelm.Console.window_id ) && document.getElementById( Pelm.Console.state_id ) && document.getElementById( Pelm.Console.state_id ).checked === true )
        {
            Pelm.Console.line_num++;
            document.getElementById( Pelm.Console.window_id ).innerHTML += '<span style="background-color: #FFFF00; color: #000000;">' + Pelm.Console.line_num + '. ' + msg + '</span><br />';
            document.getElementById( Pelm.Console.window_id ).scrollTop = document.getElementById( Pelm.Console.window_id ).scrollHeight;
        }

        if( navigator.userAgent.indexOf( 'Firefox' ) != -1 && window.console )
        {
            console.debug( '>>> ' + msg + ' <<<' );
        }
    }
};

/**
 * @description
 * Outputs a message in raw format.
 */
Pelm.Console.raw = function( msg )
{
    if( Pelm.debug === true )
    {
        if( document.getElementById( Pelm.Console.window_id ) && document.getElementById( Pelm.Console.state_id ) && document.getElementById( Pelm.Console.state_id ).checked === true )
        {
            Pelm.Console.line_num++;
            document.getElementById( Pelm.Console.window_id ).innerHTML += Pelm.Console.line_num + '. ' + msg + '<br />';
            document.getElementById( Pelm.Console.window_id ).scrollTop = document.getElementById( Pelm.Console.window_id ).scrollHeight;
        }

        if( navigator.userAgent.indexOf( 'Firefox' ) != -1 && window.console  )
        {
        }
    }
};

/**
 * @description
 * Resets the Console window.
 */
Pelm.Console.reset = function()
{
    if( Pelm.debug === true )
    {
        if( document.getElementById( Pelm.Console.window_id ) )
        {
            //Pelm.Console.line_num = 0;
            document.getElementById( Pelm.Console.window_id ).innerHTML = '';
            document.getElementById( Pelm.Console.window_id ).scrollTop = document.getElementById( Pelm.Console.window_id ).scrollHeight;
        }

        if( navigator.userAgent.indexOf( 'Firefox' ) != -1 && window.console  )
        {
        }
    }
};

/**
 * @description
 * Initializes the state of the Console.
 */
Pelm.Console.init = function()
{
    if( Pelm.debug === true )
    {
        if( navigator.userAgent.indexOf( 'Firefox' ) != -1 && window.console )
        {
            // Disable debugging checkbox
            if( document.getElementById( Pelm.Console.state_id ) )
            {
                document.getElementById( Pelm.Console.state_id ).checked = false;
            }

            // Hide debugging console
            if( document.getElementById( Pelm.Console.id ) )
            {
                document.getElementById( Pelm.Console.id ).style.display = 'none';
            }
        }
        else
        {
            // Enable debugging checkbox
            if( document.getElementById( Pelm.Console.state_id ) )
            {
                document.getElementById( Pelm.Console.state_id ).checked = true;
            }

            // Show debugging console
            if( document.getElementById( Pelm.Console.id ) )
            {
                document.getElementById( Pelm.Console.id ).style.display = 'block';
            }
        }
    }

};

/**
 * Pelmorex Cookie class
 *
 * @fileoverview
 * This is the source file for the Cookie class. This class is used for
 * saving data between browser sessions.
 *
 * @author Jim Ing (jing@pelmorex.com)
 */

/**
 * Cookie class for saving data.
 *
 * @class
 */
Pelm.Cookie = function()
{
};

/**
 * @description
 * Create a cookie.
 *
 * @param {string} name
 * @param {string} value
 * @param {integer} days
 *
 * @example
 * var cookieObj = new Pelm.Cookie();
 * var cookieStr = 'lat=' + center.Latitude + '|lon=' + center.Longitude + '|zoom=' + Pelm.Map.zoomLevel + '|style=' + Pelm.Map.mapStyle;
 * cookieObj.create( Pelm.Map.cookieId, cookieStr, 1 );
 */
Pelm.Cookie.prototype.create = function( name, value, days )
{
    var expires = "";

    if( days )
    {
        var date = new Date();
        date.setTime( date.getTime() + ( days * 24 * 60 * 60 * 1000 ) );
        expires = "; expires=" + date.toGMTString();
    }

    document.cookie = name + "=" + value + expires + "; path=/";
};

/**
 * @description
 * Read a cookie.
 *
 * @param {string} name
 *
 * @return {string} The data stored in the cookie.
 *
 * @example
 * var cookieObj = new Pelm.Cookie();
 * var cookieStr = cookieObj.read( Pelm.Map.cookieId );
 * var pairs = cookieStr.split( '|' );
 * var keyval;
 * var settings = [];
 * for( var i = 0; i < pairs.length; i++ )
 * {
 *     keyval = pairs[ i ].split( '=' );
 *     settings[ keyval[ 0 ] ] = keyval[ 1 ];
 * }
 * Pelm.Map.latitude = settings.lat;
 * Pelm.Map.longitude = settings.lon;
 * Pelm.Map.zoomLevel = settings.zoom;
 * Pelm.Map.mapStyle = settings.style;
 */
Pelm.Cookie.prototype.read = function( name )
{
    var nameEQ = name + "=";
    var ca = document.cookie.split( ';' );

    for( var i = 0; i < ca.length; i++ )
    {
        var c = ca[ i ];
        while( c.charAt( 0 ) == ' ' )
        {
            c = c.substring( 1, c.length );
        }
        if( c.indexOf( nameEQ ) === 0 )
        {
            return c.substring( nameEQ.length, c.length );
        }
    }

    return null;
};

/**
 * @description
 * Remove a cookie.
 *
 * @param {string} name
 */
Pelm.Cookie.prototype.remove = function( name )
{
    Pelm.Cookie.create( name, "", -1 );
};
/**
 * Pelmorex Date object
 *
 * @fileoverview
 *
 * @author Jim Ing (jing@pelmorex.com)
 */

Pelm.Date =
{
    'weekday' :
    {
        'en' : [ 'Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday' ],
        'fr' : [ 'dimanche', 'lundi', 'mardi', 'mercredi', 'jeudi', 'vendredi', 'samedi' ]
    },

    'month' :
    {
        'en' : [ 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December' ],
        'fr' : [ 'janvier', 'f&#233;vier', 'mars', 'avril', 'mai', 'juin', 'juillet', 'ao&#251;t', 'septembre', 'octobre', 'novembre', 'd&#233;cembre' ]
    },

    // http://www.worldtimezone.com/time-canada12.php
    // http://www.worldtimezone.com/time-usa12.php
    'timezone' :
    {
        // DST begins 2 AM (second Sunday in March)
        'DT' :
        {
            '-2' : {
                'en' : 'WGST', 'fr' : 'HAO' // West Greenland Summer Time
            },
            '-2.5' : {
                'en' : 'NDT', 'fr' : 'HAT' // Newfoundland Daylight Time
            },
            '-3' : {
                'en' : 'ADT', 'fr' : 'HAA' // Atlantic Daylight Time
            },
            '-4' : {
                'en' : 'EDT', 'fr' : 'HAE' // Eastern Daylight Time
            },
            '-5' : {
                'en' : 'CDT', 'fr' : 'HAC' // Central Daylight Time
            },
            '-6' : {
                'en' : 'MDT', 'fr' : 'HAR' // Mountain Daylight Time
            },
            '-7' : {
                'en' : 'PDT', 'fr' : 'HAP' // Pacific Daylight Time
            },
            '-8' : {
                'en' : 'AKDT', 'fr' : 'HAY' // Alaska Daylight Time
            },
            '-9' : {
                'en' : 'HADT', 'fr' : 'HAH' // Hawaii-Aleutian Daylight Time
            }
        },
        // DST ends 2 AM (first Sunday in November)
        'ST' :
        {
            '-3' : {
                'en' : 'WGT', 'fr' : 'HNO' // West Greenland Time
            },
            '-3.5' : {
                'en' : 'NST', 'fr' : 'HNT' // Newfoundland Standard Time
            },
            '-4' : {
                'en' : 'AST', 'fr' : 'HNA' // Atlantic Standard Time
            },
            '-5' : {
                'en' : 'EST', 'fr' : 'HNE' // Eastern Standard Time
            },
            '-6' : {
                'en' : 'CST', 'fr' : 'HNC' // Central Standard Time
            },
            '-7' : {
                'en' : 'MST', 'fr' : 'HNR' // Mountain Standard Time
            },
            '-8' : {
                'en' : 'PST', 'fr' : 'HNP' // Pacific Standard Time
            },
            '-9' : {
                'en' : 'AKST', 'fr' : 'HNY' // Alaska Standard Time
            },
            '-10' : {
                'en' : 'HAST', 'fr' : 'HNH' // Hawaii-Aleutian Standard Time
            }
        }
    }
};

/**
 * @description
 */
Pelm.Date.getAbbrevTimeZone = function( date_obj, year )
{
    // get user's timezone
    //var currDate = new Date();
    //Pelm.Console.debug( currDate.toTimeString() );

    //var tzo = ( currDate.getTimezoneOffset() / 60 ) * -1;
    var tzo = ( date_obj.getTimezoneOffset() / 60 ) * -1;

    // check if DST is in effect
    var dtst = Pelm.Date.isDST( date_obj, year );
    //Pelm.Console.debug( dtst );

    var tz = '';

    // set time zone abbreviation for North America
    if( Pelm.Date.timezone[ dtst ][ tzo ] && Pelm.Date.timezone[ dtst ][ tzo ][ Pelm.lang ] )
    {
        tz += Pelm.Date.timezone[ dtst ][ tzo ][ Pelm.lang ];
    }
    // otherwise show GMT offset
    else
    {
        tz += 'GMT' + tzo;
    }

    return tz;
};

/**
 * @description
 * Checks if a date is in Daylight Time (DT) or Standard Time (ST).
 * TODO: Most of Saskatchewan, part of Quebec (east of 63W) and Southampton Isl.(Nunavut) do not use DST.
 */
Pelm.Date.isDST = function( date_obj, year )
{
    var tmpDate;
    var num_sundays = 0;
    var dst_start_date = 0;
    var dst_end_date = 0;
    var dst_start_date_epoch = 0;
    var dst_end_date_epoch = 0;

    // find the date of the second Sunday in March for the given year
    for( var i = 1; i <= 31; i++ )
    {
        tmpDate = new Date( year, 2, i, 1, 59 );
        //Pelm.Console.debug( year + '/' + 2 + '/' + i + ' = ' + tmpDate.getDay() );

        if( tmpDate.getDay() === 0 )
        {
            num_sundays++;
        }

        if( num_sundays == 2 )
        {
            //Pelm.Console.debug( tmpDate.toLocaleString() );
            dst_start_date_epoch = tmpDate.getTime();
            dst_start_date = i;
            break;
        }
    }

    // find the date of the first Sunday in November for the given year
    for( var j = 1; j <= 30; j++ )
    {
        tmpDate = new Date( year, 10, j, 1, 59 );
        //Pelm.Console.debug( year + '/' + 10 + '/' + j + ' = ' + tmpDate.getDay() );

        if( tmpDate.getDay() === 0 )
        {
            //Pelm.Console.debug( tmpDate.toLocaleString() );
            dst_end_date_epoch = tmpDate.getTime();
            dst_end_date = j;
            break;
        }
    }

    //Pelm.Console.debug( dst_start_date + ' - ' + dst_end_date );
    //Pelm.Console.debug( dst_start_date_epoch + ' - ' + dst_end_date_epoch );

    var dtst;
    if( date_obj.getTime() > dst_start_date_epoch && date_obj.getTime() <= dst_end_date_epoch )
    {
        dtst = 'DT';
    }
    else
    {
        dtst = 'ST';
    }

    return dtst;
};

/**
 * Pelmorex Utility object
 *
 * @fileoverview
 * This is a utility object that contains methods for accessing elements,
 * working with URLs, generating GUIDs, etc.
 *
 * @author Jim Ing (jing@pelmorex.com)
 */

Pelm.Util =
{
    'timer_ids' : {}
};

/**
 * @description
 * Set the inner HTML of an element.
 */
Pelm.Util.setInnerHTML = function( elem_id, elem_html )
{
    // check if element exists
    if( document.getElementById( elem_id ) )
    {
        document.getElementById( elem_id ).innerHTML = elem_html;
    }
};

/**
 * @description
 * Loops through all the form elements to find a matching element.
 */
Pelm.Util.getElement = function( arg )
{
    var elem = null;
    if( typeof( arg ) == 'object' )
    {
        elem = arg;
    }
    else
    {
        // Find all form elements that match the element name
        var form_elem;
        for( var i = 0; i < document.forms[ 0 ].elements.length; i++ )
        {
            form_elem = document.forms[ 0 ].elements[ i ];

            if( form_elem.name == arg && form_elem.checked === true )
            {
                elem = document.getElementById( form_elem.id );
                break;
            }
        }
    }

    return elem;
};

/**
 * @description
 * Find all elements matching the specified CSS class.
 */
Pelm.Util.getElementsByClassName = function ( className )
{
    var all = document.all ? document.all : document.getElementsByTagName( '*' );
    var elements = [];

    for( var e = 0; e < all.length; e++ )
    {
        if( all[ e ].className == className )
        {
            elements[ elements.length ] = all[ e ];
        }
    }

    return elements;
};

/**
 * @description
 * Find all elements matching the specified CSS class and host name.
 */
Pelm.Util.getElementsByClassNameSrcMatch = function ( className, hostName )
{
    var all = document.all ? document.all : document.getElementsByTagName( '*' );
    var elements = [];

    for( var e = 0; e < all.length; e++ )
    {
        if( all[ e ].className == className && all[ e ].src.match( hostName ) )
        {
            elements[ elements.length ] = all[ e ];
        }
    }

    return elements;
};

/**
 * @description
 * Parse an HTTP GET string
 */
/*
Pelm.Util.parseUrl = function( name )
{
    Pelm.Console.debug( 'Before: ' + name );
    name = name.replace( /[\[]/,"\\\[").replace(/[\]]/, "\\\]" ); // TODO: JSLint reports 'bad escapement'
    Pelm.Console.debug( 'After: ' + name );

    var regexS = "[\\?&]" + name + "=([^&#]*)";
    var regex = new RegExp( regexS );
    var results = regex.exec( window.location.href );

    if( results === null )
    {
        return '';
    }
    else
    {
        return results[ 1 ];
    }
};
*/

/**
 * @description
 * Show the selected tab section and hide the others.
 */
Pelm.Util.showTabSection = function( tabPrefixId, selectedTabId )
{
    var selectedTabPrefix = selectedTabId.replace( /_section/, '' ); // TODO: suffix should not be hard-coded

    if( document.getElementById( tabPrefixId + '_tab_bar' ) )
    {
        // get the tabbed elements
        var tabElems = document.getElementById( tabPrefixId + '_tab_bar' ).getElementsByTagName( 'li' );

        // highlight the selected tab and unhighlight the others
        var tabElemPrefix;
        for( var h = 0; h < tabElems.length; h++ )
        {
            tabElemPrefix = tabElems[ h ].id.replace( /_tab/, '' ); // TODO: suffix should not be hard-coded
            if( tabElemPrefix == selectedTabPrefix )
            {
                tabElems[ h ].className = 'tab-selected'; // TODO: style should not be hard-coded
            }
            else
            {
                tabElems[ h ].className = 'tab-unselected'; // TODO: style should not be hard-coded
            }
        }

        // find the tabbed sections
        var tabDivElems = document.getElementById( tabPrefixId + '_tabbed_section' ).getElementsByTagName( 'div' );
        var tabSectionElems = [];
        for( var i = 0; i < tabDivElems.length; i++ )
        {
            if( tabDivElems[ i ].id.match( '_section' ) )
            {
                tabSectionElems.push( tabDivElems[ i ] );
            }
        }

        // enable the selected tab section and disable the others
        for( var j = 0; j < tabSectionElems.length; j++ )
        {
            if( tabSectionElems[ j ].id == selectedTabId )
            {
                tabSectionElems[ j ].style.display = 'block';
            }
            else
            {
                tabSectionElems[ j ].style.display = 'none';
            }
        }
    }
};

/**
 * @description
 * Generates a random 4-character string.
 */
Pelm.Util.S4 = function()
{
    var s4 = ( ( ( 1 + Math.random() ) * 0x10000 ) | 0 ).toString( 16 ).substring( 1 );

    return s4;
};

/**
 * @description
 * Generates a GUID-style string.
 */
Pelm.Util.guid = function()
{
    var guid = ( Pelm.Util.S4() + Pelm.Util.S4() + "-" + Pelm.Util.S4() + "-" + Pelm.Util.S4() + "-" + Pelm.Util.S4() + "-" + Pelm.Util.S4() + Pelm.Util.S4() + Pelm.Util.S4() );

    return guid;
};

/**
 * @description
 * Checks an image to determine if it's loaded or not.
 */
Pelm.Util.isImageLoaded = function( img )
{
    //Pelm.Console.debug( img );

    if( img )
    {
        // MSIE, Opera
        if( ( navigator.appName == 'Microsoft Internet Explorer' || navigator.appName == 'Opera' ) && !img.complete )
        {
            //if( img.complete === false ) { Pelm.Console.warn( 'src=' + img.src + ', onerror=' + img.onerror + ', complete=' + img.complete + ', fileSize=' + img.fileSize + ', naturalWidth=' + img.naturalWidth + ', naturalHeight=' + img.naturalHeight + ', width=' + img.width + ', height=' + img.height ); }
            return false;
        }

        // Netscape-based (Firefox, Safari, Chrome)
        if( navigator.appName == 'Netscape' && typeof img.naturalWidth != 'undefined' && img.naturalWidth === 0 )
        {
            //if( img.naturalWidth === 0 ) { Pelm.Console.warn( 'src=' + img.src + ', onerror=' + img.onerror + ', complete=' + img.complete + ', fileSize=' + img.fileSize + ', naturalWidth=' + img.naturalWidth + ', naturalHeight=' + img.naturalHeight + ', width=' + img.width + ', height=' + img.height ); }
            return false;
        }
    }

    // no other way of checking -- assume it's valid
    return true;
};

/**
 * @description
 */
Pelm.Util.setTimer = function( code, interval )
{
    //Pelm.Console.info( 'Pelm.Util.setTimer: code=' + code + ', interval=' + interval );

    var timer_id = setInterval( code, interval );

    Pelm.Util.timer_ids[ timer_id ] = '';

    return timer_id;
};

/**
 * @description
 */
Pelm.Util.clearTimer = function( timer_id )
{
    //Pelm.Console.info( 'Pelm.Util.clearTimer' );

    if( timer_id )
    {
        clearInterval( timer_id );

        Pelm.Util.timer_ids[ timer_id ] = null;
        delete Pelm.Util.timer_ids[ timer_id ];
    }
};

/**
 * @description
 */
Pelm.Util.clearAllTimers = function()
{
    Pelm.Console.info( 'Pelm.Util.clearAllTimers' );

    var timer_id;

    var cnt_before = 0;
    for( timer_id in Pelm.Util.timer_ids )
    {
        cnt_before++;
    }

    for( timer_id in Pelm.Util.timer_ids )
    {
        if( timer_id )
        {
            clearInterval( timer_id );

            Pelm.Util.timer_ids[ timer_id ] = null;
            delete Pelm.Util.timer_ids[ timer_id ];
        }
    }

    var cnt_after = 0;
    for( timer_id in Pelm.Util.timer_ids )
    {
        cnt_after++;
    }
    Pelm.Console.debug( 'before=' + cnt_before + ', after=' + cnt_after );
};

/**
 * Pelmorex JSON Script Request (JSR) class
 *
 * @fileoverview
 * This is the source file for the JSR class. This class is used for making
 * requests to web services using the dynamic script tag approach. This
 * technique is useful if you need to make requests that are cross-domain. In
 * Ajax, you'd have to set up a proxy. This technique is also cross-browser
 * compatible. However, JSR requires that the web service lets you specify a
 * JavaScript callback function.
 *
 * @author Jim Ing (jing@pelmorex.com)
 */

/**
 * JSON Script Request (JSR) class.
 *
 * @class
 * @constructor
 *
 * @param {string} srcUrl
 *
 * @example
 */
Pelm.JSR = function( srcUrl )
{
    this.srcUrl      = srcUrl;
    this.noCache     = '&noCache=' + ( new Date() ).getTime() + '&';
    this.headTagElem = document.getElementsByTagName( 'head' ).item( 0 );
    this.scriptId    = Pelm.Util.guid();
};

/**
 * @description
 * Add the script tag to the head tag.
 */
Pelm.JSR.prototype.addScriptTag = function()
{
    try
    {
        Pelm.Console.debug( this.scriptObj.id + ': <a href="' + this.scriptObj.src + '" target="_blank">' + this.scriptObj.src + '</a>' );

        this.headTagElem.appendChild( this.scriptObj );
    }
    catch( e )
    {
        Pelm.Console.error( 'addScriptTag: ' + e.description );
    }
};

/**
 * @description
 * Build the script tag.
 */
Pelm.JSR.prototype.buildScriptTag = function()
{
    try
    {
        this.scriptObj = document.createElement( 'script' );

        this.scriptObj.setAttribute( 'type', 'text/javascript' );
        this.scriptObj.setAttribute( 'src', this.srcUrl + this.noCache );
        this.scriptObj.setAttribute( 'id', this.scriptId );
    }
    catch( e )
    {
        Pelm.Console.error( 'buildScriptTag: ' + e.description );
    }
};

/**
 * @description
 * Remove the script tag from the head tag.
 */
Pelm.JSR.prototype.removeScriptTag = function()
{
    // TODO: add logic to remove any previous script tags that weren't removed?
    try
    {
        if( this.scriptObj )
        {
            if( document.getElementById( this.scriptObj.id ) )
            {
                Pelm.Console.debug( 'Removing ' + this.scriptObj.id );
                this.headTagElem.removeChild( this.scriptObj );
            }
            else
            {
                Pelm.Console.warn( 'Cannot find specified script object!' );
            }
        }
        else
        {
            Pelm.Console.warn( 'No script object!' );
        }
    }
    catch( e )
    {
        Pelm.Console.error( 'removeScriptTag: ' + e.description );
    }
};

/**
 * Pelmorex Map object
 *
 * @fileoverview
 * This is a "generic" map object.
 *
 * @author Jim Ing (jing@pelmorex.com)
 */

Pelm.Map =
{
    'id'                    : '',
    'lat'                   : 0,
    'lon'                   : 0,
    'zoom'                  : 0,
    'style'                 : '',

    'VE'                    : null,

    'crosshair_src'         : 'images/icons/crosshair_square_red.gif',

    'elem_ids'              :
    {
        'loading'           : 'loading',
        'statusbar_prefix'  : 'statusbar_'
    }
};

/**
 * @description
 * Find a VE layer.
 */
Pelm.Map.findLayer = function( title )
{
    //Pelm.Console.info( 'Pelm.Map.findLayer: ' + title );

    var layer = null;
    for( var i = 0; i < Pelm.Map.VE.GetShapeLayerCount(); i++ )
    {
        layer = Pelm.Map.VE.GetShapeLayerByIndex( i );

        if( layer.GetTitle() == title )
        {
            break;
        }

        layer = null;
    }

    return layer;
};

/**
 * @description
 * Hide the loading spinner.
 */
Pelm.Map.hideLoading = function()
{
    //Pelm.Console.info( 'Pelm.Map.hideLoading:' );

    if( document.getElementById( Pelm.Map.elem_ids.loading ) )
    {
        document.getElementById( Pelm.Map.elem_ids.loading ).style.display = 'none';
    }
};

/**
 * @description
 * Show the loading spinner.
 */
Pelm.Map.showLoading = function()
{
    //Pelm.Console.info( 'Pelm.Map.showLoading:' );

    if( document.getElementById( Pelm.Map.elem_ids.loading ) )
    {
        document.getElementById( Pelm.Map.elem_ids.loading ).style.display = 'block';
    }
};

/**
 * @description
 * Update the statusbar message, where pos = [ left | center | right ]
 */
Pelm.Map.updateStatusBar = function( pos, str )
{
    //Pelm.Console.info( 'Pelm.Map.updateStatusBar: ' + str );

    if( document.getElementById( Pelm.Map.elem_ids.statusbar_prefix + pos ) )
    {
        document.getElementById( Pelm.Map.elem_ids.statusbar_prefix + pos ).innerHTML = str;
    }
};

/**
 * @description
 * Draw a crosshair in the center of the map.
 */
Pelm.Map.drawCrosshair = function()
{
    Pelm.Console.info( 'Pelm.Map.drawCrosshair:' );

    var num_pat = /^(\d+)(px)/;
    var w_match;
    var h_match;
    if( document.getElementById( Pelm.Map.id ) )
    {
        w_match = num_pat.exec( document.getElementById( Pelm.Map.id ).style.width );
        h_match = num_pat.exec( document.getElementById( Pelm.Map.id ).style.height );
    }

    if( w_match && h_match )
    {
        var w_map = w_match[ 1 ];
        var h_map = h_match[ 1 ];

        var w_icon = 17;
        var h_icon = 17;

        var xpos = ( w_map / 2 ) - Math.floor( w_icon / 2 );
        var ypos = ( h_map / 2 ) - Math.floor( h_icon / 2 );

        var crosshair = document.createElement( 'div' );
        crosshair.innerHTML = '<img src="' + Pelm.Map.crosshair_src + '" alt="+" />';
        crosshair.style.left = xpos + 'px';
        crosshair.style.top = ypos + 'px';

        Pelm.Map.VE.AddControl( crosshair );
    }
};

/**
 * @description
 * Gets the center and bounding box coordinates of the current map view.
 * [Debugging]
 */
Pelm.Map.getCenter = function()
{
    //var mapview = Pelm.Map.VE.GetMapView();
    var stats = Pelm.Map.VE.GetCenter() + '<br />';
    //stats += mapview.TopLeftLatLong + '<br />';
    //stats += mapview.BottomRightLatLong;

    Pelm.Map.updateStatusBar( 'left', stats );
};

/**
 * Pelmorex Map Tile object
 *
 * @fileoverview
 * This is a tile map object.
 *
 * @author Jim Ing (jing@pelmorex.com)
 */

Pelm.Map.Tile =
{
    'hostname'               : '',                                               // (NEW)
    'proxy'                  : '',                                               // tileagent.php
    'basepath'               : '',                                               // http://72.14.162.214:8080/images or http://72.14.162.214/images
    'basepath_pat'           : '^http://webmaptiles',
    'border_path'           : '',

    'callback'               : '',                                               // Pelm.Map.Tile.handleResponse
    'layer_ids'              : '',                                               // noaaport_satir,mosaic
    'base_id'                : '',                                               // radar
    'interval'               : '',                                               // 60
    'duration'               : '',                                               // 6
    'interval_future'        : '',                                               // 60 (NEW)
    'duration_future'        : '',                                               // 12 (NEW)
    'rounded'                : '',                                               // 1
    'debug'                  : '',                                               // 0

    'css_class_prefix'       : '',                                               // MSVE_ImageTile msve_
    'css_class_suffix'       : '',                                               // _tile
    'extension'              : '',                                               // png
    'min_zoom_level'         : '',                                               // 1
    'max_zoom_level'         : '',                                               // 8
    'num_servers'            : '',                                               // 1 - 12 (number of sub-domains to use)
    'blank_tile_src'         : '',                                               // images/blank.gif

    'elem_ids' :
    {
        'tile_layers'        : '',                                               // tile_layers_control
        'toggle_prefix'      : '',                                               // toggle_
        'speed_val'          : '',                                               // speed_value
        'start'              : '',                                               // start
        'stop'               : '',                                               // stop
        'spinner_prefix'     : '',                                               // spinner_
        'spinner_txt_prefix' : '',                                               // spinner_txt_
        'label_prefix'       : '',                                               // label_
        'opacity_prefix'     : '',                                               // opacity_
        'slider_suffix'      : '',                                               // _slider
        'order_prefix'       : '',                                               // order_
        'logo_prefix'        : '',                                               // logo_ (NEW)
        'legend_prefix'      : '',                                               // legend_ (NEW)
        'timestep_num'       : '',                                               // timestep_num
        'quad_num'           : '',                                               // quad_num
        'bbox'               : '',                                               // bbox
        'connection_type'    : ''                                                // connection_type
    },

    'html' :
    {
        'date_start'         : '',
        'date_end'           : '',
        'time_start'         : '',
        'time_end'           : ''
    },

    //--- Settings -----------------------------------------------------------

    'autoplay'               : true,

    'earth_radius'           : 6378137,                                          // meters
    'tile_size'              : 256,                                              // pixels

    'average_tile_size_bytes' :
    {
        'noaaport_satir' : 30720,                                                // 30 KB
        'radar'          : 7168,                                                 // 7 KB
        'mosaic'         : 8192,                                                 // 8 KB
        'nowcast_dbz'    : 7168,                                                 // 7 KB
        'nowcast_type'   : 8192                                                  // 8 KB
    },

    //--- "Private" properties -----------------------------------------------

    'request_url'            : '',                                               // (NEW)

    'num_timesteps'          : 0,

    'reverse_map'            : {},                                               // (NEW)

    'coverage'               : {},

    'layer_list'             : [],
    'layer_list_active'      : [],

    'time_elems_all'         : [],
    'time_labels_all'        : [],

    'time_elems'             : {},
    'time_labels'            : {},

    'coverage_points'        : {},
    'has_coverage'           : null,

    'has_data'               : {},                                               // (NEW)

    'quad_elems'             : {},

    'quad_ids'               : {},

    'tile_cache'             : {},

    'chk_id'                 : '',
    'img_id'                 : [],
    'ani_id'                 : [],

    'ready_state'            : false,                                            // (NEW)
    'ready_id'               : '',
    'img_loaded'             : {},
    'timer'                  : {},
    'bytes'                  : {},

    'quad_cnt'               : 0,
    'quads_found'            : null,
    'quad_check_cnt'         : 0,
    'img_check_cnt'          : 0,

    'pos'                    : [],
    'cnt'                    : 0,
    'iterations'             : 0,
    'delay_ms'               : 100,
    'animating'              : false,

    'jsr_id'                 : '',
    'jsr'                    : null
};

/**
 * @description
 * Initialize the tile object.
 */
Pelm.Map.Tile.init = function()
{
    Pelm.Console.info( 'Pelm.Map.Tile.init:' );
    Pelm.Map.updateStatusBar( 'left', 'Initializing tile layer...' );

    // need to null everything to allow garbage collector to clean up memory

    // MSIE
    Pelm.Map.Tile.layer_list = null;
    Pelm.Map.Tile.layer_list_active = null;
    Pelm.Map.Tile.time_elems_all = null;
    Pelm.Map.Tile.time_labels_all = null;
    Pelm.Map.Tile.time_elems = null;
    Pelm.Map.Tile.time_labels = null;
    Pelm.Map.Tile.coverage_points = null;
    Pelm.Map.Tile.quad_elems = null;
    Pelm.Map.Tile.tile_cache = null;

    // Firefox
    delete Pelm.Map.Tile.layer_list;
    delete Pelm.Map.Tile.layer_list_active;
    delete Pelm.Map.Tile.time_elems_all;
    delete Pelm.Map.Tile.time_labels_all;
    delete Pelm.Map.Tile.time_elems;
    delete Pelm.Map.Tile.time_labels;
    delete Pelm.Map.Tile.coverage_points;
    delete Pelm.Map.Tile.quad_elems;
    delete Pelm.Map.Tile.tile_cache;

    // reset properties
    Pelm.Map.Tile.layer_list = [];
    Pelm.Map.Tile.layer_list_active = [];
    Pelm.Map.Tile.time_elems_all = [];
    Pelm.Map.Tile.time_labels_all = [];
    Pelm.Map.Tile.time_elems = {};
    Pelm.Map.Tile.time_labels = {};

    Pelm.Map.Tile.coverage_points = {};
    Pelm.Map.Tile.has_coverage = null;

    Pelm.Map.Tile.quad_elems = {};
    Pelm.Map.Tile.tile_cache = {};

    Pelm.Map.Tile.ready_state = false;
    Pelm.Map.Tile.img_loaded = {};
    Pelm.Map.Tile.timer = {};

    Pelm.Map.Tile.quad_cnt = 0;
    Pelm.Map.Tile.quads_found = null;
    Pelm.Map.Tile.quad_check_cnt = 0;

    Pelm.Map.Tile.img_check_cnt = 0;
    Pelm.Map.Tile.pos = [];
    Pelm.Map.Tile.cnt = 0;
    Pelm.Map.Tile.iterations = 0;

    // initialize settings
    Pelm.Map.Tile.earth_circum = Pelm.Map.Tile.earth_radius * 2.0 * Math.PI;
    Pelm.Map.Tile.earth_half_circum = Pelm.Map.Tile.earth_circum / 2;

    Pelm.Map.Tile.initLayerList();

    // reset percentage loaded and layer progress bars
    for( var i = 0; i < Pelm.Map.Tile.layer_list.length; i++ )
    {
        Pelm.Util.setInnerHTML( Pelm.Map.Tile.elem_ids.spinner_txt_prefix + Pelm.Map.Tile.layer_list[ i ], '&mdash;' );

        if( document.getElementById( 'load_percent_' + Pelm.Map.Tile.layer_list[ i ] ) )
        {
            document.getElementById( 'load_percent_' + Pelm.Map.Tile.layer_list[ i ] ).style.width = '0%';
        }
    }

    // check coverage area
    var view = Pelm.Map.VE.GetMapView();
    Pelm.Map.Tile.checkCoverage( view.TopLeftLatLong.Latitude, view.TopLeftLatLong.Longitude, view.BottomRightLatLong.Latitude, view.BottomRightLatLong.Longitude, Pelm.Map.VE.GetZoomLevel() );

    Pelm.Map.Tile.has_coverage = false;
    for( var p in Pelm.Map.Tile.coverage_points )
    {
        if( Pelm.Map.Tile.coverage_points[ p ] !== 0 )
        {
            Pelm.Map.Tile.has_coverage = true;
            break;
        }
    }
    Pelm.Console.debug( 'has_coverage: ' + Pelm.Map.Tile.has_coverage );

    return;
};

/**
 * @description
 * Dynamically populate the layer list from the HTML form.
 *
 * +-----------------------+----------------+
 * | Input Element ID      | Layer ID       |
 * +-----------------------+----------------+
 * | toggle_noaaport_satir | noaaport_satir |
 * | toggle_radar          | radar          |
 * | toggle_mosaic         | mosaic         |
 * +-----------------------+----------------+
 */
Pelm.Map.Tile.initLayerList = function()
{
    Pelm.Console.info( 'Pelm.Map.Tile.initLayerList:' );
    Pelm.Map.updateStatusBar( 'left', 'Initializing layer list...' );

    Pelm.Map.Tile.reverse_map = [];
    Pelm.Map.Tile.layer_list = [];
    Pelm.Map.Tile.layer_list_active = [];

    if( document.getElementById( Pelm.Map.Tile.elem_ids.tile_layers ) )
    {
        var div_elem = document.getElementById( Pelm.Map.Tile.elem_ids.tile_layers );
        var inp_elems = div_elem.getElementsByTagName( 'input' );

        var re = new RegExp( "^" + Pelm.Map.Tile.elem_ids.toggle_prefix + "(.*)" );

        var inp_elem;
        var matches;
        var layer_id, lid;
        var parts, subparts;

        for( var i = 0; i < inp_elems.length; i++ )
        {
            inp_elem = inp_elems[ i ];

            matches = re.exec( inp_elem.id );

            if( matches !== null )
            {
                layer_id = Pelm.Map.Tile.getLayerID( matches[ 1 ] );

                Pelm.Map.Tile.layer_list.push( matches[ 1 ] );

                if( inp_elem.checked === true )
                {
                    parts = matches[ 1 ].split( "_" );

                    if( parts[ 0 ] == 'p' || parts[ 0 ] == 'f' )
                    {
                        lid = layer_mapping[ parts[ 0 ] ][ parts[ 1 ] ][ parts[ 2 ] ];
                        Pelm.Map.Tile.layer_list_active.push( lid );
                    }
                    else
                    {
                        subparts = parts[ 0 ].split( "." );

                        for( var j = 0; j < subparts.length; j++ )
                        {
                            if( subparts[ j ] == 'p' || subparts[ j ] == 'f' )
                            {
                                lid = layer_mapping[ subparts[ j ] ][ parts[ 1 ] ][ parts[ 2 ] ];
                                Pelm.Map.Tile.layer_list_active.push( lid );
                            }
                        }
                    }
                }

                Pelm.Map.Tile.reverse_map[ layer_id ] = matches[ 1 ];
            }
        }

        //Pelm.Console.warn( 'layer_list_active = ' + Pelm.Map.Tile.layer_list_active.length );
        if( Pelm.Map.Tile.layer_list_active.length === 0 )
        {
            Pelm.Map.updateStatusBar( 'left', '' );
            Pelm.Map.updateStatusBar( 'center', '' );
            Pelm.Map.updateStatusBar( 'right', '' );

            disableTimestepSlider(); // TODO
        }

        //Pelm.Console.debug( 'Pelm.Map.Tile.reverse_map:' );
        //Pelm.Console.debug( Pelm.Map.Tile.reverse_map );

        //Pelm.Console.debug( 'Pelm.Map.Tile.layer_list:' );
        //Pelm.Console.debug( Pelm.Map.Tile.layer_list );

        //Pelm.Console.debug( 'Pelm.Map.Tile.layer_list_active:' );
        //Pelm.Console.debug( Pelm.Map.Tile.layer_list_active );
    }
    else
    {
        Pelm.Console.warn( 'Could not find the element containing the tile layer controls.' );
    }
};

/**
 * @description
 * Send the JSON request for tile data.
 */
Pelm.Map.Tile.sendRequest = function()
{
    Pelm.Console.info( 'Pelm.Map.Tile.sendRequest:' );
    Pelm.Map.updateStatusBar( 'left', 'Sending request...' );

    Pelm.Map.Tile.jsr = new Pelm.JSR( Pelm.Map.Tile.request_url );
    Pelm.Map.Tile.jsr.buildScriptTag();
    Pelm.Map.Tile.jsr.addScriptTag();

    return;
};

/**
 * @description
 * This is callback function to handle the JSON response.
 */
Pelm.Map.Tile.handleResponse = function()
{
    Pelm.Console.info( 'Pelm.Map.Tile.handleResponse:' );
    Pelm.Map.updateStatusBar( 'left', 'Handling response...' );

    if( RS )
    {
        var visible;
        var opacity, zindex, cnt, pos;
        var hasData = true; // TODO?
        var layer_id;
        var checkbox_id;

        Pelm.Map.Tile.num_past_timesteps = ( RS.metadata.num_past_timesteps > 0 ) ? RS.metadata.num_past_timesteps : 0;
        //Pelm.Map.Tile.coverage = RS.metadata.coverage;

        for( layer_id in RS.layers )
        {
            if( layer_id != 'metadata' )
            {
                Pelm.Map.Tile.time_elems[ layer_id ] = [];
                Pelm.Map.Tile.time_labels[ layer_id ] = [];

                checkbox_id = Pelm.Map.Tile.reverse_map[ layer_id ];

                visible = true;

                if( Pelm.Map.Tile[ Pelm.Map.Tile.elem_ids.opacity_prefix + checkbox_id ] )
                {
                    opacity = document.getElementById( Pelm.Map.Tile.elem_ids.opacity_prefix + checkbox_id ).value;
                }
                else
                {
                    opacity = document.getElementById( Pelm.Map.Tile.elem_ids.opacity_prefix + 'all' ).value;
                }

                zindex = document.getElementById( Pelm.Map.Tile.elem_ids.order_prefix + checkbox_id ).value;

                if( Pelm.Map.Tile.coverage_points[ layer_id ] && Pelm.Map.Tile.coverage_points[ layer_id ] !== 0 )
                {
                    Pelm.Util.setInnerHTML( Pelm.Map.Tile.elem_ids.spinner_txt_prefix + checkbox_id, '0%' );

                    // show spinner
                    //Pelm.Map.Tile.showSpinner( layer_id );
                }
                else
                {
                    Pelm.Util.setInnerHTML( Pelm.Map.Tile.elem_ids.spinner_txt_prefix + checkbox_id, '&mdash;' );
                }

                // get last time step -- safer than using first time step in case
                // tile generation stops for a long period
                Pelm.Map.Tile.has_data[ layer_id ] = false;
                for( var h in RS.layers[ layer_id ].timesteps )
                {
                    //Pelm.Console.debug( '>>> ' + h + ': ' + RS.layers[ layer_id ].timesteps[ h ] );
                    if( RS.layers[ layer_id ].timesteps[ h ] != '0' )
                    {
                        Pelm.Map.Tile.has_data[ layer_id ] = true;
                    }
                }
                var last_timestepid = h;

                if( Pelm.Map.Tile.has_data[ layer_id ] === true )
                {
                    cnt = 0;
                    var first_timestepid;
                    var any_timestepid;
                    for( var i in RS.layers[ layer_id ].timesteps )
                    {
                        Pelm.Console.debug( i + '=' + RS.layers[ layer_id ].timesteps[ i ] + ', layer_subdir=' + RS.layers[ layer_id ].layer_subdir );

                        // get first time step
                        cnt++;
                        if( cnt == 1 )
                        {
                            first_timestepid = i;
                        }

                        // get any non-zero time step
                        if( RS.layers[ layer_id ].timesteps[ i ] != '0' )
                        {
                            any_timestepid = i;
                        }

                        // use base layer as reference for ideal time steps
                        if( layer_id == Pelm.Map.Tile.base_id )
                        {
                            Pelm.Map.Tile.time_elems_all.push( i );
                            Pelm.Map.Tile.time_labels_all.push( Pelm.Map.Tile.formatTimestamp( i ) );
                        }

                        Pelm.Map.Tile.time_elems[ layer_id ].push( RS.layers[ layer_id ].timesteps[ i ] );
                        Pelm.Map.Tile.time_labels[ layer_id ].push( Pelm.Map.Tile.formatTimestamp( RS.layers[ layer_id ].timesteps[ i ] ) );
                    }

                    Pelm.Map.Tile.showSpinner( layer_id );

                    if( RS.layers[ layer_id ].timesteps[ last_timestepid ] != '0' )
                    {
                        Pelm.Map.Tile.addLayer( layer_id, RS.layers[ layer_id ].layer_subdir, RS.layers[ layer_id ].timesteps[ last_timestepid ], opacity, zindex, visible );
                        pos = last_timestepid;
                    }
                    else
                    {
                        Pelm.Console.warn( 'Intial time step for ' + layer_id + ' is zero! Using "any" time step of ' + any_timestepid );

                        Pelm.Map.Tile.addLayer( layer_id, RS.layers[ layer_id ].layer_subdir, RS.layers[ layer_id ].timesteps[ any_timestepid ], opacity, zindex, visible );
                        pos = any_timestepid;
                    }

                    Pelm.Map.updateStatusBar( 'right', Pelm.Map.Tile.formatTimestamp( last_timestepid ) );
                }
                else
                {
                    Pelm.Console.warn( 'Skipping ' + layer_id );
                    Pelm.Map.updateStatusBar( 'left', 'Skipping ' + layer_id );
                    Pelm.Util.setInnerHTML( 'load_percent_' + checkbox_id, '<div class="status_error">' + messages.not_available[ Pelm.lang ] + '</div>' );
                }
            }
        }

        if( hasData === true )
        {
            // update master time frame meter
            if( Pelm.Map.Tile.layer_list_active.length > 0 )
            {
                if( Pelm.Map.Tile.timestep_slider )
                {
                    //Pelm.Map.Tile.timestep_slider.f_setValue( 0 );
                    //Pelm.Map.Tile.timestep_slider.f_setValue( Pelm.Map.Tile.time_elems_all.length - 1 );
                    Pelm.Map.Tile.timestep_slider.f_setValue( pos );
                }
            }

            // update number of timesteps in the legend
            Pelm.Util.setInnerHTML( Pelm.Map.Tile.elem_ids.timestep_num, Pelm.Map.Tile.time_elems_all.length );

            // update max value of Tigra slider
            Pelm.Map.Tile.timestep_slider.n_maxValue = Pelm.Map.Tile.time_elems_all.length - 1;
            Pelm.Map.Tile.timestep_slider.n_pix2value = Pelm.Map.Tile.timestep_slider.n_pathLength / ( Pelm.Map.Tile.timestep_slider.n_maxValue - Pelm.Map.Tile.timestep_slider.n_minValue );

            // wait a few seconds for VE tiles to load before checking
            Pelm.Map.Tile.chk_id = Pelm.Util.setTimer( 'Pelm.Map.Tile.checkQuads()', 1000 );
        }
        else
        {
            Pelm.Console.warn( 'Missing data' );

            Pelm.Map.updateStatusBar( 'left', 'Missing data' );
            //Pelm.Map.hideLoading();
        }

        // remove script tag to clean up memory
        // TODO: this causes IE to crash when the map is panned or zoomed
        //Pelm.Map.Tile.jsr.removeScriptTag();
    }
    else
    {
        Pelm.Console.warn( 'Missing RS' );
        Pelm.Map.updateStatusBar( 'left', 'Missing data' );
        //Pelm.Map.hideLoading();
    }

    return;
};

/**
 * @description
 * Add VE tile layer.
 */
Pelm.Map.Tile.addLayer = function( tileId, layerSubDir, timeStep, opacity, zindex, visibleOnLoad )
{
    Pelm.Console.info( 'Pelm.Map.Tile.addLayer: [' + tileId + '],[' + layerSubDir + '],[' + timeStep + '],[' + opacity + '],[' + zindex + '],[' + visibleOnLoad );
    Pelm.Map.updateStatusBar( 'left', 'Adding ' + tileId + ' layer...' );

    // remove existing layer
    if( Pelm.Map.VE.GetTileLayerByID( tileId ) )
    {
        Pelm.Map.VE.DeleteTileLayer( tileId );
    }

    if( timeStep != '0' )
    {
        var urlPath;
        if( Pelm.Map.Tile.num_servers > 1 )
        {
            //var subdomain_num = Math.ceil( Math.random() * Pelm.Map.Tile.num_servers );
            //urlPath = 'http://webmaptiles' + subdomain_num + '.weather.ca/images/' + layerSubDir + '/' + timeStep + '/%4.' + Pelm.Map.Tile.extension;

            urlPath = 'http://webmaptiles1.weather.ca/images/' + layerSubDir + '/' + timeStep + '/%4.' + Pelm.Map.Tile.extension;
        }
        else
        {
            urlPath = Pelm.Map.Tile.basepath + '/' + layerSubDir + '/' + timeStep + '/%4.' + Pelm.Map.Tile.extension;
        }
        //Pelm.Console.warn( urlPath );

        try
        {
            var tileSourceSpec = new VETileSourceSpecification( tileId, urlPath );

            tileSourceSpec.NumServers = Pelm.Map.Tile.num_servers;

            tileSourceSpec.Bounds = [
                new VELatLongRectangle(
                    new VELatLong( Pelm.Map.Tile.coverage[ tileId ].TopLeft.lat, Pelm.Map.Tile.coverage[ tileId ].TopLeft.lon ),
                    new VELatLong( Pelm.Map.Tile.coverage[ tileId ].BottomRight.lat, Pelm.Map.Tile.coverage[ tileId ].BottomRight.lon )
                )
            ];

            tileSourceSpec.MinZoom = Pelm.Map.Tile.min_zoom_level;
            tileSourceSpec.MaxZoom = Pelm.Map.Tile.max_zoom_level;

            if( IE6 )
            {
                // Don't set the opacity -- setting the opacity causes black
                // areas to appear around the tile images.
            }
            else
            {
                tileSourceSpec.Opacity = opacity;
            }

            tileSourceSpec.ZIndex = zindex;

            Pelm.Console.debug( tileSourceSpec );

            //Pelm.Map.VE.AddTileLayer( tileSourceSpec, visibleOnLoad );
            Pelm.Map.VE.AddTileLayer( tileSourceSpec, true );
        }
        catch( e )
        {
            Pelm.Console.error( e.message );
        }
    }
    else
    {
        Pelm.Console.warn( 'Got timestep of 0!' );
    }
};

/**
 * @description
 * Add border tile layer.
 */
Pelm.Map.Tile.addBorderLayer = function( tileId, opacity, zindex, visibleOnLoad )
{
    Pelm.Console.info( 'Pelm.Map.Tile.addBorderLayer: [' + tileId + '],[' + opacity + '],[' + zindex + '],[' + visibleOnLoad );
    Pelm.Map.updateStatusBar( 'left', 'Adding ' + tileId + ' layer...' );

    // remove existing layer
    if( Pelm.Map.VE.GetTileLayerByID( tileId ) )
    {
        Pelm.Map.VE.DeleteTileLayer( tileId );
    }

    try
    {
        var tileSourceSpec = new VETileSourceSpecification( tileId, Pelm.Map.Tile.border_path );

        tileSourceSpec.NumServers = 1;
        tileSourceSpec.MinZoomLevel = 0;
        tileSourceSpec.MaxZoomLevel = 9;
        tileSourceSpec.GetTilePath = Pelm.Map.Tile.getBorderTilePath;

        tileSourceSpec.Bounds = [
            new VELatLongRectangle(
                new VELatLong( 60.3, -170.3 ),
                new VELatLong( 10.83, -29.43 )
            )
        ];

        if( IE6 )
        {
            // Don't set the opacity -- setting the opacity causes black
            // areas to appear around the tile images.
        }
        else
        {
            tileSourceSpec.Opacity = opacity;
        }

        tileSourceSpec.ZIndex = zindex;

        Pelm.Console.debug( tileSourceSpec );

        //Pelm.Map.VE.AddTileLayer( tileSourceSpec, visibleOnLoad );
        Pelm.Map.VE.AddTileLayer( tileSourceSpec, true );
    }
    catch( e )
    {
        Pelm.Console.error( e.message );
    }
};

/**
 * @description
 * Get border tile URLs.
 */
Pelm.Map.Tile.getBorderTilePath = function( tileContext )
{
    if( tileContext != null && tileContext != "undefined" )
    {
        var key = '';
        var cell = 0;

        if( tileContext.ZoomLevel < 10 )
        {
            key = key + "L0" + tileContext.ZoomLevel + "/";
        }
        else
        {
            key = key + "L" + tileContext.ZoomLevel + "/";
        }

        var x = tileContext.XPos;
        var y = tileContext.YPos;

        if( tileContext.XPos < 15 )
        {
            key = key + "R0000000" +  y.toString(16)  + "/";
        }
        else
        {
            key = key + "R000000" + y.toString( 16 ) + "/";
        }

        if( tileContext.YPos < 15 )
        {
            key = key + "C0000000" + x.toString( 16 );
        }
        else
        {
            key = key + "C000000" + x.toString( 16 );
        }

        var path = Pelm.Map.Tile.border_path + key + ".png";
        return path;
    }
    else{
        return false;
    }
};

/**
 * @description
 * Checks that all the VE tile quadrants are loaded.
 */
Pelm.Map.Tile.checkQuads = function()
{
    Pelm.Console.info( 'Pelm.Map.Tile.checkQuads: attempt=' + Pelm.Map.Tile.quad_check_cnt + ', setInterval id=' + Pelm.Map.Tile.chk_id );
    Pelm.Map.updateStatusBar( 'left', 'Checking quadrants...' );

    if( Pelm.Map.Tile.chk_id )
    {
        Pelm.Util.clearTimer( Pelm.Map.Tile.chk_id );
    }

    if( Pelm.Map.Tile.ani_id.all )
    {
        Pelm.Util.clearTimer( Pelm.Map.Tile.ani_id.all );
    }

    Pelm.Map.Tile.quad_check_cnt++;

    if( Pelm.Map.VE.GetZoomLevel() <= Pelm.Map.Tile.max_zoom_level )
    {
        Pelm.Map.Tile.quad_elems = Pelm.Map.Tile.loadQuads();
        Pelm.Console.debug( Pelm.Map.Tile.quad_elems );

        var layer_id;
        var cnt;

        Pelm.Map.Tile.quads_found = true;

        // check number of quads
        Pelm.Console.debug( Pelm.Map.Tile.layer_list_active );
        for( var i = 0; i < Pelm.Map.Tile.layer_list_active.length; i++ )
        {
            layer_id = Pelm.Map.Tile.layer_list_active[ i ];

            // check if layer has data
            if( Pelm.Map.Tile.has_data[ layer_id ] === true )
            {
                // check coverage area
                Pelm.Console.debug( Pelm.Map.Tile.coverage_points[ layer_id ] + ', ' + Pelm.Map.Tile.coverage_points[ layer_id ] );
                if( Pelm.Map.Tile.coverage_points[ layer_id ] && Pelm.Map.Tile.coverage_points[ layer_id ] !== 0 )
                {
                    cnt = 0;
                    for( var j in Pelm.Map.Tile.quad_elems[ layer_id ] )
                    {
                        cnt++;
                    }

                    Pelm.Console.debug( layer_id + ': ' + cnt + ' == ' + Pelm.Map.Tile.quad_ids[ Pelm.Map.Tile.elem_ids.bbox + '_' + layer_id ].length + '?' );

                    if( cnt < Pelm.Map.Tile.quad_ids[ Pelm.Map.Tile.elem_ids.bbox + '_' + layer_id ].length )
                    {
                        Pelm.Map.Tile.quads_found = false;
                    }

                    if( Pelm.Map.Tile.quads_found === true )
                    {
                        if( !Pelm.Map.Tile.tile_cache[ layer_id ] )
                        {
                            Pelm.Map.Tile.fetchTiles( Pelm.Map.Tile.layer_list_active[ i ] );
                        }
                    }
                }
            }
            else
            {
                Pelm.Console.warn( 'Skipping ' + layer_id );
                Pelm.Map.updateStatusBar( 'left', 'Skipping ' + layer_id );
            }
        }

        if( Pelm.Map.Tile.quads_found === false )
        {
            // TODO: hard-coded number
            if( Pelm.Map.Tile.quad_check_cnt < 90 )
            {
                Pelm.Map.Tile.chk_id = Pelm.Util.setTimer( 'Pelm.Map.Tile.checkQuads()', 1000 );
            }
            else
            {
                Pelm.Console.warn( 'Gave up!' );

                // TODO?
                Pelm.Map.Tile.ready_state = true;

                // reset counter
                Pelm.Map.Tile.quad_check_cnt = 0;

                // fetch tile layer
                for( var l = 0; l < Pelm.Map.Tile.layer_list_active.length; l++ )
                {
                    layer_id = Pelm.Map.Tile.layer_list_active[ l ];

                    if( Pelm.Map.Tile.coverage_points[ layer_id ] && Pelm.Map.Tile.coverage_points[ layer_id ] !== 0 )
                    {
                        Pelm.Map.Tile.fetchTiles( layer_id );
                    }
                }

                Pelm.Map.Tile.ready_id = Pelm.Util.setTimer( 'Pelm.Map.Tile.isReady()', 1000 );
            }
        }
        else
        {
            Pelm.Map.Tile.ready_id = Pelm.Util.setTimer( 'Pelm.Map.Tile.isReady()', 1000 );
        }
    }

    Pelm.Map.updateStatusBar( 'left', '' );
    //Pelm.Map.hideLoading();
};

/**
 * @description
 * Checks if the all the tiles are loaded.
 */
Pelm.Map.Tile.isReady = function()
{
    Pelm.Console.info( 'Pelm.Map.Tile.isReady: ' + Pelm.Map.Tile.ready_id );

    var ready = true;
    var layer_id;

    for( var i = 0; i < Pelm.Map.Tile.layer_list_active.length; i++ )
    {
        layer_id = Pelm.Map.Tile.layer_list_active[ i ];

        // check if layer has data
        if( Pelm.Map.Tile.has_data[ layer_id ] === true )
        {
            if( Pelm.Map.Tile.coverage_points[ layer_id ] )
            {
                Pelm.Console.debug( layer_id + ': coverage_points=' + Pelm.Map.Tile.coverage_points[ layer_id ] + ', percentage_loaded=' + Pelm.Map.Tile.img_loaded[ layer_id ] );
                if( Pelm.Map.Tile.coverage_points[ layer_id ] !== 0 )
                {
                    if( Pelm.Map.Tile.img_loaded[ layer_id ] !== 100 )
                    {
                        ready = false;
                        break;
                    }
                }
            }
        }
        else
        {
            Pelm.Console.warn( 'Skipping ' + layer_id );
            Pelm.Map.updateStatusBar( 'left', 'Skipping ' + layer_id );
        }
    }

    // auto start the animation
    if( ready === true )
    {
        Pelm.Util.clearTimer( Pelm.Map.Tile.ready_id );

        Pelm.Map.Tile.ready_state = true;
        Pelm.Console.debug( 'Ready to animate' );
        Pelm.Map.updateStatusBar( 'left', 'Click the Play button animate' );
        Pelm.Map.hideLoading();

        if( Pelm.Map.Tile.autoplay === true )
        {
            var func = 'Pelm.Map.Tile.animate( "all" )';
            Pelm.Map.Tile.ani_id.all = Pelm.Util.setTimer( func, document.getElementById( Pelm.Map.Tile.elem_ids.speed_val ).value );
        }
        else
        {
            // enable play button
            Pelm.Map.Tile.toggleButton( false, Pelm.Map.Tile.elem_ids.start, Pelm.Map.Tile.html.start_on );
        }
    }
    else
    {
        // TODO: When should we stop checking?
        if( Pelm.Map.Tile.has_coverage === true )
        {
            //Pelm.Console.debug( 'Keep checking...' );
        }
        else
        {
            //Pelm.Console.debug( 'Stop checking...' );
            Pelm.Util.clearTimer( Pelm.Map.Tile.ready_id );
        }
    }
};

/**
 * @description
 * Finds all the VE quadrants and stores them in an accessible object.
 */
Pelm.Map.Tile.loadQuads = function()
{
    Pelm.Console.info( 'Pelm.Map.Tile.loadQuads:' );
    Pelm.Map.updateStatusBar( 'left', 'Loading quadrants...' );

    var elems = {};

    var all = document.all ? document.all.tags( 'img' ) : document.getElementsByTagName( 'img' );
    if( IE6 )
    {
        all = document.all ? document.all.tags( 'div' ) : document.getElementsByTagName( 'div' );
    }

    var img_src, parts, layer_id, quad_id;

    for( var e = 0; e < all.length; e++ )
    {
        img_src = all[ e ].src;
        if( IE6 && all[ e ].className && all[ e ].className == 'MSVE_ImageTile' && all[ e ].style.filter )
        {
            img_src = all[ e ].filters[ 'DXImageTransform.Microsoft.AlphaImageLoader' ].src;
        }

        if( img_src && img_src.match( Pelm.Map.Tile.basepath_pat ) )
        {
            parts = img_src.split( '/' );

            // nowcast
            if( isNaN( parts[ parts.length - 3 ] ) === true )
            {
                layer_id = parts[ parts.length - 3 ];
                //Pelm.Console.debug( 'Not Nowcast: ' + layer_id );
            }
            else
            {
                layer_id = parts[ parts.length - 5 ] + '_' + parts[ parts.length - 4 ]; // TODO
                //Pelm.Console.debug( 'Nowcast: ' + layer_id );
            }

            // check if layer has data
            if( Pelm.Map.Tile.has_data[ layer_id ] === true )
            {
                quad_id = parts[ parts.length - 1 ].split( '.' )[ 0 ];
                //Pelm.Console.debug( 'class=' + all[ e ].className + ', layer_id=' + layer_id + ', quad_id=' + quad_id + ', src=' + all[ e ].src );

                // In VE Map SDK 6.2, the CSS class for tile images changed from
                // "MSVE_ImageTile" to "MSVE_ImageTile msve_radar_tile"

                if( all[ e ].className == 'MSVE_ImageTile' || all[ e ].className == Pelm.Map.Tile.css_class_prefix + layer_id + Pelm.Map.Tile.css_class_suffix )
                {
                    if( !elems[ layer_id ] )
                    {
                        elems[ layer_id ] = {};
                    }

                    if( !elems[ layer_id ][ quad_id ] )
                    {
                        elems[ layer_id ][ quad_id ] = {};
                    }

                    // store the quad element so we can access it later because VE
                    // tiles don't have IDs
                    elems[ layer_id ][ quad_id ] = all[ e ];
                }
            }
            else
            {
                Pelm.Console.warn( 'Skipping ' + layer_id );
                Pelm.Map.updateStatusBar( 'left', 'Skipping ' + layer_id );
            }
        }
    }

    Pelm.Console.debug( 'Pelm.Map.Tile.quad_elems =' );
    Pelm.Console.debug( elems );

    return elems;
};

/**
 * @description
 * Fetches the tiles for a layer and caches them in a tile cache object.
 */
Pelm.Map.Tile.fetchTiles = function( layer_id )
{
    Pelm.Console.info( 'Pelm.Map.Tile.fetchTiles: ' + layer_id );

    Pelm.Map.updateStatusBar( 'left', 'Fetching tiles...' );
    //Pelm.Map.showLoading();
    //Pelm.Map.Tile.showSpinner( layer_id );

    // check when images are loaded
    var func = 'Pelm.Map.Tile.checkImages( "' + layer_id + '" )';
    Pelm.Map.Tile.img_id[ layer_id ] = Pelm.Util.setTimer( func, 500 );

    Pelm.Map.Tile.tile_cache[ layer_id ] = {};

    var timestep_id;
    var real_timestep_id;
    var found;
    var img_path;
    var tile_obj;

    // initialize layer's tile cache
    if( !Pelm.Map.Tile.tile_cache[ layer_id ] )
    {
        Pelm.Map.Tile.tile_cache[ layer_id ] = {};
    }

    for( var quad_id in Pelm.Map.Tile.quad_elems[ layer_id ] )
    {
        if( !Pelm.Map.Tile.tile_cache[ layer_id ][ quad_id ] )
        {
            Pelm.Map.Tile.tile_cache[ layer_id ][ quad_id ] = {};
        }

        for( var i = 0; i < Pelm.Map.Tile.time_elems_all.length; i++ )
        {
            Pelm.Map.Tile.cnt++;

            timestep_id = Pelm.Map.Tile.time_elems_all[ i ];
            real_timestep_id = Pelm.Map.Tile.time_elems[ layer_id ][ i ];
            //Pelm.Console.debug( timestep_id + ' : ' + real_timestep_id );

            if( !Pelm.Map.Tile.tile_cache[ layer_id ][ quad_id ][ timestep_id ] )
            {
                Pelm.Map.Tile.tile_cache[ layer_id ][ quad_id ][ timestep_id ] = {};
            }

            tile_obj = new Image();

            found = false;
            for( var j = 0; j < Pelm.Map.Tile.time_elems[ layer_id ].length; j++ )
            {
                if( Pelm.Map.Tile.time_elems[ layer_id ][ j ] == real_timestep_id )
                {
                    found = true;
                    break;
                }
            }

            if( found === true )
            {
                if( real_timestep_id != '0' )
                {
                    if( Pelm.Map.Tile.num_servers > 1 )
                    {
                        var subdomain_num = Math.ceil( Math.random() * Pelm.Map.Tile.num_servers );
                        img_path = 'http://webmaptiles' + subdomain_num + '.weather.ca/images/' + RS.layers[ layer_id ].layer_subdir + '/' + real_timestep_id + '/' + quad_id + '.png';
                    }
                    else
                    {
                        img_path = Pelm.Map.Tile.basepath + '/' + RS.layers[ layer_id ].layer_subdir + '/' + real_timestep_id + '/' + quad_id + '.png';
                    }
                }
                else
                {
                    Pelm.Console.warn( 'Using a blank image for 0! (' + layer_id + ', ' + quad_id + ', ' + timestep_id + ')' );

                    img_path = Pelm.Map.Tile.blank_tile_src;
                }
            }
            else
            {
                Pelm.Console.warn( 'Using a blank image!' );

                img_path = Pelm.Map.Tile.blank_tile_src;
            }
            Pelm.Console.debug( img_path );

            tile_obj.src = img_path;

            Pelm.Map.Tile.tile_cache[ layer_id ][ quad_id ][ timestep_id ] = tile_obj;
        }
    }

    //Pelm.Console.debug( Pelm.Map.Tile.quad_elems );
    Pelm.Console.debug( 'Pelm.Map.Tile.tile_cache =' );
    Pelm.Console.debug( Pelm.Map.Tile.tile_cache );

    Pelm.Map.updateStatusBar( 'left', '' );
    //Pelm.Map.hideLoading();
};

/**
 * @description
 * Animation handler.
 */
Pelm.Map.Tile.animate = function()
{
    //Pelm.Console.debug( 'animate:' );

    // TODO: check once
    var enabled = false;
    var layer_id;
    var checkbox_id;

    for( var i = 0; i < Pelm.Map.Tile.layer_list_active.length; i++ )
    {
        layer_id = Pelm.Map.Tile.layer_list_active[ i ];
        checkbox_id = Pelm.Map.Tile.reverse_map[ layer_id ];

        if( document.getElementById( Pelm.Map.Tile.elem_ids.toggle_prefix + checkbox_id ).disabled === false )
        {
            enabled = true;
            break;
        }
    }

    Pelm.Util.clearTimer( Pelm.Map.Tile.ani_id.all );

    if( enabled === true )
    {
        // toggle the buttons at the beginning
        if( Pelm.Map.Tile.animating === false )
        {
            Pelm.Map.Tile.toggleButtonsAll( 'on' );
            Pelm.Map.Tile.toggleButton( true, Pelm.Map.Tile.elem_ids.start, Pelm.Map.Tile.html.start_off );
        }

        Pelm.Map.Tile.iterations++;

        if( Pelm.Map.Tile.layer_list_active.length === 0 )
        {
            // do nothing
        }
        else
        {
            Pelm.Map.Tile.animating = true;

            Pelm.Map.Tile.animateLayers();
        }

        // re-animate (checks for changes to speed)
        var func = 'Pelm.Map.Tile.animate( "all" )';
        Pelm.Map.Tile.ani_id.all = Pelm.Util.setTimer( func, Pelm.Map.Tile.delay_ms );
    }
    else
    {
        Pelm.Map.Tile.toggleButtonsAll( 'off' );
    }

    return;
};

/**
 * @description
 * Animates multiple layers.
 */
Pelm.Map.Tile.animateLayers = function()
{
    //Pelm.Console.info( 'animateLayers:' );

    var new_pos;
    var timestep_id;
    var layer_id;

    // get time step
    new_pos = ( Pelm.Map.Tile.pos.all < ( Pelm.Map.Tile.time_elems_all.length - 1 ) ) ? Pelm.Map.Tile.pos.all + 1 : 0;
    timestep_id = Pelm.Map.Tile.time_elems_all[ new_pos ];

    // animate
    for( var i = 0; i < Pelm.Map.Tile.layer_list_active.length; i++ )
    {
        layer_id = Pelm.Map.Tile.layer_list_active[ i ];

        for( var quad_id in Pelm.Map.Tile.tile_cache[ layer_id ] )
        {
            //Pelm.Console.debug( '[' + layer_id + '][' + quad_id + '][' + timestep_id + '] = ' + Pelm.Map.Tile.tile_cache[ layer_id ][ quad_id ][ timestep_id ].src );
            //Pelm.Map.Tile.quad_elems[ layer_id ][ quad_id ].src = Pelm.Map.Tile.tile_cache[ layer_id ][ quad_id ][ timestep_id ].src;

            if( IE6 )
            {
                try
                {
                    Pelm.Map.Tile.quad_elems[ layer_id ][ quad_id ].filters[ 'DXImageTransform.Microsoft.AlphaImageLoader' ].src = Pelm.Map.Tile.tile_cache[ layer_id ][ quad_id ][ timestep_id ].src;
                }
                catch( e )
                {
                    // TODO: try to find this error - "The system cannot locate the resource specified."
                    //Pelm.Console.error( '[' + e.message + ']' );
                    //Pelm.Console.debug( '[' + layer_id + '][' + quad_id + '][' + timestep_id + ']' );
                }
            }
            else
            {
                Pelm.Map.Tile.quad_elems[ layer_id ][ quad_id ].src = Pelm.Map.Tile.tile_cache[ layer_id ][ quad_id ][ timestep_id ].src;
            }
        }
    }

    // update master time frame meter
    if( Pelm.Map.Tile.timestep_slider )
    {
        Pelm.Map.Tile.timestep_slider.f_setValue( new_pos );
    }
    //Pelm.Console.debug( new_pos );

    Pelm.Map.updateStatusBar( 'left', new_pos );

    Pelm.Map.updateStatusBar( 'right', Pelm.Map.Tile.time_labels_all[ new_pos ] );

    if( new_pos >= Pelm.Map.Tile.num_past_timesteps )
    {
        showIndicator( 'nowcast_indicator' ); // TODO
    }
    else
    {
        hideIndicator( 'nowcast_indicator' ); // TODO
    }

    // save position of current frame
    Pelm.Map.Tile.pos.all = new_pos;

    // add longer delay on last frame

    // REQUIREMENT: min delay = 2 sec, max delay = 4 sec
    if( Pelm.Map.Tile.pos.all == Pelm.Map.Tile.time_elems_all.length - 1 )
    {
        if( ( document.getElementById( Pelm.Map.Tile.elem_ids.speed_val ).value * 5 ) < 2000 )
        {
            Pelm.Map.Tile.delay_ms = 2000;
        }
        else if( ( document.getElementById( Pelm.Map.Tile.elem_ids.speed_val ).value * 5 ) > 4000 )
        {
            Pelm.Map.Tile.delay_ms = 4000;
        }
        else
        {
            Pelm.Map.Tile.delay_ms = document.getElementById( Pelm.Map.Tile.elem_ids.speed_val ).value * 5;
        }
    }
    else
    {
        Pelm.Map.Tile.delay_ms =  document.getElementById( Pelm.Map.Tile.elem_ids.speed_val ).value;
    }

    return;
};

/**
 * @description
 * Starts the animation loop.
 */
Pelm.Map.Tile.startLoop = function( layer_id )
{
    Pelm.Console.info( 'Pelm.Map.Tile.startLoop: ' + layer_id );
    Pelm.Map.updateStatusBar( 'left', 'Starting loop...' );

    var func = 'Pelm.Map.Tile.animate( "all" )';
    Pelm.Map.Tile.ani_id.all = Pelm.Util.setTimer( func, document.getElementById( Pelm.Map.Tile.elem_ids.speed_val ).value );

    return;
};

/**
 * @description
 * Stops the animation loop.
 */
Pelm.Map.Tile.stopLoop = function( layer_id, mode )
{
    Pelm.Console.info( 'Pelm.Map.Tile.stopLoop: ' + layer_id );
    Pelm.Map.updateStatusBar( 'left', 'Stopped ' + layer_id );

    if( Pelm.Map.Tile.img_id[ layer_id ] )
    {
        Pelm.Util.clearTimer( Pelm.Map.Tile.img_id[ layer_id ] );
    }

    Pelm.Map.Tile.animating = false;

    if( layer_id != 'all' )
    {
        if( Pelm.Map.Tile.ani_id[ layer_id ] )
        {
            Pelm.Util.clearTimer( Pelm.Map.Tile.ani_id[ layer_id ] );
        }

        //Pelm.Map.Tile.pos[ layer_id ] = 0;
    }
    else
    {
        if( Pelm.Map.Tile.ani_id.all )
        {
            Pelm.Util.clearTimer( Pelm.Map.Tile.ani_id.all );
        }

        //Pelm.Map.Tile.pos.all = 0;
    }

    if( Pelm.Map.Tile.chk_id )
    {
        Pelm.Util.clearTimer( Pelm.Map.Tile.chk_id );
    }

    if( Pelm.Map.Tile.ready_id )
    {
        Pelm.Util.clearTimer( Pelm.Map.Tile.ready_id );
    }

    if( Pelm.Map.Tile.layer_list_active.length > 0 )
    {
        Pelm.Map.Tile.toggleButton( false, Pelm.Map.Tile.elem_ids.start, Pelm.Map.Tile.html.start_on );
    }
    Pelm.Map.Tile.toggleButton( true, Pelm.Map.Tile.elem_ids.stop, Pelm.Map.Tile.html.stop_off );

    Pelm.Util.clearAllTimers();

    return;
};

/**
 * @description
 * Finds a VE layer by title.
 */
Pelm.Map.Tile.findLayer = function( title )
{
    Pelm.Console.info( 'Pelm.Map.Tile.findLayer: ' + title );

    var layer = null;
    for( var i = 0; i < Pelm.Map.VE.GetTileLayerCount(); i++ )
    {
        layer = Pelm.Map.VE.GetTileLayerByIndex( i );
        Pelm.Console.debug( layer );

        if( layer.ID == title )
        {
            break;
        }
    }

    return layer;
};

/**
 * @description
 * Toggles a VE tile layer.
 */
Pelm.Map.Tile.toggleLayer = function( elem )
{
    Pelm.Console.info( 'Pelm.Map.Tile.toggleLayer: ' + elem.value );
    Pelm.Map.updateStatusBar( 'left', 'Toggled ' + elem.value + ' layer' );

    var quad_id, lid, all_checked;

    // toggle layer
    var layer_id = elem.value;

    // TODO: hide layers if radio button is used
    if( elem.type == 'radio')
    {
        for( var k = 0; k < Pelm.Map.Tile.layer_list.length; k++ )
        {
            lid = Pelm.Map.Tile.layer_list[ k ];

            if( lid != layer_id )
            {
                for( quad_id in Pelm.Map.Tile.tile_cache[ lid ] )
                {
                    Pelm.Map.Tile.quad_elems[ lid ][ quad_id ].style.display = 'none';
                }
            }
        }
    }

    if( elem.checked === true )
    {
        // fetch tiles if layer isn't cached yet
        if( !Pelm.Map.Tile.tile_cache[ layer_id ] )
        {
            Pelm.Console.warn( 'Layer not cached yet!' );

            // TODO: over a slower connection, fetchTiles doesn't seem to work
            //Pelm.Map.Tile.addLayer( layer_id, RS.layers[ layer_id ].timesteps[ last_timestepid ], opacity, zindex, visible );
            //Pelm.Map.Tile.fetchTiles( layer_id );

            // TODO: replace this TEMPORARY solution
            refreshMap();
        }
        else
        {
            // show layer
            for( quad_id in Pelm.Map.Tile.tile_cache[ layer_id ] )
            {
                Pelm.Map.Tile.quad_elems[ layer_id ][ quad_id ].style.display = 'block';
            }
        }

        // update opacity settings
        Pelm.Map.Tile.updateOpacity( layer_id, document.getElementById( Pelm.Map.Tile.elem_ids.opacity_prefix + layer_id ).value );
    }
    else
    {
        // hide layer
        if( Pelm.Map.Tile.tile_cache[ layer_id ] )
        {
            for( quad_id in Pelm.Map.Tile.tile_cache[ layer_id ] )
            {
                Pelm.Map.Tile.quad_elems[ layer_id ][ quad_id ].style.display = 'none';
            }
        }
    }

    // update list of active layers
    Pelm.Map.Tile.layer_list_active = [];
    for( var j = 0; j < Pelm.Map.Tile.layer_list.length; j++ )
    {
        lid = Pelm.Map.Tile.layer_list[ j ];

        if( document.getElementById( Pelm.Map.Tile.elem_ids.toggle_prefix + lid ).checked === true )
        {
            Pelm.Map.Tile.layer_list_active.push( lid );
        }
    }

    // update master time frame meter
    if( Pelm.Map.Tile.layer_list_active.length === 0 )
    {
        Pelm.Map.updateStatusBar( 'center', '' );

        Pelm.Map.Tile.toggleButtonsAll( 'off' );

        // stop loop
        Pelm.Map.Tile.stopLoop( 'all', 'auto' );
    }
    else
    {
        Pelm.Map.Tile.toggleButtonsAll( 'on' );

        if( Pelm.Map.Tile.timestep_slider )
        {
            Pelm.Map.Tile.timestep_slider.f_setValue( 0 );
        }
    }

    // check if all checkboxes are selected
    all_checked = true;
    for( var m = 0; m < Pelm.Map.Tile.layer_list.length; m++ )
    {
        lid = Pelm.Map.Tile.layer_list[ m ];

        if( document.getElementById( Pelm.Map.Tile.elem_ids.toggle_prefix + lid ).checked === false )
        {
            all_checked = false;
            break;
        }
    }
};

/**
 * @description
 * Formats a timestamp.
 *
 * e.g., 200808191900 -> Tuesday, August 19, 2008 3:00:00 PM
 */
Pelm.Map.Tile.formatTimestamp = function( ts )
{
    //Pelm.Console.info( 'Pelm.Map.Tile.formatTimestamp: ' + ts );

    if( ts && ts.length == 12 )
    {
        var yyyy = parseInt( ts.substr( 0, 4 ), 10 );
        var mm = parseInt( ts.substr( 4, 2 ), 10 );
        var dd = parseInt( ts.substr( 6, 2 ), 10 );
        var hh = parseInt( ts.substr( 8, 2 ), 10 );
        var ii = parseInt( ts.substr( 10, 2 ), 10 );
        var ss = 0;

        // convert string format to UTC date object
        var tsDate = new Date( Date.UTC( yyyy, ( mm - 1 ), dd, hh, ii, ss ) );

        // extract parts
        var year = tsDate.getFullYear();
        var monthName = Pelm.Date.month[ Pelm.lang ][ tsDate.getMonth() ];
        var dayName = Pelm.Date.weekday[ Pelm.lang ][ tsDate.getDay() ];
        var day = tsDate.getDate();

        var hour;
        if( tsDate.getHours() === 0 )
        {
            hour = ( Pelm.lang == 'fr' ) ? 0 : 12;
        }
        else if( tsDate.getHours() > 12 )
        {
            hour = ( Pelm.lang == 'fr' ) ? tsDate.getHours() : tsDate.getHours() - 12;
        }
        else
        {
            hour = tsDate.getHours();
        }

        var minute = ( tsDate.getMinutes() < 10 ) ? '0' + tsDate.getMinutes() : tsDate.getMinutes();
        var second = ( tsDate.getSeconds() < 10 ) ? '0' + tsDate.getSeconds() : tsDate.getSeconds();
        var ampm = ( tsDate.getHours() < 12 ) ? 'AM' : 'PM';

        // get abbreviated time zone
        var tz = Pelm.Date.getAbbrevTimeZone( tsDate, year );

        var fmtDate;
/*
        if( Pelm.lang == 'fr' )
        {
            fmtDate  = Pelm.Map.Tile.html.date_start + day + ' ' + monthName + ' ' + year+ Pelm.Map.Tile.html.date_end;
            fmtDate += Pelm.Map.Tile.html.time_start + hour + ':' + minute + ' (' + tz + ')' + Pelm.Map.Tile.html.time_end;
        }
        else
        {
            fmtDate  = Pelm.Map.Tile.html.date_start + dayName + ' ' + monthName + ' ' + day + ' ' + year + Pelm.Map.Tile.html.date_end;
            fmtDate += Pelm.Map.Tile.html.time_start + hour + ':' + minute + ' ' + ampm + ' (' + tz + ')' + Pelm.Map.Tile.html.time_end;
        }
*/
        if( Pelm.lang == 'fr' )
        {
            fmtDate = Pelm.Map.Tile.html.date_start + dayName + ' ' + day + ' ' + monthName + Pelm.Map.Tile.html.date_end + Pelm.Map.Tile.html.time_start + hour + ':' + minute + ' ' + tz + Pelm.Map.Tile.html.time_end;
        }
        else
        {
            fmtDate = Pelm.Map.Tile.html.date_start + dayName + ', ' + monthName + ' ' + day + Pelm.Map.Tile.html.date_end + Pelm.Map.Tile.html.time_start + hour + ':' + minute + ' ' + ampm + ' ' + tz + Pelm.Map.Tile.html.time_end;
        }

        return fmtDate;

        //return tsDate.toLocaleString();
        //return tsDate.toLocaleTimeString();
    }
    else
    {
        return ts;
    }
};

/**
 * @description
 * Show time frame by index value.
 */
Pelm.Map.Tile.showTimeFrameByIndex = function( arg )
{
    //Pelm.Console.info( 'Pelm.Map.Tile.showTimeFrameByIndex: ' + arg );

    Pelm.Map.Tile.showTimeFrame( 'all', Pelm.Map.Tile.time_elems_all[ arg ] );

    if( arg >= Pelm.Map.Tile.num_past_timesteps )
    {
        showIndicator( 'nowcast_indicator' ); // TODO
    }
    else
    {
        hideIndicator( 'nowcast_indicator' ); // TODO
    }

    return;
};

/**
 * @description
 * Show time frame by time step ID.
 */
Pelm.Map.Tile.showTimeFrame = function( layer_id, timestep_id )
{
    //Pelm.Console.info( 'Pelm.Map.Tile.showTimeFrame: ' + layer_id + ', ' + timestep_id );

    var lid;
    var quad_id;
    var pos;

    if( layer_id == 'all' )
    {
        for( var i = 0; i < Pelm.Map.Tile.layer_list_active.length; i++ )
        {
            lid = Pelm.Map.Tile.layer_list_active[ i ];

            // load tiles
            for( quad_id in Pelm.Map.Tile.tile_cache[ lid ] )
            {
                if( IE6 )
                {
                    Pelm.Map.Tile.quad_elems[ lid ][ quad_id ].filters[ 'DXImageTransform.Microsoft.AlphaImageLoader' ].src = Pelm.Map.Tile.tile_cache[ lid ][ quad_id ][ timestep_id ].src;
                }
                else
                {
                    Pelm.Map.Tile.quad_elems[ lid ][ quad_id ].src = Pelm.Map.Tile.tile_cache[ lid ][ quad_id ][ timestep_id ].src;
                }
            }

            // update master time frame meter
            for( var k = 0; k < Pelm.Map.Tile.time_elems_all.length; k++ )
            {
                if( Pelm.Map.Tile.time_elems_all[ k ] == timestep_id )
                {
                    pos = k;
                    Pelm.Map.Tile.pos.all = k;
                    break;
                }
            }

            //Pelm.Console.debug( 'lid=' + lid + ', timestep_id=' + timestep_id + ', pos=' + pos );
            if( Pelm.Map.Tile.timestep_slider )
            {
                Pelm.Map.Tile.timestep_slider.f_setValue( pos );
            }
        }

        Pelm.Map.updateStatusBar( 'left', pos );
        Pelm.Map.updateStatusBar( 'right', Pelm.Map.Tile.formatTimestamp( timestep_id ) );
    }
    else
    {
        // load tiles
        for( quad_id in Pelm.Map.Tile.tile_cache[ layer_id ] )
        {
            if( IE6 )
            {
                Pelm.Map.Tile.quad_elems[ layer_id ][ quad_id ].filters[ 'DXImageTransform.Microsoft.AlphaImageLoader' ].src = Pelm.Map.Tile.tile_cache[ layer_id ][ quad_id ][ timestep_id ].src;
            }
            else
            {
                Pelm.Map.Tile.quad_elems[ layer_id ][ quad_id ].src = Pelm.Map.Tile.tile_cache[ layer_id ][ quad_id ][ timestep_id ].src;
            }
        }
    }

    // stop loop
    if( Pelm.Map.Tile.animating === true )
    {
        Pelm.Map.Tile.stopLoop( 'all', 'auto' );
    }

    //Pelm.Console.debug( 'pos=' + Pelm.Map.Tile.pos.all );

    return;
};

/**
 * @description
 * Hides the layer spinner.
 */
Pelm.Map.Tile.hideSpinner = function( layer_id )
{
    var checkbox_id = Pelm.Map.Tile.reverse_map[ layer_id ];

    Pelm.Console.debug(Pelm.Map.Tile.reverse_map);
    Pelm.Console.info( 'Pelm.Map.Tile.hideSpinner: ' + layer_id + ' [' + checkbox_id + ']' );

    //if( document.getElementById( Pelm.Map.Tile.elem_ids.toggle_prefix + checkbox_id ).checked === true )
    //{
        if( document.getElementById( Pelm.Map.Tile.elem_ids.spinner_prefix + checkbox_id ) )
        {
            document.getElementById( Pelm.Map.Tile.elem_ids.toggle_prefix + checkbox_id ).style.display = 'inline';
            document.getElementById( Pelm.Map.Tile.elem_ids.spinner_prefix + checkbox_id ).style.display = 'none';
        }
    //}

    return;
};

/**
 * @description
 * Shows the layer spinner.
 */
Pelm.Map.Tile.showSpinner = function( layer_id )
{
    var checkbox_id = Pelm.Map.Tile.reverse_map[ layer_id ];

    Pelm.Console.info( 'Pelm.Map.Tile.showSpinner: ' + layer_id + ' [' + checkbox_id + ']' );

    //if( document.getElementById( Pelm.Map.Tile.elem_ids.toggle_prefix + checkbox_id ).checked === true )
    //{
        if( document.getElementById( Pelm.Map.Tile.elem_ids.spinner_prefix + checkbox_id ) )
        {
            document.getElementById( Pelm.Map.Tile.elem_ids.toggle_prefix + checkbox_id ).style.display = 'none';
            document.getElementById( Pelm.Map.Tile.elem_ids.spinner_prefix + checkbox_id ).style.display = 'inline';
        }
    //}

    return;
};

/**
 * @description
 * Checks when all the images in the tile cache are loaded.
 */
Pelm.Map.Tile.checkImages = function( layer_id )
{
    var checkbox_id = Pelm.Map.Tile.reverse_map[ layer_id ];
    Pelm.Console.info( 'Pelm.Map.Tile.checkImages: ' + layer_id + ' (' + checkbox_id + ')' );

    if( Pelm.Map.Tile.img_id[ layer_id ] )
    {
        Pelm.Util.clearTimer( Pelm.Map.Tile.img_id[ layer_id ] );
    }

    if( !Pelm.Map.Tile.timer[ layer_id ] )
    {
        Pelm.Map.Tile.timer[ layer_id ] = {};
        Pelm.Map.Tile.timer[ layer_id ].startTime = new Date();
    }

    if( Pelm.Map.Tile.timer[ layer_id ] )
    {
        Pelm.Map.Tile.timer[ layer_id ].stopTime = new Date();
    }

    Pelm.Map.Tile.img_check_cnt++;

    var timestep_id;
    var real_timestep_id;
    var loaded = false;
    var total = 0;
    var count = 0;

    Pelm.Map.Tile.bytes[ layer_id ] = 0;

    for( var quad_id in Pelm.Map.Tile.tile_cache[ layer_id ] )
    {
        for( var i = 0; i < Pelm.Map.Tile.time_elems_all.length; i++ )
        {
            total++;

            timestep_id = Pelm.Map.Tile.time_elems_all[ i ];
            real_timestep_id = Pelm.Map.Tile.time_elems[ layer_id ][ i ];
            //Pelm.Console.debug( timestep_id + ' : ' + real_timestep_id );

            if( Pelm.Map.Tile.tile_cache[ layer_id ][ quad_id ][ timestep_id ].fileSize !== undefined )
            {
                Pelm.Map.Tile.bytes[ layer_id ] += parseInt( Pelm.Map.Tile.tile_cache[ layer_id ][ quad_id ][ timestep_id ].fileSize, 10 );
            }

            loaded = Pelm.Util.isImageLoaded( Pelm.Map.Tile.tile_cache[ layer_id ][ quad_id ][ timestep_id ] );

            if( loaded === true )
            {
                count++;
            }
            else
            {
                //break;
                //Pelm.Console.debug( '[' + layer_id + '][' + quad_id + '][' + timestep_id + ']' );
            }
        }
    }

    Pelm.Console.debug( 'Pelm.Map.Tile.tile_cache:' );
    Pelm.Console.debug( Pelm.Map.Tile.tile_cache );
    Pelm.Console.debug( layer_id + ': ' + count + ' / ' + total );

    if( total > 0 )
    {
        var percentage = Math.floor( count / total * 100 );

        Pelm.Map.Tile.img_loaded[ layer_id ] = percentage;

        var elapsed = ( Pelm.Map.Tile.timer[ layer_id ].stopTime - Pelm.Map.Tile.timer[ layer_id ].startTime ) / 1000;
        var elapsed_num = Math.round( elapsed * Math.pow( 10, 1 ) ) / Math.pow( 10, 1 );
        //Pelm.Console.debug( Pelm.Map.Tile.timer[ layer_id ].stopTime + ' - ' + Pelm.Map.Tile.timer[ layer_id ].startTime + ' = ' + elapsed + ' -> ' + elapsed_num );

        var filesize = Math.round( Pelm.Map.Tile.bytes[ layer_id ] / 1024 );
        var filesize_html = ( filesize > 0 ) ? filesize : '~' + ( Pelm.Map.Tile.average_tile_size_bytes[ layer_id ] * count / 1024 );

        var rate = 0;
        if( Pelm.Map.Tile.bytes[ layer_id ] > 0 && elapsed > 0 )
        {
            rate = Math.round( ( Pelm.Map.Tile.bytes[ layer_id ] / elapsed ) / 1024 );
        }
        // assume an average layer size for non-IE browsers that don't support the fileSize property on an image object
        else if( elapsed > 0 )
        {
            Pelm.Console.debug( 'Using an average tile size of ' + Pelm.Map.Tile.average_tile_size_bytes[ layer_id ] + ' for "' + layer_id + '"' );
            rate = Math.round( ( Pelm.Map.Tile.average_tile_size_bytes[ layer_id ] * count / elapsed ) / 1024 );
        }
        //Pelm.Console.debug( Pelm.Map.Tile.bytes[ layer_id ] + ' / ' + elapsed + ' / 1024 = ' + rate );

        // adjust settings for detected bandwidth
        var rate_type;
        var connection_type;
        var rate_html;

        // cached or unknown
        if( rate === 0 )
        {
            rate_type = ( filesize > 0 ) ? 'Cached' : 'N/A';
            connection_type = ( filesize > 0 ) ? 'Cached' : 'Unknown';
            rate_html = '&mdash;';
        }
        else
        {
            rate_type = rate;
            rate_html = rate + ' KB/sec.';

            // 56 Kbps dial-up
            if( rate > 0 && rate < 7 )
            {
                connection_type = 'Dial-up';
                //Pelm.Map.Tile.speed_slider.f_setValue( 250 );
                //Pelm.Map.Tile.interval_slider.f_setValue( 60 );
                //Pelm.Map.Tile.duration_slider.f_setValue( 1 ); // 1 time step (static map)
            }
            // 128 Kbps ISDN
            else if( rate >= 7 && rate < 32 )
            {
                connection_type = 'ISDN';
                //Pelm.Map.Tile.speed_slider.f_setValue( 250 );
                //Pelm.Map.Tile.interval_slider.f_setValue( 60 );
                //Pelm.Map.Tile.duration_slider.f_setValue( 6 ); // 6 time steps
            }
            // 500 Kbps
            else if( rate >= 32 && rate < 62.5 )
            {
                connection_type = 'High-speed (500 Kbps)';
                //Pelm.Map.Tile.speed_slider.f_setValue( 250 );
                //Pelm.Map.Tile.interval_slider.f_setValue( 60 );
                //Pelm.Map.Tile.duration_slider.f_setValue( 12 ); // 12 time steps
            }
            // 1 Mbps
            else if( rate >= 62.5 && rate < 125 )
            {
                connection_type = 'High-speed (1 Mbps)';
                //Pelm.Map.Tile.speed_slider.f_setValue( 250 );
                //Pelm.Map.Tile.interval_slider.f_setValue( 60 );
                //Pelm.Map.Tile.duration_slider.f_setValue( 18 ); // 18 time steps
            }
            // 7 Mbps
            else if( rate >= 125 && rate < 875 )
            {
                connection_type = 'High-speed (7 Mbps)';
                //Pelm.Map.Tile.speed_slider.f_setValue( 100 );
                //Pelm.Map.Tile.interval_slider.f_setValue( 60 );
                //Pelm.Map.Tile.duration_slider.f_setValue( 24 ); // 24 time steps
            }
            // 10 Mbps
            else if( rate >= 875 && rate < 1250 )
            {
                connection_type = 'High-speed (10 Mbps)';
                //Pelm.Map.Tile.speed_slider.f_setValue( 100 );
                //Pelm.Map.Tile.interval_slider.f_setValue( 10 );
                //Pelm.Map.Tile.duration_slider.f_setValue( 6 ); // 36 time steps
            }
            // LAN
            else
            {
                connection_type = 'LAN';
                //Pelm.Map.Tile.speed_slider.f_setValue( 50 );
                //Pelm.Map.Tile.interval_slider.f_setValue( 10 );
                //Pelm.Map.Tile.duration_slider.f_setValue( 8 ); // 48 time steps
            }
        }

        // update layer progress bar
        if( document.getElementById( 'load_percent_' + checkbox_id ) )
        {
            document.getElementById( 'load_percent_' + checkbox_id ).style.width = percentage + '%';
        }

        // update table stats
        Pelm.Util.setInnerHTML( 'load_percent_' + checkbox_id, percentage + '%' );
        Pelm.Util.setInnerHTML( 'load_tiles_num_' + checkbox_id, count );
        Pelm.Util.setInnerHTML( 'load_secs_' + checkbox_id, elapsed_num );
        Pelm.Util.setInnerHTML( 'load_bytes_' + checkbox_id, filesize_html );
        Pelm.Util.setInnerHTML( 'load_type_' + checkbox_id, rate_type );

        if( percentage >= 100 )
        {
            Pelm.Console.debug( 'Hiding spinner ' + layer_id );
            Pelm.Map.Tile.hideSpinner( layer_id );
            Pelm.Map.Tile.img_check_cnt = 0;

            Pelm.Util.setInnerHTML( Pelm.Map.Tile.elem_ids.connection_type, connection_type );

            // show the layer
            for( quad_id in Pelm.Map.Tile.tile_cache[ layer_id ] )
            {
                Pelm.Map.Tile.quad_elems[ layer_id ][ quad_id ].style.display = 'block';
            }
        }
        else
        {
            // TODO: how long do we keep checking for? 1 min = 120 * 0.5s
            if( Pelm.Map.Tile.img_check_cnt > 60 )
            {
                Pelm.Console.warn( 'Hide spinner!' );

                Pelm.Map.Tile.img_check_cnt = 0;
                Pelm.Console.debug( 'Hiding spinner ' + layer_id );
                //Pelm.Map.Tile.hideSpinner( layer_id );
                Pelm.Map.Tile.toggleButtonsAll( 'on' );
                Pelm.Map.hideLoading();
            }
            else
            {
                var func = 'Pelm.Map.Tile.checkImages( "' + layer_id + '" )';
                Pelm.Map.Tile.img_id[ layer_id ] = Pelm.Util.setTimer( func, 500 );
            }
        }
    }
    else if( total === 0 )
    {
        Pelm.Console.debug( 'Hiding spinner ' + layer_id );
        //Pelm.Map.Tile.hideSpinner( layer_id );

        Pelm.Util.setInnerHTML( 'load_percent_' + checkbox_id, '&mdash;' );
        Pelm.Util.setInnerHTML( 'load_tiles_num_' + checkbox_id, '&mdash;' );
        Pelm.Util.setInnerHTML( 'load_secs_' + checkbox_id, '&mdash;' );
        Pelm.Util.setInnerHTML( 'load_bytes_' + checkbox_id, '&mdash;' );
        Pelm.Util.setInnerHTML( 'load_type_' + checkbox_id, '&mdash;' );

        Pelm.Map.Tile.img_check_cnt = 0;
    }

    //Pelm.Console.debug( Pelm.Map.Tile.img_loaded )
    return;
};

/**
 * @description
 * Update the layer's opacity.
 */
Pelm.Map.Tile.updateOpacity = function( layer_id, val )
{
    //Pelm.Console.info( 'Pelm.Map.Tile.updateOpacity: ' + layer_id + ', ' + val );

    var ie_val;

    for( var quad_id in Pelm.Map.Tile.quad_elems[ layer_id ] )
    {
        if( navigator.appName == 'Microsoft Internet Explorer' )
        {
            ie_val = Math.ceil( val / 1.5 * 100 );

            Pelm.Map.Tile.quad_elems[ layer_id ][ quad_id ].style.filter = 'alpha(opacity=' + ie_val + ')';
        }
        else if( navigator.appName == 'Netscape' )
        {
            Pelm.Map.Tile.quad_elems[ layer_id ][ quad_id ].style.opacity = val;
        }
        else
        {
            // TODO: support other browsers like Opera?
        }
    }
};

//----------------------------------------------------------------------------

/**
 * @description
 * Convert degrees to radians.
 */
Pelm.Map.Tile.convertDegToRad = function( d )
{
    //Pelm.Console.info( 'convertDegToRad' );

    return d * Math.PI / 180.0;
};

/**
 * @description
 * Calculate meters per pixel.
 */
Pelm.Map.Tile.calcMetersPerPixel = function( zoom )
{
    //Pelm.Console.info( 'calcMetersPerPixel:' );

    var arc = Pelm.Map.Tile.earth_circum / ( ( 1 << zoom ) * Pelm.Map.Tile.tile_size );
    return arc;
};

/**
 * @description
 * Convert the latitude to a Y-axis pixel coordinate.
 */
Pelm.Map.Tile.convertLatitudeToYAtZoom = function( lat, zoom )
{
    //Pelm.Console.info( 'convertLatitudeToYAtZoom:' );

    var arc = Pelm.Map.Tile.calcMetersPerPixel( zoom );
    var sinLat = Math.sin( Pelm.Map.Tile.convertDegToRad( lat ) );
    var metersY = Pelm.Map.Tile.earth_radius / 2 * Math.log( ( 1 + sinLat ) / ( 1 - sinLat ) );
    var y = Math.round( ( Pelm.Map.Tile.earth_half_circum - metersY ) / arc );
    return y;
};

/**
 * @description
 * Convert the longitude to an X-axis pixel coordinate.
 */
Pelm.Map.Tile.convertLongitudeToXAtZoom = function( lon, zoom )
{
    //Pelm.Console.info( 'convertLongitudeToXAtZoom:' );

    var arc = Pelm.Map.Tile.calcMetersPerPixel( zoom );
    var metersX = Pelm.Map.Tile.earth_radius * Pelm.Map.Tile.convertDegToRad( lon );
    var x = Math.round( ( Pelm.Map.Tile.earth_half_circum + metersX ) / arc );
    return x;
};

/**
 * @description
 * Convert a tile location to its quad key.
 */
Pelm.Map.Tile.convertTileToQuadKey = function( tx, ty, zl )
{
    //Pelm.Console.info( 'convertTileToQuadKey:' );

    var quad = '';
    var mask;
    var cell;

    for( var i = zl; i > 0; i-- )
    {
        mask = 1 << ( i - 1 );
        cell = 0;
        if( ( tx & mask ) !== 0 )
        {
            cell++;
        }

        if( ( ty & mask ) !== 0 )
        {
            cell += 2;
        }

        quad += cell;
    }

    return quad;
};

/**
 * @description
 * Calculate the number of quads for a specified layer's coverage area.
 */
Pelm.Map.Tile.calcNumQuads = function( layer_id, topLeftLat, topLeftLon, bottomRightLat, bottomRightLon, zoom )
{
    Pelm.Console.info( 'calcNumQuads:' );
/*
    topLeftLat = 43.65334172566217;
    topLeftLon = -79.41544532775879;
    bottomRightLat = 43.63160242951277;
    bottomRightLon = -79.35879707336427;
*/
    var meterTopLeftY = Pelm.Map.Tile.convertLatitudeToYAtZoom( topLeftLat, zoom );
    var meterTopLeftX = Pelm.Map.Tile.convertLongitudeToXAtZoom( topLeftLon, zoom );
    var meterBottomRightY = Pelm.Map.Tile.convertLatitudeToYAtZoom( bottomRightLat, zoom );
    var meterBottomRightX = Pelm.Map.Tile.convertLongitudeToXAtZoom( bottomRightLon, zoom );

    //

    var x_pts = {};
    x_pts[ meterTopLeftX ] = '';
    for( var x = meterTopLeftX; x <= meterBottomRightX; x = x + Pelm.Map.Tile.tile_size )
    {
        x_pts[ x ] = '';
    }
    x_pts[ meterBottomRightX ] = '';

    var y_pts = {};
    y_pts[ meterTopLeftY ] = '';
    for( var y = meterTopLeftY; y <= meterBottomRightY; y = y + Pelm.Map.Tile.tile_size )
    {
        y_pts[ y ] = '';
    }
    y_pts[ meterBottomRightY ] = '';

    var col, row, quad;
    var quad_ids = {};

    for( var xp in x_pts )
    {
        for( var yp in y_pts )
        {
            col = xp / Pelm.Map.Tile.tile_size;
            row = yp / Pelm.Map.Tile.tile_size;
            quad = Pelm.Map.Tile.convertTileToQuadKey( col, row, zoom );
//            Pelm.Console.debug( xp + ', ' + yp + ', ' + quad );

            quad_ids[ quad ] = '';
        }
    }
    //Pelm.Console.debug( quad_ids );

    // Get number of quads
    Pelm.Map.Tile.quad_ids[ layer_id ] = [];

    var quad_cnt = 0;
    for( var h in quad_ids )
    {
        quad_cnt++;
        Pelm.Map.Tile.quad_ids[ layer_id ].push( h );
    }

    // Find the intersecting quads between the bounding box and the layer's coverage area

    if( layer_id != Pelm.Map.Tile.elem_ids.bbox )
    {
        Pelm.Map.Tile.quad_ids[ Pelm.Map.Tile.elem_ids.bbox + '_' + layer_id ] = [];

        for( var i = 0; i < Pelm.Map.Tile.quad_ids[ Pelm.Map.Tile.elem_ids.bbox ].length; i++ )
        {
            for( var j = 0; j < Pelm.Map.Tile.quad_ids[ layer_id ].length; j++ )
            {
                if( Pelm.Map.Tile.quad_ids[ Pelm.Map.Tile.elem_ids.bbox ][ i ] == Pelm.Map.Tile.quad_ids[ layer_id ][ j ] )
                {
                    Pelm.Map.Tile.quad_ids[ Pelm.Map.Tile.elem_ids.bbox + '_' + layer_id ].push( Pelm.Map.Tile.quad_ids[ Pelm.Map.Tile.elem_ids.bbox ][ i ] );
                }
            }
        }

        return Pelm.Map.Tile.quad_ids[ Pelm.Map.Tile.elem_ids.bbox + '_' + layer_id ].length;
    }
    else
    {
        return quad_cnt;
    }

};

/**
 * @description
 * Checks if we have tile coverage for the current view.
 */
Pelm.Map.Tile.checkCoverage = function( topLeftLat, topLeftLon, bottomRightLat, bottomRightLon, zoom )
{
    Pelm.Console.info( 'checkCoverage: topLeftLat=' + topLeftLat + ', topLeftLon=' + topLeftLon + '; bottomRightLat=' + bottomRightLat + ', bottomRightLon=' + bottomRightLon );

    var topRightLat = topLeftLat;
    var topRightLon = bottomRightLon;
    var bottomLeftLat = bottomRightLat;
    var bottomLeftLon = topLeftLon;

    var cnt;
    var parts;
    var layer_id;

    // calculate number of quads
    Pelm.Map.Tile.quad_cnt = Pelm.Map.Tile.calcNumQuads( 'bbox', topLeftLat, topLeftLon, bottomRightLat, bottomRightLon, zoom );

    // update number of quads in the legend
    Pelm.Util.setInnerHTML( Pelm.Map.Tile.elem_ids.quad_num, Pelm.Map.Tile.quad_cnt );

    //
    var layer_coverage, num;
    for( var h = 0; h < Pelm.Map.Tile.layer_list.length; h++ )
    {
        parts = Pelm.Map.Tile.layer_list[ h ].split( "_" );
        layer_id = Pelm.Map.Tile.getLayerID( Pelm.Map.Tile.layer_list[ h ] );

        if( Pelm.Map.Tile.coverage[ layer_id ] )
        {
            layer_coverage = Pelm.Map.Tile.coverage[ layer_id ];

            num = Pelm.Map.Tile.calcNumQuads( layer_id, layer_coverage.TopLeft.lat, layer_coverage.TopLeft.lon, layer_coverage.BottomRight.lat, layer_coverage.BottomRight.lon, zoom );
            Pelm.Util.setInnerHTML( 'quads_' + layer_id, num );
        }
    }

    //
    Pelm.Console.warn( Pelm.Map.Tile.coverage );

    for( var i = 0; i < Pelm.Map.Tile.layer_list.length; i++ )
    {
        layer_id = Pelm.Map.Tile.getLayerID( Pelm.Map.Tile.layer_list[ i ] );
        Pelm.Console.warn( Pelm.Map.Tile.layer_list[ i ] + ' -> ' + layer_id );

        cnt = 0;

        // check if coverage area is inside the bounding box

        if( Pelm.Map.Tile.coverage[ layer_id ] )
        {
            // top left
            if( topLeftLat >= Pelm.Map.Tile.coverage[ layer_id ].TopLeft.lat && Pelm.Map.Tile.coverage[ layer_id ].TopLeft.lat >= bottomLeftLat && topLeftLon <= Pelm.Map.Tile.coverage[ layer_id ].TopLeft.lon && Pelm.Map.Tile.coverage[ layer_id ].TopLeft.lon <= topRightLon )
            {
                cnt++;
            }

            // top right
            if( topRightLat >= Pelm.Map.Tile.coverage[ layer_id ].TopRight.lat && Pelm.Map.Tile.coverage[ layer_id ].TopRight.lat >= bottomRightLat && topLeftLon <= Pelm.Map.Tile.coverage[ layer_id ].TopRight.lon && Pelm.Map.Tile.coverage[ layer_id ].TopRight.lon <= topRightLon )
            {
                cnt++;
            }

            // bottom right
            if( topRightLat >= Pelm.Map.Tile.coverage[ layer_id ].BottomRight.lat && Pelm.Map.Tile.coverage[ layer_id ].BottomRight.lat >= bottomRightLat && bottomLeftLon <= Pelm.Map.Tile.coverage[ layer_id ].BottomRight.lon && Pelm.Map.Tile.coverage[ layer_id ].BottomRight.lon <= bottomRightLon )
            {
                cnt++;
            }

            // bottom left
            if( topLeftLat >= Pelm.Map.Tile.coverage[ layer_id ].BottomLeft.lat && Pelm.Map.Tile.coverage[ layer_id ].BottomLeft.lat >= bottomLeftLat && bottomLeftLon <= Pelm.Map.Tile.coverage[ layer_id ].BottomLeft.lon && Pelm.Map.Tile.coverage[ layer_id ].BottomLeft.lon <= bottomRightLon )
            {
                cnt++;
            }

            // check if the bounding box is inside the coverage area
            if( cnt === 0 )
            {
                // top left
                if( Pelm.Map.Tile.coverage[ layer_id ].TopLeft.lat >= topLeftLat && topLeftLat >= Pelm.Map.Tile.coverage[ layer_id ].BottomLeft.lat && Pelm.Map.Tile.coverage[ layer_id ].TopLeft.lon <= topLeftLon && topLeftLon <= Pelm.Map.Tile.coverage[ layer_id ].TopRight.lon )
                {
                    cnt++;
                }

                // top right
                if( Pelm.Map.Tile.coverage[ layer_id ].TopRight.lat >= topRightLat && topRightLat >= Pelm.Map.Tile.coverage[ layer_id ].BottomRight.lat && Pelm.Map.Tile.coverage[ layer_id ].TopLeft.lon <= topRightLon && topRightLon <= Pelm.Map.Tile.coverage[ layer_id ].TopRight.lon )
                {
                    cnt++;
                }

                // bottom right
                if( Pelm.Map.Tile.coverage[ layer_id ].TopRight.lat >= bottomRightLat && bottomRightLat >= Pelm.Map.Tile.coverage[ layer_id ].BottomRight.lat && Pelm.Map.Tile.coverage[ layer_id ].BottomLeft.lon <= bottomRightLon && bottomRightLon <= Pelm.Map.Tile.coverage[ layer_id ].BottomRight.lon )
                {
                    cnt++;
                }

                // bottom left
                if( Pelm.Map.Tile.coverage[ layer_id ].TopLeft.lat >= bottomLeftLat && bottomLeftLat >= Pelm.Map.Tile.coverage[ layer_id ].BottomLeft.lat && Pelm.Map.Tile.coverage[ layer_id ].BottomLeft.lon <= bottomLeftLon && bottomLeftLon <= Pelm.Map.Tile.coverage[ layer_id ].BottomRight.lon )
                {
                    cnt++;
                }
            }

            Pelm.Console.warn( layer_id + ': ' + cnt + ' points' );

            Pelm.Map.Tile.coverage_points[ layer_id ] = cnt;
        }
        else
        {
            Pelm.Console.warn( 'No coverage data for ' + layer_id );
        }
    }

    Pelm.Console.debug( Pelm.Map.Tile.coverage_points );
};

/**
 * @description
 * Toggle a button's state.
 */
Pelm.Map.Tile.toggleButton = function( elem_disabled_state, elem_id, elem_style )
{
    //Pelm.Console.info( 'toggleButton: elem_disabled_state=' + elem_disabled_state + ', elem_id=' + elem_id + ', elem_style=' + elem_style );

    if( document.getElementById( elem_id ) )
    {
        document.getElementById( elem_id ).disabled = elem_disabled_state;

        if( elem_style !== '' )
        {
            document.getElementById( elem_id ).className = elem_style;
        }
    }
};

/**
 * @description
 * Toggle all buttons.
 */
Pelm.Map.Tile.toggleButtonsAll = function( state )
{
    Pelm.Console.info( 'toggleButtonsAll: ' + state );

    var elem_disabled_state = ( state == 'off' ) ? true : false;

    Pelm.Map.Tile.toggleButton( elem_disabled_state, Pelm.Map.Tile.elem_ids.first, Pelm.Map.Tile.html[ 'first_' + state ] );
    Pelm.Map.Tile.toggleButton( elem_disabled_state, Pelm.Map.Tile.elem_ids.prev, Pelm.Map.Tile.html[ 'prev_' + state ] );

    if( ( state == 'on' && Pelm.Map.Tile.animating === false ) || ( Pelm.Map.VE && Pelm.Map.VE.GetZoomLevel() > Pelm.Map.Tile.max_zoom_level ) )
    {
        Pelm.Map.Tile.toggleButton( elem_disabled_state, Pelm.Map.Tile.elem_ids.start, Pelm.Map.Tile.html[ 'start_' + state ] );
    }

    Pelm.Map.Tile.toggleButton( elem_disabled_state, Pelm.Map.Tile.elem_ids.stop, Pelm.Map.Tile.html[ 'stop_' + state ] );
    Pelm.Map.Tile.toggleButton( elem_disabled_state, Pelm.Map.Tile.elem_ids.next, Pelm.Map.Tile.html[ 'next_' + state ] );
    Pelm.Map.Tile.toggleButton( elem_disabled_state, Pelm.Map.Tile.elem_ids.last, Pelm.Map.Tile.html[ 'last_' + state ] );
};

/**
 * @description
 */
Pelm.Map.Tile.getLayerID = function( layer_str )
{
    //Pelm.Console.info( 'Pelm.Map.Tile.getLayerID: ' + layer_str );

    var layer_id = '';

    var parts = layer_str.split( "_" );

    if( parts[ 0 ] == 'p' || parts[ 0 ] == 'f' )
    {
        layer_id = layer_mapping[ parts[ 0 ] ][ parts[ 1 ] ][ parts[ 2 ] ];
    }
    else
    {
        var subparts = parts[ 0 ].split( "." );

        for( var j = 0; j < subparts.length; j++ )
        {
            if( subparts[ j ] == 'p' || subparts[ j ] == 'f' )
            {
                layer_id = layer_mapping[ subparts[ j ] ][ parts[ 1 ] ][ parts[ 2 ] ];
            }
        }
    }

    return layer_id;
};
