﻿/**
<summary>
TileClient:
- World
- Viewport
- Point
  PointFromBinary
- Tile
</summary>
<remarks>
Reference urls:
Classes/objects http://mckoss.com/jscript/object.htm
Inheritance     http://www.cs.rit.edu/~atk/JavaScript/manuals/jsobj/
                http://blogs.msdn.com/ptorr/archive/2006/06/19/638195.aspx
</remarks>
*/
String.prototype.pad = function(l, s, t)
{
/// <summary>
/// Jonas Raoni Soares Silva
/// http://jsfromhell.com/string/pad
/// </summary>
    return s || (s = " "), (l -= this.length) > 0 ? (s = new Array(Math.ceil(l / s.length)
        + 1).join(s)).substr(0, t = !t ? l : t == 1 ? 0 : Math.ceil(l / 2))
        + this + s.substr(0, l - t) : this;
};

function World (zoom, tilesize)
{
/// <summary>
/// Helper object for calculations regarding the 'world' tile-space
/// </summary>
    this.Zoom = zoom;
    this.TileSize = tilesize;
    this.Columns = Math.pow(2, this.Zoom);
    this.Rows = Math.pow(2, this.Zoom);
    this.PixelSize = this.Columns * this.TileSize;
    
    this.set_Zoom = function (zoomLevel)
    {
        this.Zoom      = zoomLevel;
        this.Columns   = Math.pow(2, this.Zoom);
        this.Rows      = Math.pow(2, this.Zoom);
        this.PixelSize = this.Columns * this.TileSize;
        //this.Width   = this.Columns * this.TileSize;
        //this.Height  = this.Rows * this.TileSize;
    }
    
    this.get_LongLat = function (coordinate)
    {
    /// <summary>Return the longitude/latitude of the Point at the current zoom level</summary>
        var x = ((coordinate.X / this.PixelSize) * 360.0 ) - 180.0;
        var y = this.RadiansToDegrees (2 * Math.atan(Math.exp(Math.PI * (1.0 - 2.0 * coordinate.Y / this.PixelSize))) - Math.PI * 0.5);
        var point = eval('({"Longitude":'+x+', "Latitude":'+y+'})');
        return point;
    }
    this.get_Point = function (longitude, latitude)
    {
    /// <summary>Return the pixel location within the 'world' at the current zoom level</summary>
        var coordinate = new Point(0,0);
        coordinate.X = (longitude + 180.0) * this.PixelSize / 360.0;
        coordinate.Y = (this.PixelSize * 0.5) * (1 - (Math.log(Math.tan(DegreesToRadians(latitude) * 0.5 + Math.PI * 0.25))) / Math.PI);
        return coordinate;
    }
    this.RadiansToDegrees = function (rad)
    {
    /// <summary>Math helper method</summary>
        return (rad / Math.PI * 180.0);
    }
    this.DegreesToRadians = function (deg)
    {
    /// <summary>Math helper method</summary>
        return (deg * Math.PI / 180.0);
    }
    this.toString = function()
    {
    /// <summary>String represetation of the World</summary>
        return 'WORLD \r\nZoom:        ' + this.Zoom
        + '\r\nColumnsRows: ' + this.Columns + ',' + this.Rows 
        + '\r\nPixelSize: ' + this.PixelSize;
    }
}

function Viewport(width, height, tilesize)
{
/// <summary>
/// Helper object for calculations regarding the 'viewport' tile-space
/// </summary>
    this.TileSize = tilesize;
    this.Width   = width;
    this.Height  = height;
    this.Columns = Math.floor(width/tilesize);
    this.Rows    = Math.floor(height/tilesize);
    
    this.Top     = 0;
    this.Left    = 0;
    //this.Zoom    = 1;
    
    this.toString = function()
    {
    /// <summary>String represetation of the Viewport</summary>
        return 'VIEW ' + '\r\nTopLeft:     ' + this.Top + ',' + this.Left
        + '\r\nWidthHeight: ' + this.Width + ',' + this.Height
        + '\r\nColumnsRows: ' + this.Columns + ',' + this.Rows ;
    }
}

