Search code examples
javascriptd3.jstile

D3-Tile Not All Tiles Load


I'm trying to reproduce this example with D3 v4 and d3-tile v0.0.4 (as noted in this question, there are some small adjustments to be made). For simplicity, I removed the raster vectors and set the window size to be 960x500.

At multiple spots of zooming and panning, the top left tile will not load. Any ideas as to why? Do I need to pick different window dimensions? I recommend expanding the code snippet in full screen to see the problem.

When I run basically the same xample but with d3-tile v0.0, the issue goes away, so something must have changed in the updates.

<!DOCTYPE html>
<meta charset="utf-8">
<style>
    body {
        margin: 0;
    }

    path {
        fill: none;
        stroke: red;
        stroke-linejoin: round;
        stroke-width: 1.5px;
    }
</style>
<svg></svg>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="https://unpkg.com/[email protected]/build/d3-tile.js"></script>
<script>
    var pi = Math.PI,
        tau = 2 * pi;

    var width = 960;
        height = 500;

    // Initialize the projection to fit the world in a 1×1 square centered at the origin.
    var projection = d3.geoMercator()
        .scale(1 / tau)
        .translate([0, 0]);

    var path = d3.geoPath()
        .projection(projection);

    var tile = d3.tile()
        .size([width, height]);

    var zoom = d3.zoom()
        .scaleExtent([1 << 11, 1 << 14])
        .on("zoom", zoomed);

    var svg = d3.select("svg")
        .attr("width", width)
        .attr("height", height);

    var raster = svg.append("g");

    // Compute the projected initial center.
    var center = projection([-98.5, 39.5]);

    // Apply a zoom transform equivalent to projection.{scale,translate,center}.
    svg
        .call(zoom)
        .call(zoom.transform, d3.zoomIdentity
            .translate(width / 2, height / 2)
            .scale(1 << 12)
            .translate(-center[0], -center[1]));

    function zoomed() {
        var transform = d3.event.transform;

        var tiles = tile
            .scale(transform.k)
            .translate([transform.x, transform.y])
            ();

        projection
            .scale(transform.k / tau)
            .translate([transform.x, transform.y]);

        var image = raster
            .attr("transform", stringify(tiles.scale, tiles.translate))
            .selectAll("image")
            .data(tiles, function(d) {
                return d;
            });

        image.exit().remove();

        image.enter().append("image")
            .attr('xlink:href', function(d) {
                return 'http://' + 'abc' [d.y % 3] + '.tile.openstreetmap.org/' +
                    d.z + '/' + d.x + '/' + d.y + '.png';
            })
            .attr('x', function(d) {
                return d.x * 256;
            })
            .attr('y', function(d) {
                return d.y * 256;
            })
            .attr("width", 256)
            .attr("height", 256);
    }

    function stringify(scale, translate) {
        var k = scale / 256,
            r = scale % 1 ? Number : Math.round;
        return "translate(" + r(translate[0] * scale) + "," + r(translate[1] * scale) + ") scale(" + k + ")";
    }
</script>


Solution

  • All tiles are drawn from what I can see. If you look at the DOM, there are the proper number of tiles, it is just that the first tile appears as though it never exits properly (as it should); nonetheless, it is assigned the new datum. However, as you don't use an update selection, it never updates remaining where it is first drawn. This results in either overlapping images if you scroll west, or a hidden image off screen if you scroll east.

    A solution, would be to use a complete exit/enter/merge/update cycle, but that leaves the unsatisfactory outcome of not knowing why the unexpected behavior occurred in the first place:

    <!DOCTYPE html>
    <meta charset="utf-8">
    <style>
        body {
            margin: 0;
        }
    
        path {
            fill: none;
            stroke: red;
            stroke-linejoin: round;
            stroke-width: 1.5px;
        }
    </style>
    <svg></svg>
    <script src="https://d3js.org/d3.v4.min.js"></script>
    <script src="https://unpkg.com/[email protected]/build/d3-tile.js"></script>
    <script>
        var pi = Math.PI,
            tau = 2 * pi;
    
        var width = 960;
            height = 500;
    
        // Initialize the projection to fit the world in a 1×1 square centered at the origin.
        var projection = d3.geoMercator()
            .scale(1 / tau)
            .translate([0, 0]);
    
        var path = d3.geoPath()
            .projection(projection);
    
        var tile = d3.tile()
            .size([width, height]);
    
        var zoom = d3.zoom()
            .scaleExtent([1 << 11, 1 << 14])
            .on("zoom", zoomed);
    
        var svg = d3.select("svg")
            .attr("width", width)
            .attr("height", height);
    
        var raster = svg.append("g");
    
        // Compute the projected initial center.
        var center = projection([-98.5, 39.5]);
    
        // Apply a zoom transform equivalent to projection.{scale,translate,center}.
        svg
            .call(zoom)
            .call(zoom.transform, d3.zoomIdentity
                .translate(width / 2, height / 2)
                .scale(1 << 12)
                .translate(-center[0], -center[1]));
    
        function zoomed() {
            var transform = d3.event.transform;
    
            var tiles = tile
                .scale(transform.k)
                .translate([transform.x, transform.y])
                ();
    
            projection
                .scale(transform.k / tau)
                .translate([transform.x, transform.y]);
    
            var image = raster
                .attr("transform", stringify(tiles.scale, tiles.translate))
                .selectAll("image")
                .data(tiles, function(d) {
                    return d;
                });
    
            image.exit().remove();
            // enter:
            var entered = image.enter().append("image");
            // update:
            image = entered.merge(image)
                .attr('xlink:href', function(d) {
                    return 'http://' + 'abc' [d.y % 3] + '.tile.openstreetmap.org/' +
                        d.z + '/' + d.x + '/' + d.y + '.png';
                })
                .attr('x', function(d) {
                    return d.x * 256;
                })
                .attr('y', function(d) {
                    return d.y * 256;
                })
                .attr("width", 256)
                .attr("height", 256);
        }
    
        function stringify(scale, translate) {
            var k = scale / 256,
                r = scale % 1 ? Number : Math.round;
            return "translate(" + r(translate[0] * scale) + "," + r(translate[1] * scale) + ") scale(" + k + ")";
        }
    </script>