
//----------------------------------------------------------------------
// 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 )
        {
            makeMarker( markers[i] );
        }
        for( i = 0; i < overlays.length; ++i )
        {
            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 )
        {
            for( y = 0; y < th; ++y )
            {
                checkMakeTile( x + tx, y + ty );
            }
        }
/*
        var s = "";
        var r = Math.ceil( Math.max( tw, th ) / 2 );

        var cx = tx + Math.floor( tw / 2 );
        var cy = ty + Math.floor( th / 2 );

        for( var i = 0; i <= r; i++ )
        {
            for( var x = -i; x < i; x++ )
            {
                for( var y = -i; y < i; y++ )
                {
                    checkMakeTile( cx + x, cy + y );
                }
            }
        }
*/

    } // 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





    // Animation State
    var AnimationDur;
    var AnimationPosition;
    var AnimationTick;
    var AnimationSource;
    var AnimationTarget;
    var AnimationTimeout;


    //----------------------------------------------------------------------
    // interpolate from a at p=0 to b at p=1
    //----------------------------------------------------------------------
    function interpolate( a, b, p )
    //----------------------------------------------------------------------
    {
        return a * ( 1.0 - p ) + b * p;

    } // interpolate


    //----------------------------------------------------------------------
    // 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


    //----------------------------------------------------------------------
    function cancelAnimation()
    //----------------------------------------------------------------------
    {
        if( !!AnimationTimeout )
        {
            window.clearTimeout( AnimationTimeout );
            AnimationTimeout = null;
        }

    } // cancelAnimation


    //----------------------------------------------------------------------
    // Callback for animation loop. Updates the view position
    // and schedules the next tick.
    //----------------------------------------------------------------------
    function animationCallback()
    //----------------------------------------------------------------------
    {
        var delta = AnimationTick / 1000 / AnimationDur;

        AnimationPosition += delta;

        var p = smooth( AnimationPosition, 1.0, 0.1, 0.25 );

        // This tick
        centerAtLogical(
            interpolate( AnimationSource.x, AnimationTarget.x, p ),
            interpolate( AnimationSource.y, AnimationTarget.y, p ) );

        // Next tick
        if( AnimationPosition >= 1.0 )
        {
            AnimationTimeout = null;
        }
        else
        {
            AnimationTimeout  = window.setTimeout( animationCallback, AnimationTick );
        }

    } // animationCallback



    //----------------------------------------------------------------------
    // Sets up an animation from the current view location to the
    // specified target view location.
    //----------------------------------------------------------------------
    function animateToSectorHex( sx, sy, hx, hy )
    //----------------------------------------------------------------------
    {
        cancelAnimation();

        var oTarget = sectorHexToLogical( sx, sy, hx, hy );

        var oSource = {};
        oSource.x = LogicalCenteredAtX;
        oSource.y = LogicalCenteredAtY;

        AnimationPosition = 0;
        AnimationDur      = 3.0; // seconds
        AnimationTick     = 40;  // ms
        AnimationSource   = oSource;
        AnimationTarget   = oTarget;
        AnimationTimeout  = window.setTimeout( animationCallback, AnimationTick );

    } // animateToSectorHex


    //----------------------------------------------------------------------
    // Event Handlers
    //----------------------------------------------------------------------

    var LastDragX = 0;
    var LastDragY = 0;

    //----------------------------------------------------------------------
    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 );
        }

    } // onMouseMove


    //----------------------------------------------------------------------
    function onMouseDrag( e )
    //----------------------------------------------------------------------
    {
        e = e ? e : window.event;

        e.cancelBubble = true;
        if( e.stopPropagation ) { e.stopPropagation(); }

        var dx =  - ( e.clientX - LastDragX );
        var dy =  - ( e.clientY - LastDragY );

        centerAtPhysical( PhysicalCenteredAtX + dx, PhysicalCenteredAtY + dy );

        LastDragX = e.clientX;
        LastDragY = e.clientY;

    } // onMouseDrag


    //----------------------------------------------------------------------
    function onMouseUp( e )
    //----------------------------------------------------------------------
    {
        e = e ? e : window.event;

        e.cancelBubble = true;
        if( e.stopPropagation ) { e.stopPropagation(); }

        if( document.removeEventListener )
        {
            document.removeEventListener( 'mousemove', onMouseDrag, true );
            document.removeEventListener( 'mouseup',   onMouseUp,   true );

            document.removeEventListener( 'mousemove', onMouseMove, true ); // TODO: Remove this line (?)
        }
        else
        {
            if( DragContainer.releaseCapture )
            {
                DragContainer.releaseCapture();
            }

            DragContainer.onmousemove = onMouseMove;
            DragContainer.onmouseup   = null;
        }

    } // onMouseUp


    //----------------------------------------------------------------------
    function onStartDrag( e )
    //----------------------------------------------------------------------
    {
        // Not supported in NN7
        if( DragContainer.focus )
        {
            DragContainer.focus();
        }

        e = e ? e : window.event;

        cancelAnimation();

        e.cancelBubble = true;
        if( e.stopPropagation ) { e.stopPropagation(); }

        if( DragContainer.addEventListener )
        {
            // Add events to document, so that events outside the container are seen
            document.addEventListener( 'mousemove', onMouseDrag, true );
            document.addEventListener( 'mouseup',   onMouseUp,   true );
        }
        else
        {
            if( DragContainer.setCapture )
            {
                DragContainer.setCapture( true );
            }

            DragContainer.onmousemove = onMouseDrag;
            DragContainer.onmouseup   = onMouseUp;
        }

        LastDragX = e.clientX;
        LastDragY = e.clientY;

        if( e.preventDefault )
        {
            // Block image drag/drop in Mozilla
            e.preventDefault();
        }

    } // onStartDrag


    //----------------------------------------------------------------------
    function onDoubleClick( e )
    //----------------------------------------------------------------------
    {
        e = e ? e : window.event;

        cancelAnimation();

        e.cancelBubble = true;
        if( e.stopPropagation ) { e.stopPropagation(); }

        // 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;

    } // onDoubleClick



    //----------------------------------------------------------------------
    function onMouseWheel( e )
    //----------------------------------------------------------------------
    {
        e = e ? e : window.event;

        var delta;
        if( e.detail )
        {
            // Mozilla/Opera
            delta = e.detail * -40;
        }
        else if( e.wheelDelta )
        {
            // MSIE
            delta = e.wheelDelta;
        }

        cancelAnimation();

        e.cancelBubble = true;
        if( e.stopPropagation ) { e.stopPropagation(); }
        if( e.preventDefault ) { e.preventDefault(); }

        if( delta < 0 )
        {
            self.ZoomOut();
        }
        else if( delta > 0 )
        {
            self.ZoomIn();
        }

        return false;

    } // onMouseWheel

    //----------------------------------------------------------------------
	function onKeyDown(e) 
    //----------------------------------------------------------------------
	{
	    if (!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
    if( DragContainer.addEventListener )
    {
        // W3C
        DragContainer.addEventListener( 'mousedown',      onStartDrag,   true  );
        DragContainer.addEventListener( 'dblclick',       onDoubleClick, true  );
        DragContainer.addEventListener( 'mousemove',      onMouseMove,   false );
        DragContainer.addEventListener( 'keydown',        onKeyDown,     false );
    }
    else
    {
        // IE
        DragContainer.onmousedown  = onStartDrag;
        DragContainer.ondblclick   = onDoubleClick;
        DragContainer.onmousemouse = onMouseMove;
        DragContainer.onkeydown    = onKeyDown;

        DragContainer.ondragstart   = function() { return false; }; // Block image drag/drop in MSIE
        DragContainer.onselectstart = function() { return false; }; // Block selection in MSIE
    }

    if( DragContainer.addEventListener && !window.opera )
    {
        // W3C
        DragContainer.addEventListener( "DOMMouseScroll", onMouseWheel,  false );
    }
    else
    {
        // IE/Opera
        DragContainer.onmousewheel = onMouseWheel;
    }


    if( window.addEventListener )
    {
        // W3C
        window.addEventListener( 'resize', onResize, false );
    }
    else
    {
        // IE
        DragContainer.onresize     = onResize;
    }

    //----------------------------------------------------------------------
    // 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 )
    //----------------------------------------------------------------------
    {
        cancelAnimation();
        //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
