"use strict";
/*jslint onevar: false, white: false, bitwise: false */
/*global document, window */

//----------------------------------------------------------------------
// Enumerated types
//----------------------------------------------------------------------

var MapOptions = {};

MapOptions.SectorGrid           = 0x0001;
MapOptions.SubsectorGrid        = 0x0002;
MapOptions.SectorsSelected      = 0x0004;
MapOptions.SectorsAll           = 0x0008;
MapOptions.SectorsMask          = 0x000c;
MapOptions.BordersMajor         = 0x0010;
MapOptions.BordersMinor         = 0x0020;
MapOptions.BordersMask          = 0x0030;
MapOptions.NamesMajor           = 0x0040;
MapOptions.NamesMinor           = 0x0080;
MapOptions.NamesMask            = 0x00c0;
MapOptions.WorldsCapitals       = 0x0100;
MapOptions.WorldsHomeworlds     = 0x0200;
MapOptions.RoutesSelected       = 0x0400;
MapOptions.PrintStyle           = 0x0800;
MapOptions.CandyStyle           = 0x1000;
MapOptions.StyleMask            = 0x1800;
MapOptions.ForceHexes           = 0x2000;
MapOptions.Mask                 = 0x3fff;


//----------------------------------------------------------------------
// Astrometric Constants
//----------------------------------------------------------------------

var Astrometrics = {};

