/** 
* @requires OpenLayers/Layer/XYZ.js 
*/

/** 
* Class: OpenLayers.Layer.ArcGISCacheToken   
* Layer for accessing cached map tiles from an ArcGIS Server style mapcache. 
* Tile must already be cached for this layer to access it. This does not require 
* ArcGIS Server itself.
* 
* A few attempts have been made at this kind of layer before. See 
* http://trac.osgeo.org/openlayers/ticket/1967 
* and 
* http://trac.osgeo.org/openlayers/browser/sandbox/tschaub/arcgiscache/lib/OpenLayers/Layer/ArcGISCache.js
*
* Typically the problem encountered is that the tiles seem to "jump around".
* This is due to the fact that the actual max extent for the tiles on AGS layers
* changes at each zoom level due to the way these caches are constructed.
* We have attempted to use the resolutions, tile size, and tile origin
* from the cache meta data to make the appropriate changes to the max extent
* of the tile to compensate for this behavior.  This must be done as zoom levels change
* and before tiles are requested, which is why methods from base classes are overridden.
*
* For reference, you can access mapcache meta data in two ways. For accessing a 
* mapcache through ArcGIS Server, you can simply go to the landing page for the
* layer. (ie. http://services.arcgisonline.com/ArcGIS/rest/services/World_Street_Map/MapServer)
* For accessing it directly through HTTP, there should always be a conf.xml file
* in the root directory. 
* (ie. http://serverx.esri.com/arcgiscache/DG_County_roads_yesA_backgroundDark/Layers/conf.xml)
*  
*Inherits from: 
*  - <OpenLayers.Layer.XYZ>             
*/
OpenLayers.Layer.ArcGISCacheToken = OpenLayers.Class(OpenLayers.Layer.XYZ, {

    /**
    * APIProperty: url
    * {String | Array} The base URL for the layer cache.  You can also
    *     provide a list of URL strings for the layer if your cache is
    *     available from multiple origins.  This must be set before the layer
    *     is drawn.
    */
    url: null,

    /**
    * APIProperty: tileOrigin
    * {<OpenLayers.LonLat>} The location of the tile origin for the cache.
    *     An ArcGIS cache has it's origin at the upper-left (lowest x value
    *     and highest y value of the coordinate system).  The units for the
    *     tile origin should be the same as the units for the cached data.
    */
    tileOrigin: null,

    /**
    * APIProperty: tileSize
    * {<OpenLayers.Size>} This size of each tile. Defaults to 256 by 256 pixels.
    */
    tileSize: new OpenLayers.Size(256, 256),

    /**
    * APIProperty: useAGS
    * {Boolean} Indicates if we are going to be accessing the ArcGIS Server (AGS)
    *     cache via an AGS MapServer or directly through HTTP. When accessing via
    *     AGS the path structure uses a standard z/y/x structure. But AGS actually
    *     stores the tile images on disk using a hex based folder structure that looks
    *     like "http://example.com/mylayer/L00/R00000000/C00000000.png".  Learn more
    *     about this here:
    *     http://blogs.esri.com/Support/blogs/mappingcenter/archive/2010/08/20/Checking-Your-Local-Cache-Folders.aspx
    *     Defaults to true;
    */
    useArcGISServer: true,

    /**
    * APIProperty: type
    * {String} Image type for the layer.  This becomes the filename extension
    *     in tile requests.  Default is "png" (generating a url like
    *     "http://example.com/mylayer/L00/R00000000/C00000000.png").
    */
    type: 'png',

    /**
    * APIProperty: useScales
    * {Boolean} Optional override to indicate that the layer should use 'scale' information
    *     returned from the server capabilities object instead of 'resolution' information.
    *     This can be important if your tile server uses an unusual DPI for the tiles.
    */
    useScales: false,

    /**
    * APIProperty: overrideDPI
    * {Boolean} Optional override to change the OpenLayers.DOTS_PER_INCH setting based 
    *     on the tile information in the server capabilities object.  This can be useful 
    *     if your server has a non-standard DPI setting on its tiles, and you're only using 
    *     tiles with that DPI.  This value is used while OpenLayers is calculating resolution
    *     using scales, and is not necessary if you have resolution information. (This is
    *     typically the case)  Regardless, this setting can be useful, but is dangerous
    *     because it will impact other layers while calculating resolution.  Only use this
    *     if you know what you are doing.  (See OpenLayers.Util.getResolutionFromScale)
    */
    overrideDPI: false,

    token: null,

    /**
    * Constructor: OpenLayers.Layer.ArcGISCacheToken 
    * Creates a new instance of this class 
    * 
    * Parameters: 
    * name - {String} 
    * url - {String} 
    * options - {Object} extra layer options
    */
    initialize: function (name, url, options) {
        OpenLayers.Layer.XYZ.prototype.initialize.apply(this, arguments);

        if (this.resolutions) {
            this.serverResolutions = this.resolutions;
            this.maxExtent = this.getMaxExtentForResolution(this.resolutions[0]);
        }

        // this block steps through translating the values from the server layer JSON 
        // capabilities object into values that we can use.  This is also a helpful
        // reference when configuring this layer directly.
        if (this.layerInfo) {
            // alias the object
            var info = this.layerInfo;

            // build our extents
            var startingTileExtent = new OpenLayers.Bounds(
                info.fullExtent.xmin,
                info.fullExtent.ymin,
                info.fullExtent.xmax,
                info.fullExtent.ymax
            );

            // set our projection based on the given spatial reference.
            // esri uses slightly different IDs, so this may not be comprehensive
            this.projection = 'EPSG:' + info.spatialReference.wkid;
            this.sphericalMercator = (info.spatialReference.wkid == 102100);

            // convert esri units into openlayers units (basic feet or meters only)
            this.units = (info.units == "esriFeet") ? 'ft' : 'm';

            // optional extended section based on whether or not the server returned
            // specific tile information
            if (!!info.tileInfo) {
                // either set the tiles based on rows/columns, or specific width/height
                this.tileSize = new OpenLayers.Size(
                    info.tileInfo.width || info.tileInfo.cols,
                    info.tileInfo.height || info.tileInfo.rows
                );

                // this must be set when manually configuring this layer
                this.tileOrigin = new OpenLayers.LonLat(
                    info.tileInfo.origin.x,
                    info.tileInfo.origin.y
                );

                var upperLeft = new OpenLayers.Geometry.Point(
                    startingTileExtent.left,
                    startingTileExtent.top
                );

                var bottomRight = new OpenLayers.Geometry.Point(
                    startingTileExtent.right,
                    startingTileExtent.bottom
                );

                if (this.useScales) {
                    this.scales = [];
                } else {
                    this.resolutions = [];
                }

                this.lods = [];
                // elemoine
                // change this loop, as it causes an "undefined" value
                // at the end of the resolutions array
                // http://andrewdupont.net/2006/05/18/javascript-associative-arrays-considered-harmful/
                //for (var key in info.tileInfo.lods) {
                //  var lod = info.tileInfo.lods[i];
                for (var i=0, l=info.tileInfo.lods.length; i<l; i++) {
                    var lod = info.tileInfo.lods[i];
                    if (this.useScales) {
                        this.scales.push(lod.scale);
                    } else {
                        this.resolutions.push(lod.resolution);
                    }

                    var start = this.getContainingTileCoords(upperLeft, lod.resolution);
                    lod.startTileCol = start.x;
                    lod.startTileRow = start.y;

                    var end = this.getContainingTileCoords(bottomRight, lod.resolution);
                    lod.endTileCol = end.x;
                    lod.endTileRow = end.y;
                    this.lods.push(lod);
                }

                this.maxExtent = this.calculateMaxExtentWithLOD(this.lods[0]);
                this.serverResolutions = this.resolutions;
                if (this.overrideDPI && info.tileInfo.dpi) {
                    // see comment above for 'overrideDPI'
                    OpenLayers.DOTS_PER_INCH = info.tileInfo.dpi;
                }
            }
        }
    },

    /** 
    * Method: getContainingTileCoords
    * Calculates the x/y pixel corresponding to the position of the tile
    *     that contains the given point and for the for the given resolution.
    * 
    * Parameters:
    * point - {<OpenLayers.Geometry.Point>} 
    * res - {Float} The resolution for which to compute the extent.
    * 
    * Returns: 
    * {<OpenLayers.Pixel>} The x/y pixel corresponding to the position 
    * of the upper left tile for the given resolution.
    */
    getContainingTileCoords: function (point, res) {
        return new OpenLayers.Pixel(
           Math.max(Math.floor((point.x - this.tileOrigin.lon) / (this.tileSize.w * res)), 0),
           Math.max(Math.floor((this.tileOrigin.lat - point.y) / (this.tileSize.h * res)), 0)
        );
    },

    /** 
    * Method: calculateMaxExtentWithLOD
    * Given a Level of Detail object from the server, this function
    *     calculates the actual max extent
    * 
    * Parameters: 
    * lod - {Object} a Level of Detail Object from the server capabilities object 
    representing a particular zoom level
    * 
    * Returns: 
    * {<OpenLayers.Bounds>} The actual extent of the tiles for the given zoom level
    */
    calculateMaxExtentWithLOD: function (lod) {
        // the max extent we're provided with just overlaps some tiles
        // our real extent is the bounds of all the tiles we touch

        var numTileCols = (lod.endTileCol - lod.startTileCol) + 1;
        var numTileRows = (lod.endTileRow - lod.startTileRow) + 1;

        var minX = this.tileOrigin.lon + (lod.startTileCol * this.tileSize.w * lod.resolution);
        var maxX = minX + (numTileCols * this.tileSize.w * lod.resolution);

        var maxY = this.tileOrigin.lat - (lod.startTileRow * this.tileSize.h * lod.resolution);
        var minY = maxY - (numTileRows * this.tileSize.h * lod.resolution);
        return new OpenLayers.Bounds(minX, minY, maxX, maxY);
    },

    /** 
    * Method: calculateMaxExtentWithExtent
    * Given a 'suggested' max extent from the server, this function uses
    *     information about the actual tile sizes to determine the actual
    *     extent of the layer.
    * 
    * Parameters: 
    * extent - {<OpenLayers.Bounds>} The 'suggested' extent for the layer
    * res - {Float} The resolution for which to compute the extent.
    * 
    * Returns: 
    * {<OpenLayers.Bounds>} The actual extent of the tiles for the given zoom level
    */
    calculateMaxExtentWithExtent: function (extent, res) {
        var upperLeft = new OpenLayers.Geometry.Point(extent.left, extent.top);
        var bottomRight = new OpenLayers.Geometry.Point(extent.right, extent.bottom);
        var start = this.getContainingTileCoords(upperLeft, res);
        var end = this.getContainingTileCoords(bottomRight, res);
        var lod = {
            resolution: res,
            startTileCol: start.x,
            startTileRow: start.y,
            endTileCol: end.x,
            endTileRow: end.y
        };
        return this.calculateMaxExtentWithLOD(lod);
    },

    /** 
    * Method: getUpperLeftTileCoord
    * Calculates the x/y pixel corresponding to the position 
    *     of the upper left tile for the given resolution.
    * 
    * Parameters: 
    * res - {Float} The resolution for which to compute the extent.
    * 
    * Returns: 
    * {<OpenLayers.Pixel>} The x/y pixel corresponding to the position 
    * of the upper left tile for the given resolution.
    */
    getUpperLeftTileCoord: function (res) {
        var upperLeft = new OpenLayers.Geometry.Point(
            this.maxExtent.left,
            this.maxExtent.top);
        return this.getContainingTileCoords(upperLeft, res);
    },

    /** 
    * Method: getLowerRightTileCoord
    * Calculates the x/y pixel corresponding to the position 
    *     of the lower right tile for the given resolution.
    *  
    * Parameters: 
    * res - {Float} The resolution for which to compute the extent.
    * 
    * Returns: 
    * {<OpenLayers.Pixel>} The x/y pixel corresponding to the position
    * of the lower right tile for the given resolution.
    */
    getLowerRightTileCoord: function (res) {
        var bottomRight = new OpenLayers.Geometry.Point(
            this.maxExtent.right,
            this.maxExtent.bottom);
        return this.getContainingTileCoords(bottomRight, res);
    },

    /** 
    * Method: getMaxExtentForResolution
    * Since the max extent of a set of tiles can change from zoom level
    *     to zoom level, we need to be able to calculate that max extent 
    *     for a given resolution.
    *
    * Parameters: 
    * res - {Float} The resolution for which to compute the extent.
    * 
    * Returns: 
    * {<OpenLayers.Bounds>} The extent for this resolution
    */
    getMaxExtentForResolution: function (res) {
        var start = this.getUpperLeftTileCoord(res);
        var end = this.getLowerRightTileCoord(res);

        var numTileCols = (end.x - start.x) + 1;
        var numTileRows = (end.y - start.y) + 1;

        var minX = this.tileOrigin.lon + (start.x * this.tileSize.w * res);
        var maxX = minX + (numTileCols * this.tileSize.w * res);

        var maxY = this.tileOrigin.lat - (start.y * this.tileSize.h * res);
        var minY = maxY - (numTileRows * this.tileSize.h * res);
        return new OpenLayers.Bounds(minX, minY, maxX, maxY);
    },

    /** 
    * APIMethod: clone 
    * Returns an exact clone of this OpenLayers.Layer.ArcGISCacheToken
    * 
    * Parameters: 
    * [obj] - {Object} optional object to assign the cloned instance to.
    *  
    * Returns: 
    * {<OpenLayers.Layer.ArcGISCacheToken>} clone of this instance 
    */
    clone: function (obj) {
        if (obj == null) {
            obj = new OpenLayers.Layer.ArcGISCacheToken(this.name, this.url, this.options);
        }
        return OpenLayers.Layer.XYZ.prototype.clone.apply(this, [obj]);
    },

    /**
    * Method: getMaxExtent
    * Get this layer's maximum extent.
    *
    * Returns:
    * {OpenLayers.Bounds}
    */
    getMaxExtent: function () {
        var resolution = this.map.getResolution();
        return this.maxExtent = this.getMaxExtentForResolution(resolution);
    },

    /**
    * Method: getTileOrigin
    * Determine the origin for aligning the grid of tiles.  
    *     The origin will be derived from the layer's <maxExtent> property. 
    *
    * Returns:
    * {<OpenLayers.LonLat>} The tile origin.
    */
    getTileOrigin: function () {
        var extent = this.getMaxExtent();
        return new OpenLayers.LonLat(extent.left, extent.bottom);
    },

    /**
    * Method: getURL
    * Determine the URL for a tile given the tile bounds.  This is should support
    *     urls that access tiles through an ArcGIS Server MapServer or directly through
    *     the hex folder structure using HTTP.  Just be sure to set the useArcGISServer
    *     property appropriately!  This is basically the same as 
    *     'OpenLayers.Layer.TMS.getURL',  but with the addition of hex addressing,
    *     and tile rounding.
    *
    * Parameters:
    * bounds - {<OpenLayers.Bounds>}
    *
    * Returns:
    * {String} The URL for a tile based on given bounds.
    */
    getURL: function (bounds) {
        var res = this.getResolution();

        // tile center
        var originTileX = (this.tileOrigin.lon + (res * this.tileSize.w / 2));
        var originTileY = (this.tileOrigin.lat - (res * this.tileSize.h / 2));

        var center = bounds.getCenterLonLat();
        var point = { x: center.lon, y: center.lat };
        var x = (Math.round(Math.abs((center.lon - originTileX) / (res * this.tileSize.w))));
        var y = (Math.round(Math.abs((originTileY - center.lat) / (res * this.tileSize.h))));
        var z = this.map.getZoom();

        // this prevents us from getting pink tiles (non-existant tiles)
        if (this.lods) {
            var lod = this.lods[this.map.getZoom()];
            if ((x < lod.startTileCol || x > lod.endTileCol)
                || (y < lod.startTileRow || y > lod.endTileRow)) {
                return null;
            }
        }
        else {
            var start = this.getUpperLeftTileCoord(res);
            var end = this.getLowerRightTileCoord(res);
            if ((x < start.x || x >= end.x)
                || (y < start.y || y >= end.y)) {
                return null;
            }
        }

        // Construct the url string
        var url = this.url;
        var s = '' + x + y + z;

        if (url instanceof Array) {
            url = this.selectUrl(s, url);
        }

        // Accessing tiles through ArcGIS Server uses a different path
        // structure than direct access via the folder structure.
        if (this.useArcGISServer) {
            // AGS MapServers have pretty url access to tiles
            url = url + '/tile/${z}/${y}/${x}';
            if (this.token) {
                url = url + "?token=" + this.token;
            }
        } else {
            // The tile images are stored using hex values on disk.
            x = 'C' + this.zeroPad(x, 8, 16);
            y = 'R' + this.zeroPad(y, 8, 16);
            z = 'L' + this.zeroPad(z, 2, 16);
            url = url + '/${z}/${y}/${x}.' + this.type;
        }

        // Write the values into our formatted url
        url = OpenLayers.String.format(url, { 'x': x, 'y': y, 'z': z });

        return url;
    },

    /**
    * Method: zeroPad
    * Create a zero padded string optionally with a radix for casting numbers.
    *
    * Parameters:
    * num - {Number} The number to be zero padded.
    * len - {Number} The length of the string to be returned.
    * radix - {Number} An integer between 2 and 36 specifying the base to use
    *     for representing numeric values.
    */
    zeroPad: function (num, len, radix) {
        var str = num.toString(radix || 10);
        while (str.length < len) {
            str = "0" + str;
        }
        return str;
    },

    CLASS_NAME: 'OpenLayers.Layer.ArcGISCacheToken'
}); 

