"use strict";
/*jslint onevar: false, white: false, bitwise: false */
/*global document, window */

// TODO: touch/tslippy3.js has much better internal logic - merge!


//----------------------------------------------------------------------
// Enumerated types
//----------------------------------------------------------------------

var MapOptions = {
    SectorGrid           : 0x0001,
    SubsectorGrid        : 0x0002,
    SectorsSelected      : 0x0004,
    SectorsAll           : 0x0008,
    SectorsMask          : 0x000c,
    BordersMajor         : 0x0010,
    BordersMinor         : 0x0020,
    BordersMask          : 0x0030,
    NamesMajor           : 0x0040,
    NamesMinor           : 0x0080,
    NamesMask            : 0x00c0,
    WorldsCapitals       : 0x0100,
    WorldsHomeworlds     : 0x0200,
    RoutesSelected       : 0x0400,
    PrintStyle           : 0x0800,
    CandyStyle           : 0x1000,
    StyleMask            : 0x1800,
    ForceHexes           : 0x2000,
    Mask                 : 0x3fff
};

//----------------------------------------------------------------------
// Astrometric Constants
//----------------------------------------------------------------------

var Astrometrics = {
    ParsecScaleX : Math.cos(Math.PI/6), // cos(30)
    ParsecScaleY : 1.0,
    SectorWidth  : 32,
    SectorHeight : 40,
    ReferenceHexX : 1, // Reference is at Core 0140
    ReferenceHexY : 40,
    TileWidth  : 256,
    TileHeight : 256,
    MinScale : 0.015625,
    MaxScale : 512
};

