iD.Background = function() {
    var tile = d3.geo.tile(),
        projection,
        cache = {},
        transformProp = iD.util.prefixCSSProperty('Transform'),
        source = d3.functor('');

    function atZoom(t, distance) {
        var power = Math.pow(2, distance);
        var az = [
            Math.floor(t[0] * power),
            Math.floor(t[1] * power),
            t[2] + distance];
        az.push(source(az));
        return az;
    }

    function upZoom(t, distance) {
        var az = atZoom(t, distance), tiles = [];
        for (var x = 0; x < 2; x++) {
            for (var y = 0; y < 2; y++) {
                var up = [az[0] + x, az[1] + y, az[2]];
                up.push(source(up));
                tiles.push(up);
            }
        }
        return tiles;
    }

    // derive the tiles onscreen, remove those offscreen and position tiles
    // correctly for the currentstate of `projection`
    function background() {
        var tiles = tile
            .scale(projection.scale())
            .scaleExtent(source.scaleExtent || [1, 17])
            .translate(projection.translate())(),
            scaleExtent = tile.scaleExtent(),
            z = Math.max(Math.log(projection.scale()) / Math.log(2) - 8, 0),
            rz = Math.max(scaleExtent[0], Math.min(scaleExtent[1], Math.floor(z))),
            ts = 256 * Math.pow(2, z - rz),
            tile_origin = [
                projection.scale() / 2 - projection.translate()[0],
                projection.scale() / 2 - projection.translate()[1]],
            ups = {};

        tiles.forEach(function(d) {
            d.push(source(d));
            // this tile has not been loaded yet
            if (!cache[d] &&
                cache[atZoom(d, -1)] &&
                !ups[atZoom(d, -1)]) {
                ups[atZoom(d, -1)] = true;
                tiles.push(atZoom(d, -1));
            } else if (!cache[d]) {
                upZoom(d, 1).forEach(function(u) {
                    if (cache[u] && !ups[u]) {
                        ups[u] = true;
                        tiles.push(u);
                    }
                });
            }
        });

        var image = this
            .selectAll('img')
            .data(tiles, function(d) { return d; });

        image.exit().remove();

        function load(d) {
            cache[d] = true;
            d3.select(this).on('load', null);
        }

        function error() {
            d3.select(this).remove();
        }

        image.enter().append('img')
            .attr('class', 'tile')
            .attr('src', function(d) { return d[3]; })
            .on('error', error)
            .on('load', load);

        function tileSize(d) {
            return Math.ceil(256 * Math.pow(2, z - d[2])) / 256;
        }

        image.style(transformProp, function(d) {
            var _ts = 256 * Math.pow(2, z - d[2]);
            var scale = tileSize(d);
            return 'translate(' +
                Math.round((d[0] * _ts) - tile_origin[0]) + 'px,' +
                Math.round((d[1] * _ts) - tile_origin[1]) + 'px) scale(' + scale + ',' + scale + ')';
        });

        if (Object.keys(cache).length > 100) cache = {};
    }

    background.projection = function(_) {
        if (!arguments.length) return projection;
        projection = _;
        return background;
    };

    background.size = function(_) {
        if (!arguments.length) return tile.size();
        tile.size(_);
        return background;
    };

    background.source = function(_) {
        if (!arguments.length) return source;
        source = _;
        return background;
    };

    return background;
};