Astrometrics.ParsecScaleX = Math.cos(Math.PI/6); // cos(30)
Astrometrics.ParsecScaleY = 1.0;
Astrometrics.SectorWidth  = 32;
Astrometrics.SectorHeight = 40;
Astrometrics.ReferenceHexX = 1; // Reference is at Core 0140
Astrometrics.ReferenceHexY = 40;
Astrometrics.TileWidth  = 256;
Astrometrics.TileHeight = 256;
Astrometrics.MinScale = 0.015625;
Astrometrics.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


    
    

    //----------------------------------------------------------------------
    // Event Handlers
    //----------------------------------------------------------------------

    var LastDragX = 0;
    var LastDragY = 0;


    var DOMHelpers = {

        'addEvent': function(element, type, listener, useCapture) {
            if (element.addEventListener) {
                // Gecko wants 'DOMMouseScroll', Webkit/Opera wants 'mousewheel'; do both
                element.addEventListener(type, listener, useCapture);
                if (type === 'mousewheel') {
                    element.addEventListener('DOMMouseScroll', listener, useCapture);
                }
            }
            else if (element.attachEvent) {
                // IE
                element.attachEvent('on' + type, listener);
            }
        },

        'removeEvent': function(element, type, listener, useCapture) {
            if (element.removeEventListener) {
                // Gecko wants 'DOMMouseScroll', Webkit/Opera wants 'mousewheel'; do both
                element.removeEventListener(type, listener, useCapture);
                if (type === 'mousewheel') {
                    element.removeEventListener('DOMMouseScroll', listener, useCapture);
                }
            }
            else if (element.detachEvent) {
                // IE
                element.detachEvent('on' + type, listener);
            }
        },

        'setCapture': function(element) {
            if (element.setCapture) {
                element.setCapture(true);
            }
        },

        'focus': function(element) {
            if (element.focus) {
                element.focus();
            }
        },

        'releaseCapture': function(element) {
            if (element.releaseCapture) {
                element.releaseCapture();
            }
        },

        '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;
        }
    };

    //----------------------------------------------------------------------
    function onResize(e)
    //----------------------------------------------------------------------
    {
        if (PhysicalCenteredAtX !== null && PhysicalCenteredAtY !== null) {
            centerAtPhysical(PhysicalCenteredAtX, PhysicalCenteredAtY);
        }

    } // onResize


    var LastHoverX = 0;
    var LastHoverY = 0;

    //----------------------------------------------------------------------
    function onMouseMove(e)
    //----------------------------------------------------------------------
    {
        if (!self.OnHover) {
            return;
        }

        e = e ? e : window.event;

        // 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 (LastHoverX !== hx || LastHoverY !== hy) {
            LastHoverX = hx;
            LastHoverY = hy;

            self.OnHover(hx, hy);
        }

        return DOMHelpers.cancelEvent(e);

    } // onMouseMove


    //----------------------------------------------------------------------
    function onMouseDrag(e)
    //----------------------------------------------------------------------
    {
        e = e ? e : window.event;

        var dx = -(e.clientX - LastDragX);
        var dy = -(e.clientY - LastDragY);

        centerAtPhysical(PhysicalCenteredAtX + dx, PhysicalCenteredAtY + dy);

        LastDragX = e.clientX;
        LastDragY = e.clientY;

        return DOMHelpers.cancelEvent(e);

    } // onMouseDrag


    //----------------------------------------------------------------------
    function onMouseUp(e)
    //----------------------------------------------------------------------
    {
        e = e ? e : window.event;

        DOMHelpers.removeEvent(document, 'mousemove', onMouseDrag, true);
        DOMHelpers.removeEvent(document, 'mouseup', onMouseUp, true);
        DOMHelpers.releaseCapture(document);

        return DOMHelpers.cancelEvent(e);

    } // onMouseUp


    //----------------------------------------------------------------------
    function onStartDrag(e)
    //----------------------------------------------------------------------
    {
        e = e ? e : window.event;

        DOMHelpers.focus(DragContainer);

        if (animation) { animation.cancel(); animation = null; }

        // Add events to document, so that events outside the container are seen
        DOMHelpers.addEvent(document, 'mousemove', onMouseDrag, true);
        DOMHelpers.addEvent(document, 'mouseup', onMouseUp, true);
        DOMHelpers.setCapture(document);

        LastDragX = e.clientX;
        LastDragY = e.clientY;

        return DOMHelpers.cancelEvent(e);

    } // onStartDrag


    //----------------------------------------------------------------------
    function onDoubleClick(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;

        centerAtPhysical(PhysicalCenteredAtX + dx, PhysicalCenteredAtY + dy);

        if (e.altKey) {
            self.ZoomOut();
        }
        else if (Scale < 64) {
            // Don't zoom in past the "maximum detail" setting
            self.ZoomIn();
        }

        LastDragX = e.clientX;
        LastDragY = e.clientY;

        return DOMHelpers.cancelEvent(e);

    } // onDoubleClick



    //----------------------------------------------------------------------
    function onMouseWheel(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);

    } // onMouseWheel


    //----------------------------------------------------------------------
    function onKeyDown(e)
    //----------------------------------------------------------------------
    {
        e = e ? e : window.event;

        var key = e.which || e.keyCode;
        if (key === 73) { self.Scroll(0, -10); } // I
        else if (key === 74) { self.Scroll(-10, 0); } // J
        else if (key === 75) { self.Scroll(0, 10); } // K
        else if (key === 76) { self.Scroll(10, 0); } // L
        else if (key === 109 || key === 189) { self.ZoomOut(); } // -_ (Firefox and IE respectively)
        else if (key === 61 || key === 187) { self.ZoomIn(); } // += (Firefox and IE respectively)

    } // onKeyDown


    //----------------------------------------------------------------------
    // Constructor
    //----------------------------------------------------------------------

    DragContainer = mapContainer;
    DragContainer.style.cursor = "move";

    ScrollPane = document.createElement("div");
    ScrollPane.style.position = "absolute";
    ScrollPane.innerHTML = "";

    DragContainer.appendChild(ScrollPane);

    // Event Handlers
    DOMHelpers.addEvent(DragContainer, 'mousedown', onStartDrag, false);
    DOMHelpers.addEvent(DragContainer, 'dblclick', onDoubleClick, false);
    DOMHelpers.addEvent(DragContainer, 'mousemove', onMouseMove, false);
    DOMHelpers.addEvent(DragContainer, 'keydown', onKeyDown, false);
    DOMHelpers.addEvent(DragContainer, 'mousewheel', onMouseWheel, false);
    DOMHelpers.addEvent(window, 'resize', onResize, false);

    //----------------------------------------------------------------------
    // 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