//----------------------------------------------------------------------
//
// Usage:
//
//   var map = new Map( document.getElementById("YourMapDiv") );
//
//   map.OnScaleChanged   = function() { update scale indicator }
//   map.OnOptionsChanged = function() { update control panel }
//   map.OnDisplayChanged = function() { update permalink }
//   map.OnHover          = function( x, y ) { show data }
//
//   var hx = map.GetHexX();
//   var hy = map.GetHexY();
//   var x = map.GetX();
//   var y = map.GetY();
//   var s = map.GetScale();
//   var o = map.GetOptions();
//
//   map.SetScale( scale, bRefresh );
//   map.SetOptions( flags, bRefresh );
//   map.SetPosition( x, y );
//
//   map.ScaleCenterAtSectorHex( scale, sx, sy, hx, hy );
//   map.CenterAtSectorHex( sx, sy, hx, hy );
//   map.Scroll( dx, dy );
//   map.ZoomIn();
//   map.ZoomOut();
//
//----------------------------------------------------------------------
function Map(mapContainer)
//----------------------------------------------------------------------
{
    // For references to "this" within callbacks and closures
    var self = this;

    // Viewport Descriptions
    var PhysicalCenteredAtX = null;
    var PhysicalCenteredAtY = null;
    var LogicalCenteredAtX = null;
    var LogicalCenteredAtY = null;
    var HexCenteredAtX = null;
    var HexCenteredAtY = null;
    var Scale = 2;
    var Options = MapOptions.SectorGrid | MapOptions.SubsectorGrid | MapOptions.SectorsSelected | MapOptions.BordersMajor | MapOptions.BordersMinor | MapOptions.NamesMajor | MapOptions.WorldsCapitals | MapOptions.WorldsHomeworlds;

    // Tiles
    var TileCache;

    // HTML Elements
    var DragContainer;
    var ScrollPane;

    // Markers and Overlays
    var markers = [];
    var overlays = [];

    //----------------------------------------------------------------------
    // Events
    //----------------------------------------------------------------------

    this.OnScaleChanged = null;
    this.OnOptionsChanged = null;
    this.OnDisplayChanged = null;
    this.OnHover = null;

    //----------------------------------------------------------------------
    // Internal Methods
    //----------------------------------------------------------------------

    //----------------------------------------------------------------------
    function sectorHexToLogical(sx, sy, hx, hy)
    //----------------------------------------------------------------------
    {
        var o = {};

        // Offset from origin
        o.x = (sx * Astrometrics.SectorWidth) + hx - Astrometrics.ReferenceHexX;
        o.y = (sy * Astrometrics.SectorHeight) + hy - Astrometrics.ReferenceHexY;

        // Offset from the "corner" of the hex
        o.x -= 0.5;
        o.y -= ((hx % 2) === 0) ? 0 : 0.5;

        // Scale to non-homogenous coordinates
        o.x *= Astrometrics.ParsecScaleX;
        o.y *= -Astrometrics.ParsecScaleY;

        return o;

    } // sectorHexToLogical


    //----------------------------------------------------------------------
    //
    //----------------------------------------------------------------------
    function makeMarker(marker)
    //----------------------------------------------------------------------
    {
        if (marker === null) {
            return;
        }

        // TODO: Consider preserving existing item        
        if (marker.element && marker.element.parentNode) {
            marker.element.parentNode.removeChild(marker.element);
            marker.element = null;
        }

        // Compute physical location
        var pt = sectorHexToLogical(marker.sx, marker.sy, marker.hx, marker.hy);
        pt.x = pt.x * Scale;
        pt.y = -pt.y * Scale;

        var div;
        div = document.createElement("div");
        div.className = "marker";
        div.id = marker.id;

        div.style.left = pt.x + "px";
        div.style.top = pt.y + "px";
        div.style.zIndex = marker.z;

        marker.element = div;
        ScrollPane.appendChild(div);
    }

    //----------------------------------------------------------------------
    //
    //----------------------------------------------------------------------
    function makeOverlay(overlay)
    //----------------------------------------------------------------------
    {
        if (overlay === null) {
            return;
        }

        // TODO: Consider preserving existing item        
        if (overlay.element && overlay.element.parentNode) {
            overlay.element.parentNode.removeChild(overlay.element);
            overlay.element = null;
        }

        // Compute physical location
        var x = overlay.x, y = overlay.y, w = overlay.w, h = overlay.h;
        x *= Scale;
        y *= -Scale;
        w *= Scale;
        h *= Scale;

        var div;
        div = document.createElement("div");
        div.className = "overlay";
        div.id = overlay.id;

        div.style.left = x + "px";
        div.style.top = y + "px";
        div.style.width = w + "px";
        div.style.height = h + "px";
        div.style.zIndex = overlay.z;

        overlay.element = div;
        ScrollPane.appendChild(div);
    }

    //----------------------------------------------------------------------
    // Flush the tiles. Note that this does not refresh them.
    //----------------------------------------------------------------------
    function clearTileCache()
    //----------------------------------------------------------------------
    {
        TileCache = null;

        var child = ScrollPane.firstChild;
        while (child) {
            var cur = child;
            child = cur.nextSibling;

            if (cur.className === "tile") {
                ScrollPane.removeChild(cur);
            }
        }

        // Reposition markers and overlays
        var i;
        for (i = 0; i < markers.length; i += 1) {
            makeMarker(markers[i]);
        }
        for (i = 0; i < overlays.length; i += 1) {
            makeOverlay(overlays[i]);
        }

    } // clearTileCache

    //----------------------------------------------------------------------
    // Load a tile. This does not check the cache.
    // The tile element is returned.
    //----------------------------------------------------------------------
    function makeTile(x, y)
    //----------------------------------------------------------------------
    {
        var oTile;
        oTile = document.createElement("img");
        oTile.className = "tile";
        oTile.src = "Tile.aspx?x=" + x + "&y=" + y + "&scale=" + Scale + "&options=" + Options;

        oTile.style.position = "absolute";
        oTile.style.left = (Astrometrics.TileWidth * x) + "px";
        oTile.style.top = (Astrometrics.TileHeight * y) + "px";
        oTile.style.width = Astrometrics.TileWidth + "px";
        oTile.style.height = Astrometrics.TileHeight + "px";

        ScrollPane.appendChild(oTile);
        return oTile;

    } // makeTile


    //----------------------------------------------------------------------
    // Load a tile if it isn't already cached
    //----------------------------------------------------------------------
    function checkMakeTile(x, y)
    //----------------------------------------------------------------------
    {
        var key = "tile_" + x + "_" + y;

        if (!TileCache) {
            TileCache = {};
        }

        if (!TileCache[key]) {
            TileCache[key] = makeTile(x, y);
        }

    } // checkMakeTile


    //----------------------------------------------------------------------
    // Based on the current scroll position and scale, make
    // sure the appropriate tiles have been cached
    //----------------------------------------------------------------------
    function updateTiles()
    //----------------------------------------------------------------------
    {
        var tx = Math.round((-ScrollPane.offsetLeft) / Astrometrics.TileWidth);
        var ty = Math.round((-ScrollPane.offsetTop) / Astrometrics.TileHeight);

        var tw = Math.round(DragContainer.offsetWidth / Astrometrics.TileWidth);
        var th = Math.round(DragContainer.offsetHeight / Astrometrics.TileHeight);

        tx -= 1;
        ty -= 1;
        tw += 2;
        th += 2;

        // TODO: Spiral out from center instead, so region of interest
        // loads first!

        var x, y;
        for (x = 0; x < tw; x += 1) {
            for (y = 0; y < th; y += 1) {
                checkMakeTile(x + tx, y + ty);
            }
        }

    } // updateTiles


    //----------------------------------------------------------------------
    // This places the specified physical coordinate (pixel)
    // at the center of the viewport
    //----------------------------------------------------------------------
    function centerAtPhysical(x, y)
    //----------------------------------------------------------------------
    {
        PhysicalCenteredAtX = x;
        PhysicalCenteredAtY = y;

        LogicalCenteredAtX = x / Scale;
        LogicalCenteredAtY = -y / Scale;

        HexCenteredAtX = Math.round((LogicalCenteredAtX / Astrometrics.ParsecScaleX) + 0.5);
        HexCenteredAtY = Math.round((-LogicalCenteredAtY / Astrometrics.ParsecScaleY) + ((HexCenteredAtX % 2 === 0) ? 0.5 : 0));

        if (self.OnDisplayChanged) {
            self.OnDisplayChanged();
        }

        // Center the drag container offset within the map
        ScrollPane.style.left = (-x + DragContainer.offsetWidth / 2) + "px";
        ScrollPane.style.top = (-y + DragContainer.offsetHeight / 2) + "px";

        updateTiles();

    } // centerAtPhysical


    //----------------------------------------------------------------------
    // This places the specified logical coordinate (parsec)
    // at the center of the viewport
    //----------------------------------------------------------------------
    function centerAtLogical(x, y)
    //----------------------------------------------------------------------
    {
        centerAtPhysical(x * Scale, -y * Scale);

    } // centerAtLogical


    //----------------------------------------------------------------------
    function refreshDisplay()
    //----------------------------------------------------------------------
    {
        clearTileCache();
        centerAtLogical(LogicalCenteredAtX, LogicalCenteredAtY);

    } // refreshDisplay


    //----------------------------------------------------------------------
    function shouldAnimateToSectorHex(scale, sx, sy, hx, hy)
    //----------------------------------------------------------------------
    {
        if (scale !== Scale) {
            return false;
        }

        var oTarget = sectorHexToLogical(sx, sy, hx, hy);

        return (
            (Math.abs(oTarget.x - LogicalCenteredAtX) < Astrometrics.SectorWidth * 2) &&
            (Math.abs(oTarget.y - LogicalCenteredAtY) < Astrometrics.SectorHeight * 2));

    } // shouldAnimateToSectorHex



    //----------------------------------------------------------------------
    // Time smoothing function - input time is t within duration dur.
    // Acceleration period is a, deceleration period is d.
    //
    // Example:     t_filtered = smooth( t, 1.0, 0.25, 0.25 );
    //
    // Reference:   http://www.w3.org/TR/2005/REC-SMIL2-20050107/smil-timemanip.html
    //----------------------------------------------------------------------
    function smooth(t, dur, a, d)
    //----------------------------------------------------------------------
    {
        var dacc = dur * a;
        var ddec = dur * d;
        var r = 1 / (1 - a / 2 - d / 2);
        var r_t, tdec, pd;

        if (t < dacc) {
            r_t = r * (t / dacc);
            return t * r_t / 2;
        }
        else if (t <= (dur - ddec)) {
            return r * (t - dacc / 2);
        }
        else {
            tdec = t - (dur - ddec);
            pd = tdec / ddec;

            return r * (dur - dacc / 2 - ddec + tdec * (2 - pd) / 2);
        }
    } // smooth

    //
    // dur = total duration (seconds)
    // tick = time between callbacks (milliseconds)
    // callback = function called with animation position (0.0 ... 1.0)
    // smooth = optional smoothing function
    //
    function Animation(dur, tick, callback, smooth) {
        if (typeof callback !== 'function') { throw new TypeError(); }

        var pos = 0;

        var self = this;
        this.timerid = window.setInterval(function() {

            pos += (tick / 1000) / dur;
            var p = pos;

            if (typeof smooth === 'function') {
                p = smooth(p);
            }

            callback(p);

            // Next tick
            if (p >= 1.0) {
                window.clearInterval(self.timerid);
            }

        }, tick);
    }
    Animation.prototype.cancel = function() {
        if (this.timerid) {
            window.clearInterval(this.timeout);
        }
    };

    var animation = null;
    //----------------------------------------------------------------------
    // Sets up an animation from the current view location to the
    // specified target view location.
    //----------------------------------------------------------------------
    function animateToSectorHex(sx, sy, hx, hy)
    //----------------------------------------------------------------------
    {
        if (animation) { animation.cancel(); animation = null; }

        var oTarget = sectorHexToLogical(sx, sy, hx, hy);

        var oSource = {};
        oSource.x = LogicalCenteredAtX;
        oSource.y = LogicalCenteredAtY;

        if (animation) {
            animation.cancel();
        }

        function interpolate(a, b, p) {
            return a * (1.0 - p) + b * p;
        }

        animation = new Animation(3.0, 40, function(p) {
            centerAtLogical(
                interpolate(oSource.x, oTarget.x, p),
                interpolate(oSource.y, oTarget.y, p));
            },
            function(p) {
                return smooth(p, 1.0, 0.1, 0.25);
            });


    } // animateToSectorHex


    // ======================================================================
    // Static functions to normalize behavior across event models
    // ======================================================================

    var DOMHelpers = {};
    DOMHelpers.addEvent = function(element, type, listener, useCapture) {
        if (element.addEventListener) {
            // Gecko wants 'DOMMouseScroll', WebKit/Presto wants 'mousewheel'; do both
            element.addEventListener(type, listener, useCapture);
            if (type === 'mousewheel') { element.addEventListener('DOMMouseScroll', listener, useCapture); }
        }
        else if (element.attachEvent) {
            // IE<9
            element.attachEvent('on' + type, listener);
        }
    };
    DOMHelpers.removeEvent = function(element, type, listener, useCapture) {
        if (element.removeEventListener) {
            // Gecko wants 'DOMMouseScroll', WebKit/Presto want 'mousewheel'; do both
            element.removeEventListener(type, listener, useCapture);
            if (type === 'mousewheel') { element.removeEventListener('DOMMouseScroll', listener, useCapture); }
        }
        else if (element.detachEvent) {
            // IE<9
            element.detachEvent('on' + type, listener);
        }
    };
    DOMHelpers.setCapture = function(element) {
        if (element.setCapture) {
            element.setCapture(true);
        }
    };
    DOMHelpers.focus = function(element) {
        if (element.focus) {
            element.focus();
        }
    };
    DOMHelpers.releaseCapture = function(element) {
        if (element.releaseCapture) {
            element.releaseCapture();
        }
    };
    DOMHelpers.cancelEvent = function(e) {
        e = e ? e : window.event;

        if (e.stopPropagation) { e.stopPropagation(); }

        if (e.preventDefault) { e.preventDefault(); }

        e.cancelBubble = true;
        e.cancel = true;
        e.returnValue = false;

        return false;
    };



    //----------------------------------------------------------------------
    // Constructor
    //----------------------------------------------------------------------

    DragContainer = mapContainer;
    DragContainer.style.cursor = "move";

    ScrollPane = document.createElement("div");
    ScrollPane.style.position = "absolute";
    ScrollPane.innerHTML = "";

    DragContainer.appendChild(ScrollPane);

    //----------------------------------------------------------------------
    // Event Handlers
    //----------------------------------------------------------------------


    DOMHelpers.addEvent(window, 'resize', function(e) {
        e = e ? e : window.event;

        if (PhysicalCenteredAtX !== null && PhysicalCenteredAtY !== null) {
            self.Scroll(0, 0);
        }
    });


    // Hover
    var hover_x = 0, hover_y = 0;
    DOMHelpers.addEvent(DragContainer, 'mousemove', function(e) {
        e = e ? e : window.event;

        if (!self.OnHover) {
            return;
        }

        // Compute the physical coordinates
        var px = PhysicalCenteredAtX + e.clientX - DragContainer.offsetLeft - DragContainer.offsetWidth / 2;
        var py = PhysicalCenteredAtY + e.clientY - DragContainer.offsetTop - DragContainer.offsetHeight / 2;

        // Convert to logical coordinates
        var lx = px / Scale;
        var ly = -py / Scale;

        // Convert to hex coordinates
        var hx = Math.round((lx / Astrometrics.ParsecScaleX) + 0.5);
        var hy = Math.round((-ly / Astrometrics.ParsecScaleY) + ((hx % 2 === 0) ? 0.5 : 0));

        // Throttle the events
        if (hover_x !== hx || hover_y !== hy) {
            hover_x = hx;
            hover_y = hy;

            self.OnHover(hx, hy);
        }

        return DOMHelpers.cancelEvent(e);
    });


    // Dragging
    var dragging, drag_x, drag_y;

    DOMHelpers.addEvent(DragContainer, 'mousedown', function(e) {
        e = e ? e : window.event;

        DOMHelpers.focus(DragContainer);

        if (animation) { animation.cancel(); animation = null; }

        dragging = true;
        drag_x = e.clientX;
        drag_y = e.clientY;
        DOMHelpers.setCapture(document);

        return DOMHelpers.cancelEvent(e);
    }, true);


    DOMHelpers.addEvent(document, 'mousemove', function(e) {
        e = e ? e : window.event;

        if (dragging) {
            var dx = drag_x - e.clientX;
            var dy = drag_y - e.clientY;

            self.Scroll(dx, dy);

            drag_x = e.clientX;
            drag_y = e.clientY;
            return DOMHelpers.cancelEvent(e);
        }
    }, true);

    DOMHelpers.addEvent(document, 'mouseup', function(e) {
        e = e ? e : window.event;

        if (dragging) {
            dragging = false;
            DOMHelpers.releaseCapture(document);
            return DOMHelpers.cancelEvent(e);
        }
    });


    DOMHelpers.addEvent(DragContainer, 'click', function(e) {
        e = e ? e : window.event;

        return DOMHelpers.cancelEvent(e);
    });


    DOMHelpers.addEvent(DragContainer, 'dblclick', function(e) {
        e = e ? e : window.event;

        if (animation) { animation.cancel(); animation = null; }

        // Center the drag container offset within the map

        var dx = e.clientX - DragContainer.offsetLeft - DragContainer.offsetWidth / 2;
        var dy = e.clientY - DragContainer.offsetTop - DragContainer.offsetHeight / 2;

        self.Scroll(dx, dy);

        if (e.altKey) {
            self.ZoomOut();
        }
        else if (Scale < 64) {
            // Don't zoom in past the "maximum detail" setting
            self.ZoomIn();
        }

        drag_x = e.clientX;
        drag_y = e.clientY;

        return DOMHelpers.cancelEvent(e);
    });


    DOMHelpers.addEvent(DragContainer, 'mousewheel', function(e) {
        e = e ? e : window.event;

        var delta = e.detail ? e.detail * -40 : e.wheelDelta;

        if (animation) { animation.cancel(); animation = null; }

        if (delta < 0) {
            self.ZoomOut();
        }
        else if (delta > 0) {
            self.ZoomIn();
        }

        return DOMHelpers.cancelEvent(e);
    });


    DOMHelpers.addEvent(DragContainer, 'keydown', function(e) {
        e = e ? e : window.event;

        var VK_I = 73,
            VK_J = 74,
            VK_K = 75,
            VK_L = 76,
            VK_SUBTRACT = 109,  // -_ Firefox
            VK_SUBTRACT2 = 189, // -_ IE
            VK_EQUALS = 61,     // =+ Firefox
            VK_EQUALS2 = 187;   // =+ IE

        var key = e.which || e.keyCode;
        switch (key) {
            case VK_I: self.Scroll(0, -10); break;
            case VK_J: self.Scroll(-10, 0); break;
            case VK_K: self.Scroll(0, 10); break;
            case VK_L: self.Scroll(10, 0); break;
            case VK_SUBTRACT:
            case VK_SUBTRACT2: self.ZoomOut(); break;
            case VK_EQUALS:
            case VK_EQUALS2: self.ZoomIn(); break;
        }
    });


    //----------------------------------------------------------------------
    // Public Methods
    //----------------------------------------------------------------------

    //----------------------------------------------------------------------
    this.GetHexX = function() { return HexCenteredAtX; };
    //----------------------------------------------------------------------

    //----------------------------------------------------------------------
    this.GetHexY = function() { return HexCenteredAtY; };
    //----------------------------------------------------------------------

    //----------------------------------------------------------------------
    this.GetX = function() { return LogicalCenteredAtX; };
    //----------------------------------------------------------------------

    //----------------------------------------------------------------------
    this.GetY = function() { return LogicalCenteredAtY; };
    //----------------------------------------------------------------------

    //----------------------------------------------------------------------
    this.GetScale = function() { return Scale; };
    //----------------------------------------------------------------------

    //----------------------------------------------------------------------
    this.GetOptions = function() { return Options; };
    //----------------------------------------------------------------------


    //----------------------------------------------------------------------
    this.SetOptions = function(options, refresh)
    //----------------------------------------------------------------------
    {
        if (options === Options) {
            return;
        }

        Options = options & MapOptions.Mask;
        clearTileCache();

        if (this.OnOptionsChanged) {
            this.OnOptionsChanged(Options);
        }


        if (refresh) {
            refreshDisplay();
        }

    }; // SetOptions



    //----------------------------------------------------------------------
    // This places the specified Sector, Hex coordinates (parsec)
    // at the center of the viewport, with a specific scale.
    //----------------------------------------------------------------------
    this.ScaleCenterAtSectorHex = function(scale, sx, sy, hx, hy)
    //----------------------------------------------------------------------
    {
        if (animation) { animation.cancel(); animation = null; }
        //setYouAreHere( sx, sy, hx, hy );

        if (shouldAnimateToSectorHex(scale, sx, sy, hx, hy)) {
            animateToSectorHex(sx, sy, hx, hy);
        }
        else {
            // Clear cache on non-animated transition so we are not
            // boundlessly leaking memory
            // FUTURE: Put an upper limit on the cache size instead
            clearTileCache();

            this.SetScale(scale, false);
            this.CenterAtSectorHex(sx, sy, hx, hy);
        }

    }; // ScaleCenterAtSectorHex


    //----------------------------------------------------------------------
    // This places the specified Sector, Hex coordinates (parsec)
    // at the center of the viewport
    //----------------------------------------------------------------------
    this.CenterAtSectorHex = function(sx, sy, hx, hy)
    //----------------------------------------------------------------------
    {
        var target = sectorHexToLogical(sx, sy, hx, hy);

        centerAtLogical(target.x, target.y);

    }; // CenterAtSectorHex



    //----------------------------------------------------------------------
    // Set the selected scale in pixels/parsec.
    // This causes a refresh, but the currently centered
    // logical coordinates are retained.
    //----------------------------------------------------------------------
    this.SetScale = function(scale, refresh)
    //----------------------------------------------------------------------
    {
        if (scale === Scale) {
            return;
        }
        if (scale < Astrometrics.MinScale) { scale = Astrometrics.MinScale; }
        if (scale > Astrometrics.MaxScale) { scale = Astrometrics.MaxScale; }

        Scale = scale;
        clearTileCache();

        if (this.OnScaleChanged) {
            this.OnScaleChanged(Scale);
        }

        if (refresh) {
            refreshDisplay();
        }

    }; // SetScale



    //----------------------------------------------------------------------
    // This places the specified coordinate (parsec)
    // at the center of the viewport
    //----------------------------------------------------------------------
    this.SetPosition = function(x, y)
    //----------------------------------------------------------------------
    {
        centerAtLogical(x, y);

    }; // SetPosition



    //----------------------------------------------------------------------
    // Scroll the map view by the specified dx/dy (in pixels)
    //----------------------------------------------------------------------
    this.Scroll = function(dx, dy)
    //----------------------------------------------------------------------
    {
        centerAtPhysical(PhysicalCenteredAtX + dx, PhysicalCenteredAtY + dy);

    }; // Scroll


    //----------------------------------------------------------------------
    // Zoom in to the next zoom level.
    //----------------------------------------------------------------------
    this.ZoomIn = function()
    //----------------------------------------------------------------------
    {
        this.SetScale(Scale * 2, true);

    }; // ZoomIn


    //----------------------------------------------------------------------
    // Zoom out to the next zoom level.
    //----------------------------------------------------------------------
    this.ZoomOut = function()
    //----------------------------------------------------------------------
    {
        this.SetScale(Scale / 2, true);

    }; // ZoomOut


    //----------------------------------------------------------------------
    // NOTE: This API is subject to change
    //----------------------------------------------------------------------
    this.TEMP_AddMarker = function(id, sx, sy, hx, hy)
    //----------------------------------------------------------------------
    {
        var marker = {
            "sx": sx,
            "sy": sy,
            "hx": hx,
            "hy": hy,

            "id": id,
            "z": 9
        };

        markers.push(marker);
        makeMarker(marker);

    }; // AddMarker

    //----------------------------------------------------------------------
    // NOTE: This API is subject to change
    //----------------------------------------------------------------------
    this.TEMP_AddOverlay = function(x, y, w, h)
    //----------------------------------------------------------------------
    {
        var overlay = {
            "x": x,
            "y": y,
            "w": w,
            "h": h,

            "id": "overlay",
            "z": 10
        };

        overlays.push(overlay);
        makeOverlay(overlay);

    }; // AddOverlay

}  // Map
