Index: lib/OpenLayers/Layer/Bing.js
===================================================================
--- lib/OpenLayers/Layer/Bing.js	(revision 0)
+++ lib/OpenLayers/Layer/Bing.js	(revision 0)
@@ -0,0 +1,216 @@
+/* Copyright (c) 2006-2010 by OpenLayers Contributors (see authors.txt for 
+ * full list of contributors). Published under the Clear BSD license.  
+ * See http://svn.openlayers.org/trunk/openlayers/license.txt for the
+ * full text of the license. */
+
+/**
+ * @requires OpenLayers/Layer/XYZ.js
+ */
+
+/** 
+ * Class: OpenLayers.Layer.Bing
+ * Bing layer using direct tile access as provided by Bing Maps REST Services.
+ * See http://msdn.microsoft.com/en-us/library/ff701713.aspx for more
+ * information.
+ * 
+ * Inherits from:
+ *  - <OpenLayers.Layer.XYZ>
+ */
+OpenLayers.Layer.Bing = OpenLayers.Class(OpenLayers.Layer.XYZ, {
+
+    /**
+     * Property: sphericalMercator
+     * {Boolean} always true for this layer type
+     */
+    sphericalMercator: true,
+    
+    /**
+     * Property: metadata
+     * {Object} Metadata for this layer, as returned by the callback script
+     */
+    metadata: null,
+
+    /**
+     * Constructor: OpenLayers.Layer.Bing
+     * Create a new Bing layer.
+     *
+     * Example:
+     * (code)
+     * var road = new OpenLayers.Layer.Bing({
+     *     name: "My Bing Aerial Layer",
+     *     layer: "Aerial",
+     *     key: "my-api-key-here",
+     * });
+     * (end)
+     *
+     * Parameters:
+     * config - {Object} Configuration properties for the layer.
+     *
+     * Required configuration properties:
+     * key - {String} Bing Maps API key for your application. Get one at
+     *     http://bingmapsportal.com/.
+     * layer - {String} The layer identifier.  Any non-birdseye imageryType
+     *     from http://msdn.microsoft.com/en-us/library/ff701716.aspx can be
+     *     used.
+     *
+     * Any other documented layer properties can be provided in the config object.
+     */
+    initialize: function(options) {
+        var name = options.name || "Bing " + options.layer;
+        var newArgs = [name, null, options];
+        OpenLayers.Layer.XYZ.prototype.initialize.apply(this, newArgs);
+        this.loadMetadata(options.layer); 
+    },
+
+    /**
+     * Method: loadMetadata
+     *
+     * Parameters:
+     * imageryType - {String}
+     */
+    loadMetadata: function(imageryType) {
+        this._callbackId = "_callback_" + this.id.replace(/\./g, "_");
+        // link the processMetadata method to the global scope and bind it
+        // to this instance
+        window[this._callbackId] = OpenLayers.Function.bind(
+            OpenLayers.Layer.Bing.processMetadata, this
+        );
+        var url = "http://dev.virtualearth.net/REST/v1/Imagery/Metadata/" +
+            imageryType + "?key=" + this.key + "&jsonp=" + this._callbackId +
+            "&include=ImageryProviders";
+        var script = document.createElement("script");
+        script.type = "text/javascript";
+        script.src = url;
+        script.id = this._callbackId;
+        document.getElementsByTagName("head")[0].appendChild(script);
+    },
+    
+    /**
+     * Method: initLayer
+     *
+     * Parameters:
+     * metadata - {Object} JSON response from the API
+     *
+     * Sets layer properties according to the metadata provided by the API
+     */
+    initLayer: function(metadata) {
+        this.metadata = metadata;
+        var res = metadata.resourceSets[0].resources[0];
+        var url = res.imageUrl.replace("{quadkey}", "${quadkey}");
+        this.url = [];
+        for (var i=0; i<res.imageUrlSubdomains.length; ++i) {
+            this.url.push(url.replace("{subdomain}", res.imageUrlSubdomains[i]));
+        };
+        
+        this.addOptions({
+            maxResolution: this.resolutions[res.zoomMin],
+            numZoomLevels: res.zoomMax - res.zoomMin,
+            zoomOffset: res.zoomMin
+        });
+        this.redraw();
+        this.updateAttribution();
+    },
+    
+    /**
+     * Method: getURL
+     *
+     * Paramters:
+     * bounds - {<OpenLayers.Bounds>}
+     */
+    getURL: function(bounds) {
+        if (!this.url) {
+            return OpenLayers.Util.getImagesLocation() + "blank.gif";
+        }
+        var xyz = this.getXYZ(bounds), x = xyz.x, y = xyz.y, z = xyz.z;
+        var quadDigits = [];
+        for (var i = z; i > 0; --i) {
+            var digit = '0';
+            var mask = 1 << (i - 1);
+            if ((x & mask) != 0) {
+                digit++;
+            }
+            if ((y & mask) != 0) {
+                digit++;
+                digit++;
+            }
+            quadDigits.push(digit);
+        }
+        var quadKey = quadDigits.join("");
+        var url = this.selectUrl('' + x + y + z, this.url);
+
+        return OpenLayers.String.format(url, {'quadkey': quadKey});
+    },
+    
+    /**
+     * Method: updateAttribution
+     * Updates the attribution according to the requirements outlined in
+     * http://gis.638310.n2.nabble.com/Bing-imagery-td5789168.html
+     */
+    updateAttribution: function() {
+        var metadata = this.metadata;
+        if (!metadata || !this.map) {
+            return;
+        }
+        var res = metadata.resourceSets[0].resources[0];
+        var extent = this.map.getExtent().transform(
+            this.map.getProjectionObject(),
+            new OpenLayers.Projection("EPSG:4326")
+        );
+        var providers = res.imageryProviders, zoom = this.map.getZoom(),
+            copyrights = " ", provider, i, ii, j, jj, bbox, coverage;
+        for (i=0,ii=providers.length; i<ii; ++i) {
+            provider = providers[i];
+            for (j=0,jj=provider.coverageAreas.length; j<jj; ++j) {
+                coverage = provider.coverageAreas[j];
+                bbox = OpenLayers.Bounds.fromArray(coverage.bbox);
+                if (extent.intersectsBounds(bbox) &&
+                        zoom <= coverage.zoomMax && zoom >= coverage.zoomMin) {
+                    copyrights += provider.attribution + " ";
+                }
+            }
+        }
+        this.attribution = '<span>' +
+            '<a target="_blank" href="http://www.bing.com/maps/"><img src="' +
+            metadata.brandLogoUri +
+            '" style="vertical-align: middle"></img></a>' + copyrights +
+            '<a target="_blank" '+
+            'href="http://www.microsoft.com/maps/product/terms.html">' +
+            'Terms Of Use</a></span>';
+        this.map && this.map.events.triggerEvent("changelayer", {layer: this});
+    },
+    
+    /**
+     * Method: setMap
+     */
+    setMap: function() {
+        OpenLayers.Layer.XYZ.prototype.setMap.apply(this, arguments);
+        this.updateAttribution();
+        this.map.events.register("moveend", this, this.updateAttribution);
+    },
+    
+    /**
+     * Method: destroy
+     */
+    destroy: function() {
+        this.map.events.unregister("moveend", this, this.updateAttribution);
+        OpenLayers.Layer.XYZ.prototype.destroy.apply(this, arguments);
+    },
+    
+    CLASS_NAME: "OpenLayers.Layer.Bing"
+});
+
+/**
+ * Function: OpenLayers.Layer.Bing.processMetadata
+ * This function will be bound to an instance, linked to the global scope with
+ * an id, and called by the JSONP script returned by the API.
+ *
+ * Parameters:
+ * metadata - {Object} metadata as returned by the API
+ */
+OpenLayers.Layer.Bing.processMetadata = function(metadata) {
+    this.initLayer(metadata);
+    var script = document.getElementById(this._callbackId);
+    script.parentNode.removeChild(script);
+    window[this._callbackId] = undefined; // cannot delete from window in IE
+    delete this._callbackId;
+};
Index: lib/OpenLayers/Layer/XYZ.js
===================================================================
--- lib/OpenLayers/Layer/XYZ.js	(revision 10937)
+++ lib/OpenLayers/Layer/XYZ.js	(working copy)
@@ -107,7 +107,7 @@
     },    
 
     /**
-     * Method: getUrl
+     * Method: getURL
      *
      * Parameters:
      * bounds - {<OpenLayers.Bounds>}
@@ -118,6 +118,27 @@
      *          parameters
      */
     getURL: function (bounds) {
+        var xyz = this.getXYZ(bounds);
+        var url = this.url;
+        if (url instanceof Array) {
+            var s = '' + xyz.x + xyz.y + xyz.z;
+            url = this.selectUrl(s, url);
+        }
+        
+        return OpenLayers.String.format(url, xyz);
+    },
+    
+    /**
+     * Method: getXYZ
+     * Calculates x, y and z for the given bounds.
+     *
+     * Parameters:
+     * bounds - {<OpenLayers.Bounds>}
+     *
+     * Returns:
+     * {Object} - an object with x, y and z properties.
+     */
+    getXYZ: function(bounds) {
         var res = this.map.getResolution();
         var x = Math.round((bounds.left - this.maxExtent.left) 
             / (res * this.tileSize.w));
@@ -127,16 +148,7 @@
             OpenLayers.Util.indexOf(this.serverResolutions, res) :
             this.map.getZoom() + this.zoomOffset;
 
-        var url = this.url;
-        var s = '' + x + y + z;
-        if (url instanceof Array)
-        {
-            url = this.selectUrl(s, url);
-        }
-        
-        var path = OpenLayers.String.format(url, {'x': x, 'y': y, 'z': z});
-
-        return path;
+        return {'x': x, 'y': y, 'z': z};
     },
     
     /**
@@ -204,4 +216,4 @@
          return obj;
      },
      CLASS_NAME: "OpenLayers.Layer.OSM"
-});
+});
\ No newline at end of file
Index: lib/OpenLayers.js
===================================================================
--- lib/OpenLayers.js	(revision 10937)
+++ lib/OpenLayers.js	(working copy)
@@ -127,6 +127,7 @@
             "OpenLayers/Layer/GeoRSS.js",
             "OpenLayers/Layer/Boxes.js",
             "OpenLayers/Layer/XYZ.js",
+            "OpenLayers/Layer/Bing.js",
             "OpenLayers/Layer/TMS.js",
             "OpenLayers/Layer/TileCache.js",
             "OpenLayers/Layer/Zoomify.js",
Index: examples/bing-tiles.html
===================================================================
--- examples/bing-tiles.html	(revision 0)
+++ examples/bing-tiles.html	(revision 0)
@@ -0,0 +1,34 @@
+<html xmlns="http://www.w3.org/1999/xhtml">
+  <head>
+    <title>OpenLayers Bing Tiles Example</title>
+    <link rel="stylesheet" href="../theme/default/style.css" type="text/css" />
+    <link rel="stylesheet" href="style.css" type="text/css" />
+    <style type="text/css">
+    .olControlAttribution {
+        left: 5px;
+        right: inherit;
+        bottom: 5px; 
+    }
+    </style>
+  </head>
+  <body onload="init()">
+    <h1 id="title">Basic Bing Tiles Example</h1>
+
+    <div id="tags">
+        bing tiles
+    </div>
+
+    <div id="shortdesc">Use Bing with direct tile access</div>
+
+    <div id="map" class="smallmap"></div>
+
+    <div id="docs">
+        <p>This example shows a very simple map with Bing layers that use
+        direct tile access through Bing Maps REST Services.</p><p>See
+        <a target="_blank" href="bing-tiles.js">bing-tiles.js</a> for the
+        source code.</p>
+    </div>
+    <script src="../lib/OpenLayers.js"></script>
+    <script src="bing-tiles.js"></script>
+  </body>
+</html>
Index: examples/bing-tiles.js
===================================================================
--- examples/bing-tiles.js	(revision 0)
+++ examples/bing-tiles.js	(revision 0)
@@ -0,0 +1,25 @@
+// API key for http://openlayers.org - replace with your own.
+var apiKey = "AqTGBsziZHIJYYxgivLBf0hVdrAk9mWO5cQcb8Yux8sW5M8c8opEC2lZqKR1ZZXf";
+
+var map = new OpenLayers.Map( 'map');
+
+var road = new OpenLayers.Layer.Bing({
+    key: apiKey,
+    layer: "Road"
+});
+var aerial = new OpenLayers.Layer.Bing({
+    key: apiKey,
+    layer: "Aerial"
+});
+var hybrid = new OpenLayers.Layer.Bing({
+    key: apiKey,
+    layer: "AerialWithLabels",
+    name: "Bing Aerial With Labels"
+});
+
+map.addLayers([road, aerial, hybrid]);
+map.addControl(new OpenLayers.Control.LayerSwitcher());
+map.setCenter(new OpenLayers.LonLat(-71.147, 42.472).transform(
+    new OpenLayers.Projection("EPSG:4326"),
+    map.getProjectionObject()
+), 12);    
