2

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?

Noobster
  • 1,024
  • 1
  • 13
  • 28
  • 1
    I'm not a leaflets user, but why don't you use D3 + leaflets? http://www.somebits.com/rivers/rivers-d3leaflet.html#8/37.959/-120.976 – Gerardo Furtado May 31 '18 at 04:30
  • Hello Gerardo. This implementation is hardcore complicated! Won't I need to set up a tiling server, etc? – Noobster May 31 '18 at 04:37
  • 1
    I have no idea. As I said, I'm not a leaflets user. – Gerardo Furtado May 31 '18 at 05:42
  • No worries. This implementation is beautiful, incidentally. Thanks for showing this to me. – Noobster May 31 '18 at 05:45
  • Leaflet certainly handles tile services better, but those same services *should* work with d3-tile, [this page](http://leaflet-extras.github.io/leaflet-providers/preview/) provides an excellent overview of some of the tile layers available. – Andrew Reid May 31 '18 at 21:05

2 Answers2

2

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/d3-tile@0.0.4/build/d3-tile.js"></script>
<script src="https://unpkg.com/topojson-client@3"></script>
Andrew Reid
  • 37,021
  • 7
  • 64
  • 83
  • Thanks Andrew - d3.geo.tiles is the answer. I think I've had to give up the idea of creating my custom tiles. The problem with this solution is that an internet creation is needed at all times unless the tiles are cached. – Noobster Jun 01 '18 at 04:13
  • 1
    Yes, it is a downside. Tile creation would not be too difficult if using the original data from natural earth, if desired I could go through those steps in a new answer – Andrew Reid Jun 01 '18 at 04:19
  • Andrew, that would be a great idea. I don't have an immediate need for implementation but I would be eager to learn on this one and this would definitely be helpful for the community. I am also working on your script above and raised a new issue at the following: https://stackoverflow.com/questions/50638284/d3-zoom-to-bounding-box-with-d3-tiles – Noobster Jun 01 '18 at 09:48
0

You could certainly use OpenSeadragon for this. You'd want to turn the image into tiles; you don't need a specialized server for it... there are a number of standalone scripts you can use:

http://openseadragon.github.io/examples/creating-zooming-images/

Once you have that, OpenSeadragon handles the zooming and panning for you.

To overlay SVG so that it matches the zooming and panning, use the SVG overlay plugin:

https://github.com/openseadragon/svg-overlay

It works great with SVG produced by D3.

One thing to be aware of is that OpenSeadragon does not have any geo-specific functionality, so you'll position the overlay in image pixels rather than latitude/longitude.

BTW, OpenSeadragon can also work with non-tiled images, so if you want to give it a test before tiling your image, that's no problem. You'll just want to tile your image before production so you're not sending 20mb to your users.

iangilman
  • 2,144
  • 1
  • 13
  • 16