﻿var MIN_ZOOM_LEVEL = 1;
var MAX_ZOOM_LEVEL = 19; // 6 for moon; 17 for earth
var TILE_SIZE = 256;
var GLOBE = 'earth'; // default map

var currentTopLeftQuadkey = 'q';

/// <summary>Used by TileClient_Mouse for mouseDown begin X</summary>
var beginX;
/// <summary>Used by TileClient_Mouse for mouseDown begin Y</summary>
var beginY;
var isMouseDown = false;
var added = false;
var canMouseWheel = false;
/**
<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
Intellisense    http://weblogs.asp.net/scottgu/archive/2007/06/21/vs-2008-javascript-intellisense.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;
};



String.prototype.parseDeg = function() {
/// <summary>
/// extend String object with method for parsing degrees or lat/long values to numeric degrees
///
/// this is very flexible on formats, allowing signed decimal degrees, or deg-min-sec suffixed by 
/// compass direction (NSEW). A variety of separators are accepted (eg 3º 37' 09"W) or fixed-width 
/// format without separators (eg 0033709W). Seconds and minutes may be omitted. (Minimal validation 
/// is done).
/// </summary>
/// <remarks>
/// Source: &copy; 2002-2006 Chris Veness 
/// http://www.movable-type.co.uk/scripts/latlong-vincenty.html
/// </remarks>
/// <test>
/// '53 09 02N'
/// '001 50 40W'
/// '52 12 19N'
/// '000 08 33W'
/// </test>
  if (!isNaN(this)) return Number(this);                 // signed decimal degrees without NSEW

  var degLL = this.replace(/^-/,'').replace(/[NSEW]/i,'');  // strip off any sign or compass dir'n
  var dms = degLL.split(/[^0-9.]+/);                     // split out separate d/m/s
  for (var i in dms) if (dms[i]=='') dms.splice(i,1);    // remove empty elements (see note below)
  switch (dms.length) {                                  // convert to decimal degrees...
    case 3:                                              // interpret 3-part result as d/m/s
      var deg = dms[0]/1 + dms[1]/60 + dms[2]/3600; break;
    case 2:                                              // interpret 2-part result as d/m
      var deg = dms[0]/1 + dms[1]/60; break;
    case 1:                                              // decimal or non-separated dddmmss
      if (/[NS]/i.test(this)) degLL = '0' + degLL;       // - normalise N/S to 3-digit degrees
      var deg = dms[0].slice(0,3)/1 + dms[0].slice(3,5)/60 + dms[0].slice(5)/3600; break;
    default: return NaN;
  }
  if (/^-/.test(this) || /[WS]/i.test(this)) deg = -deg; // take '-', west and south as -ve
  return deg;
  // note: whitespace at start/end will split() into empty elements (except in IE)
}

Number.prototype.toRadian = function() {  
/// <summary>
/// extend Number object with methods for converting degrees/radians
/// convert degrees to radians
/// </summary>
  return this * Math.PI / 180;
}

Number.prototype.toDegreeMinuteSecond = function(compass) {
/// <summary>
///
/// </summary>
/// <remarks>
/// http://support.microsoft.com/kb/213449
/// </remarks>
/// <test>
/// 10.46 = 10~ 27' 36"
/// </test>
    var hemisphere = '';
    if (compass)
    {
        if (compass == 'NS')
        {
            if (this < 0) hemisphere = 'S'; else hemisphere = 'N';
        }
        else if (compass == 'EW')
        {
            if (this < 0) hemisphere = 'W'; else hemisphere = 'E';
        }
    }
    var decimalDegrees = Math.abs(this);
    //Set degree to Integer of Argument Passed
    var degrees = Math.floor(decimalDegrees);
    //Set minutes to 60 times the number to the right
    //of the decimal for the variable Decimal_Deg
    var minutes = (decimalDegrees - degrees) * 60;
    //Set seconds to 60 times the number to the right of the
    //decimal for the variable Minute
    var seconds = (minutes - Math.floor(minutes)) * 60;
    //Returns the Result of degree conversion
    return degrees + '° ' + Math.floor(minutes) + "' " + Math.round(seconds) + '"' + hemisphere;
}


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 w = Math.floor(x / 180);
        //alert(x +':'+w +);
        if ( (x < -180) || (x > 180) ) //x = x % 180;
        {   // x is outside normal bounds
            
            if (w % 2)
            { // 
                //alert('a' +Math.round(x / 180) % 2);
                x = -1 * (180 - (x % 180));
            }
            else
            {
                //alert('b' +Math.round(x / 180) % 2);
                x = (x % 180);
            }
        }
        //if ( (y < -85) || (y > 85) ) y = y % 85;
        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.TopOffsetPixels  = 0;
    this.LeftOffsetPixels = 0;
    
        
    this.set_Width = function(newWidth)
    {
    /// <summary>Set the width (and columns) of the Viewport after resizing</summary>
        this.Width   = newWidth;
        this.Columns = Math.floor(this.Width/this.TileSize);
    }
    this.set_Height = function(newHeight)
    {
    /// <summary>Set the height (and rows) of the Viewport after resizing</summary>
        this.Height = newHeight;
        this.Rows   = Math.floor(this.Height/this.TileSize);
    }
    
    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 + VIEW.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 + VIEW.Rows) > world.Rows) 
        {
            row = world.Rows - VIEW.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 + VIEW.Columns) > world.Columns) 
        {
            //alert ('columns');
            column = world.Columns - VIEW.Columns; 
        }
        else if (column < 0)
        {
            column = 0;
        } 
        if ( (row + VIEW.Rows) > world.Rows) 
        {
            row = world.Rows - VIEW.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;