function Point(x,y)
{
/// <summary>Point</summary>
/// <param name="x">x coordinate as a binary string</param>
/// <param name="y">y coordinate as a binary string</param>
    this.X = x;
    this.Y = y;
    
    this.ToBase2 = function()
    {
    /// <summary>(Y,X) 111,000 = 101010 (pattern is YXYXYX)</summary>
        var binary='';
        for (var p = 0; p < this.X.length; p++)
        {
            binary = binary + this.Y.charAt(p) + this.X.charAt(p);
        }
        return binary;
    }
    this.SetXYFromBinary = function (binary)
    { 
    /// <summary>Convert binary number to coordinate</summary>
    /// <remarks>
    /// 033 = 001111 = 011,011 = 3,3  (Y, X)
    /// 123 = 011011 = 011,101 = 3,4  (Y, X)
    ///
    /// need charAt because treating string like array point.X[p] didn't work in IE7
    /// </remarks>
        var x='', y='';eval('var binaryString="'+binary+'"');
        for (var p = 0; p < binaryString.length; p++)
        {
            if (p % 2 == 0)
                y = y + '' + binaryString.charAt(p);
            else
                x = x + '' + binaryString.charAt(p);
        }
        //return new Point(x,y);  //Point.prototype.Base2ToPoint = function (binary)
        this.X = x;
        this.Y = y;
    }
    this.toString = function()
    {
    /// <summary>String represetation of the Point</summary>
        return 'POINT\r\nXY:     ' + this.X + ',' + this.Y;
    }
}
function PointFromBinary (binary)
{
/// <summary>
/// Point 'subclass' with constructor from binary (not sure how to do multiple constructors otherwise)
/// </summary>
    this.SetXYFromBinary(binary);
}
// Add Point to inheritance hierarchy
PointFromBinary.prototype=new Point;



