Search code examples
javascriptd3.jsopenseadragon

D3 Implementation: custom topology image behind D3 map


I need to display a D3 map with a topological / shaded relief background. All user functionalities need to be implemented (e.g. zoom and panning)

So far, I have layered the map over a PNG that has the topology. I then did some hacking around with the projection to align the PNG border with the map borders. I then allow the user to zoom the PNG (eg: http://bl.ocks.org/pbogden/7363519). The result is actually very good. When I pan and zoom the map moves with the PNG which is great (image below):

enter image description here

The problem is that the PNG is very heavy (20MB), and the whole resulting experience is seriously buggy to the point that is is unusable. Results are obviously use a lower resolution image, but then the topology looks crap when the user zooms in. I tried converting the PNG to JPG ... which was actually worse!

What would be the best solution to achieve my goal in D3? Initial thoughts are as follows:

(1) The d3.geo.tile plugin (http://bl.ocks.org/mbostock/4132797). The difficulty here is that I would need to create my own tiles from my PNG image. Is this a promising avenue? Would I be able to layer a D3 map on top of that? I cannot find an example with custom tiles.

(2) I've seen this successful implementation of OpenSeaDragon and D3 (http://bl.ocks.org/zloysmiertniy/0ab009ca832e7e0518e585bfa9a7ad59). The issue here is that I am not sure whether it'll be possible to implement the desired D3 functionalities (zoom, pan, transitions) such that the D3 map and the underlying image move simultaneously.

(3) Any other thoughts or ideas?


Solution

  • To turn an image into tiles you'll need to have a georeferenced image - or be able to georeference the image yourself. As I believe you are using a natural earth dataset to create this image, you could use the source tif file and work with this. I use tile mill generally for my tiles (with some python) and it is fairly straightforward. You would not be able to use your png as is for tiles.

    However, creating at tile set is unnecessary if you are looking for a hillshade or some sort of elevation/terrain texture indication. Using a leaflet example here, you can find quite a few tile providers, the ESRI.WorldShadedRelieve looks likes it fits the bill. Here's a demo with it pulled into d3 with a topojson feature drawn ontop:

    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");
        var vector = svg.append("g");
    
        // Compute the projected initial center.
        var center = projection([-98.5, 39.5]);
    
        d3.json("https://unpkg.com/world-atlas@1/world/110m.json",function(error,data) {
        
         vector.append("path")
           .datum(topojson.feature(data,data.objects.land))
           .attr("stroke","black")
           .attr("stroke-width",2)
           .attr("fill","none")
           .attr("d",path)
    
        // 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 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Shaded_Relief/MapServer/tile/' + d.z + '/' + d.y + '/' + d.x + '.png';
                })
                .attr('x', function(d) {
                    return d.x * 256;
                })
                .attr('y', function(d) {
                    return d.y * 256;
                })
                .attr("width", 256)
                .attr("height", 256);
                
          vector.selectAll("path")
            .attr("transform", "translate(" + [transform.x, transform.y] + ")scale(" + transform.k + ")")
            .style("stroke-width", 1 / transform.k);
        }
    
        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 + ")";
        }
    body {    margin: 0;  }
    <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 src="https://unpkg.com/topojson-client@3"></script>