Tile = function(pHash){
	// user set constants
	this.maxZoom = (pHash && pHash.maxzoom)?pHash.maxzoom:17;
	// ...Constants...
	this.tileSize = 256;
	this.pixelsPerLonDegree = [];
	this.pixelsPerLonRadian = [];
	this.numTiles = [];
	this.bitmapOrigo = [];
	// Note: These variable names are based on the variables names found in the
	//       Google maps.*.js code.
	this.c = 256;
	this.bc;
	this.Wa;
		
	this.fillInConstants();
}

Tile.prototype = {
	// ...Public...
	'getTileCoords': function(lat, lng, zoom) {
		return this.getTileCoordinate(lat, lng, zoom);
	},

	'getTileLatLong': function(lat, lng, zoom) {
		return this.getLatLong(lat, lng, zoom);
	},

	// ...Private...
	// Fill in the constants array
	'fillInConstants': function() {
		this.bc = 2*Math.PI;
		this.Wa = Math.PI/180;
	
		for(z = 1; z < this.maxZoom+1; z++) {
  			this.pixelsPerLonDegree[z] = this.c / 360;
  			this.pixelsPerLonRadian[z] = this.c / this.bc;
  			e = this.c / 2;
  			this.bitmapOrigo[z] = this.p(e,e);
  			this.numTiles[z] = this.c / 256;
  			this.c *= 2;
		}
	},

	'getBitmapCoordinate': function(a, b, c) {
  		ret = this.p(0,0);
  		ret.x = Math.floor(this.bitmapOrigo[c].x + b * this.pixelsPerLonDegree[c]);
  		e = Math.sin(a * this.Wa);
  		if(e > 0.9999) {
    		e = 0.9999;
  		}
  		if(e < -0.9999) {
    		e = -0.9999;
  		}
  		ret.y = Math.floor(this.bitmapOrigo[c].y + 0.5 * Math.log((1 + e) / (1 - e)) * -1*(this.pixelsPerLonRadian[c]));
  		return ret;
	},

	'getTileCoordinate': function(a, b, c) {
  		ret = this.getBitmapCoordinate(a, b, c);
  		ret.x = Math.floor(ret.x / this.tileSize);
  		ret.y = Math.floor(ret.y / this.tileSize);

  		return ret;
	},

	'getLatLong': function(a, b, c) {
		ret = this.p(0, 0);
		e = this.getBitmapCoordinate(a, b, c);
  		a = e.x;
		b = e.y;

		ret.x = (a - this.bitmapOrigo[c].x) / this.pixelsPerLonDegree[c];
		e = (b - this.bitmapOrigo[c].y) / (-1*this.pixelsPerLonRadian[c]);
		ret.y = (2 * Math.atan(Math.exp(e)) - Math.PI / 2) / this.Wa;
		return ret;
	},

	'p': function(x,y){
		return {x:x,y:y};
	},
	
	'getKeyholeString': function(a, b, c) {
		s = "";
		myX = a;
		myY = b;
		for(i = 17; i > c; i--) {
			rx = (fmod(myX, 2));
			myX = Math.floor(myX / 2);
			ry = (fmod(myY, 2));
			myY = Math.floor(myY / 2);
			s = this.getKeyholeDirection(rx, ry).s;
		}
		return 't'.s;
	},
	
	'convertXYtoLatLon': function(x,y,z) {
		var lon      = -180; // x
		var lonWidth = 360; // width 360
		
		//double lat = -90;  // y
		//double latHeight = 180; // height 180
		var lat       = -1;
		var latHeight = 2;
		
		var tilesAtThisZoom = Math.pow(2,z);
		
		lonWidth  = 360.0 / tilesAtThisZoom;
		lon       = -180 + (x * lonWidth);
		latHeight = -2.0 / tilesAtThisZoom;
		lat       = 1 + (y * latHeight);
		
		// convert lat and latHeight to degrees in a mercator projection
		// note that in fact the coordinates go from
		// about -85 to +85 not -90 to 90!
		latHeight += lat;
		latHeight = (2 * Math.atan(Math.exp(Math.PI * latHeight))) - (Math.PI / 2);
		latHeight *= (180 / Math.PI);
		
		lat = (2 * Math.atan(Math.exp(Math.PI * lat))) - (Math.PI / 2);
		lat *= (180 / Math.PI);
		
		latHeight -= lat;
		
		if (lonWidth < 0) {
			lon      = lon + lonWidth;
			lonWidth = -lonWidth;
		}
		
		if (latHeight < 0) {
			lat       = lat + latHeight;
			latHeight = -latHeight;
		}
		return new YGeoPoint(lat,lon);
	},

	'getKeyholeDirection': function(x, y) {
		if(x == 1) {
			if(y == 1) {
				return 's';
			} else if(y == 0) {
				return 'r';
			}
		} else if(x == 0) {
			if(y == 1) {
				return 't';
			} else if(y == 0) {
				return 'q';
			}
		}
		return '';
	}
}

zoom = {
  google : {
    to_universal : function(nativeZoom) {
      return  Math.min(-1, -nativeZoom);
    },  

    to_native : function(universal) {
      return  universal > 0 ? universal : -universal;
    }
  },

  virtual_earth : {
    to_universal : function(nativeZoom) {
      return  Math.min(-1, -nativeZoom);
    },  

    to_native : function(universal) {
      return  universal > 0 ? universal : -universal;
    }
  },

  yahoo : {
    to_universal : function(nativeZoom) {
      return  Math.min(-1, nativeZoom - 18);
    },  

    to_native : function(universal) {
      return  universal > 0 ? universal : Math.max(universal + 18, 1);
    }
  }
};

function inspect(obj, maxLevels, level)
{
  var str = '', type, msg;

    // Start Input Validations
    // Don't touch, we start iterating at level zero
    if(level == null)  level = 0;

    // At least you want to show the first level
    if(maxLevels == null) maxLevels = 1;
    if(maxLevels < 1)     
        return '<font color="red">Error: Levels number must be > 0</font>';

    // We start with a non null object
    if(obj == null)
    return '<font color="red">Error: Object <b>NULL</b></font>';
    // End Input Validations

    // Each Iteration must be indented
    str += '<ul>';

    // Start iterations for all objects in obj
    for(property in obj)
    {
      try
      {
          // Show "property" and "type property"
          type =  typeof(obj[property]);
          str += '<li>(' + type + ') ' + property + 
                 ( (obj[property]==null)?(': <b>null</b>'):('')) + '</li>';

          // We keep iterating if this property is an Object, non null
          // and we are inside the required number of levels
          if((type == 'object') && (obj[property] != null) && (level+1 < maxLevels))
          str += inspect(obj[property], maxLevels, level+1);
      }
      catch(err)
      {
        // Is there some properties in obj we can't access? Print it red.
        if(typeof(err) == 'string') msg = err;
        else if(err.message)        msg = err.message;
        else if(err.description)    msg = err.description;
        else                        msg = 'Unknown';

        str += '<li><font color="red">(Error) ' + property + ': ' + msg +'</font></li>';
      }
    }

      // Close indent
      str += '</ul>';

    return str;
}