function Tile(quadkey,zoom)
{
/// <summary>Tile class</summary>
/// <remarks>Models a tile</remarks>
    this.Quadkey = quadkey;
    this.Zoom   = zoom;
    this.Column = 0;
    this.Row    = 0;    
    
    //this.Latitude  = 0;
    //this.Longitude = 0;
    
    this.GetTopLeftQuadkey = function (deltaX, deltaY)
    {
    /// <summary>
    /// TODO: need different functions for the 'get relative' when choosing the new value
    ///       and when looping through
    /// </summary>
        var column = this.Column + deltaX;
        var row    = this.Row    + deltaY;
        
        var world = new World (zoom, TILE_SIZE);
        var viewport = new Viewport (512, 512, TILE_SIZE);
        if ( (column + viewport.Columns) > world.Columns) 
        {
            //alert ('column was ' + column + ' but now ' + (world.Columns - viewport.Columns) + ' ' + world.Columns + '-' +  viewport.Columns);
            column = world.Columns - viewport.Columns; 
        }
        else if (column < 0)
        {
            column = 0;
        } 
        if ( (row + viewport.Rows) > world.Rows) 
        {
            row = world.Rows - viewport.Rows; 
        }
        else if (row < 0)
        {
            row = 0;
        } 
        
        var binaryColumn = this.Base10ToBase2(column);    //Column.toString(2).pad(this.Zoom * 2, '0');
        var binaryRow    = this.Base10ToBase2(row);       //Row.toString(2).pad(this.Zoom * 2, '0');
        binXY = new Point (binaryColumn, binaryRow);
        var b2 = binXY.ToBase2();                //XYToBase2(binXY);
        var b4 = this.Base2ToBase4 (b2);         //parseInt(b2,2).toString(4).pad(zoom, '0')
        var relativeQuadkey = Base4ToQuadkey(b4);
        return relativeQuadkey;
    }
    this.GetRelativeQuadkey = function (deltaX, deltaY, max)
    {
    /// <summary>
    /// 
    /// </summary>
//        var qkb4   = QuadkeyToBase4 (this.Quadkey);
//        var binXY  = Base2ToXY (this.Base4ToBase2(qkb4)); //parseInt (qkb4,4).toString(2).pad(this.Zoom * 2, '0'));
//        var BinaryColumn = binXY.X;
//        var BinaryRow    = binXY.Y;
//        var Column = parseInt(binXY.X,2);
//        var Row    = parseInt(binXY.Y,2);
        
        var column = this.Column + deltaX; column = Math.abs(column % max); // allow 'wrapping'
        var row    = this.Row    + deltaY; row = Math.abs(row % max);       // allow 'wrapping'
        
        var binaryColumn = this.Base10ToBase2(column);    //Column.toString(2).pad(this.Zoom * 2, '0');
        var binaryRow    = this.Base10ToBase2(row);       //Row.toString(2).pad(this.Zoom * 2, '0');
        binXY = new Point (binaryColumn, binaryRow);
        var b2 = binXY.ToBase2();                //XYToBase2(binXY);
        var b4 = this.Base2ToBase4 (b2);         //parseInt(b2,2).toString(4).pad(zoom, '0')
        var relativeQuadkey = Base4ToQuadkey(b4);
        return relativeQuadkey;
    }
    
    this.GetRelativeQuadkeyFromPixels = function (clickX, clickY)
    {
    alert ('obsolete: GetRelativeQuadkeyFromPixels');
    /// <summary>
    /// Knowing this quadkey (assuming it is top-left), pass in a clicked-point and 
    /// work out which Quadkey was clicked
    /// </summary> 
        var column = this.Column + Math.floor(clickX / (TILE_SIZE/2) );
        var row    = this.Row    + Math.floor(clickY / (TILE_SIZE/2) );
        
        //alert(this.Column + ',' +this.Row +' cols adjusted for pixels: ' +row+','+column);
        
        var world = new World (zoom + 1, TILE_SIZE);
        var viewport = new Viewport (512, 512, TILE_SIZE);
        if ( (column + viewport.Columns) > world.Columns) 
        {
            //alert ('columns');
            column = world.Columns - viewport.Columns; 
        }
        else if (column < 0)
        {
            column = 0;
        } 
        if ( (row + viewport.Rows) > world.Rows) 
        {
            row = world.Rows - viewport.Rows; 
        }
        else if (row < 0)
        {
            row = 0;
        }
        
        var binaryColumn = this.Base10ToBase2(column);    //Column.toString(2).pad(this.Zoom * 2, '0');
        var binaryRow    = this.Base10ToBase2(row);       //Row.toString(2).pad(this.Zoom * 2, '0');
        binXY = new Point (binaryColumn, binaryRow);
        var b2 = binXY.ToBase2();                //XYToBase2(binXY);
        var b4 = this.Base2ToBase4 (b2);         //parseInt(b2,2).toString(4).pad(zoom, '0')
        var relativeQuadkey = Base4ToQuadkey(b4);
        return relativeQuadkey;
    }
    
    this.Base2ToBase4 = function (binary)
    {
    /// <example>
    /// 001111 = 033
    /// 011011 = 123
    /// 101111 = 233
    /// </example>
        var tzoom = (binary.length/2); // instead of zoom
        var tint = parseInt(binary,2);
        var tstr = tint.toString(4);
        var ret = tstr.pad(tzoom, '0')
        return ret;
    }
    
    this.Base4ToBase2 = function (base4, zoom)
    {
    /// <example>
    /// 231 = 101101
    /// 333 = 111111
    /// </example>
        return parseInt(base4,4).toString(2).pad(this.Zoom * 2, '0');
    }
    
    this.Base10ToBase2 = function (base10)
    {
    /// <example>
    /// 10 = 1010
    /// 15 = 1111
    /// </example>
        var tstr = base10.toString(2);
        return tstr.pad(this.Zoom, '0');
    }
    
    this.SetTileFromColumnRow = function (column, row, zoom)
    {
    /// <summary>
    /// Need to know the row, column and zoom level to do the conversion
    /// </summary>
        if (zoom) {} else {zoom = this.Zoom;}

        var binaryColumn = this.Base10ToBase2(column);    //Column.toString(2).pad(this.Zoom * 2, '0');
        var binaryRow    = this.Base10ToBase2(row);       //Row.toString(2).pad(this.Zoom * 2, '0');
        var binXY = new Point (binaryColumn, binaryRow);
        var b2 = binXY.ToBase2();                //XYToBase2(binXY);
        var b4 = this.Base2ToBase4 (b2);         //parseInt(b2,2).toString(4).pad(zoom, '0')
        this.Quadkey = Base4ToQuadkey(b4);       //return Base4ToQuadkey(b4);
    }
    
    // "constructor" logic at end, after functions are added
    var qkb4   = QuadkeyToBase4 (this.Quadkey);
    var binXY  = Base2ToPoint (this.Base4ToBase2(qkb4)); //parseInt (qkb4,4).toString(2).pad(this.Zoom * 2, '0'));
    var BinaryColumn = binXY.X;
    var BinaryRow    = binXY.Y;
    this.Column = parseInt(binXY.X,2);
    this.Row    = parseInt(binXY.Y,2);
}
function TileFromRowColumn (column, row, zoom)
{
/// <summary>
/// Tile 'subclass' with constructor from col, row (not sure how to do multiple constructors otherwise)
/// </summary>
    this.Zoom   = zoom;
    this.Column = column;
    this.Row    = row;  
    this.SetTileFromColumnRow (column, row, zoom);
}
// Add Tile to inheritance hierarchy
TileFromRowColumn.prototype = new Tile;
