Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

D3 + Leaflet: d3.geo.path() resampling

We've adapted Mike Bostock's original D3 + Leaflet example: http://bost.ocks.org/mike/leaflet/ so that it does not redraw all paths on each zoom in Leaflet.

Our code is here: https://github.com/madeincluj/Leaflet.D3/blob/master/js/leaflet.d3.js

Specifically, the projection from geographical coordinates to pixels happens here: https://github.com/madeincluj/Leaflet.D3/blob/master/js/leaflet.d3.js#L30-L35

We draw the SVG paths on the first load, then simply scale/translate the SVG to match the map.

This works very well, except for one issue: D3's path resampling, which looks great at the first zoom level, but looks progressively more broken once you start zooming in.

Is there a way to disable the resampling?

As to why we're doing this: We want to draw a lot of shapes (thousands) and redrawing them all on each zoom is impractical.

Edit After some digging, seems that resampling happens here:

function d3_geo_pathProjectStream(project) {
   var resample = d3_geo_resample(function(x, y) {
     return project([ x * d3_degrees, y * d3_degrees ]);
   });
  return function(stream) {
    return d3_geo_projectionRadians(resample(stream));
  };
}

Is there a way to skip the resampling step?

Edit 2

What a red herring! We had switched back and forth between sending a raw function to d3.geo.path().projection and a d3.geo.transform object, to no avail.

But in fact the problem is with leaflet's latLngToLayerPoint, which (obviously!) rounds point.x & point.y to integers. Which means that the more zoomed out you are when you initialize the SVG rendering, the more precision you will lose.

The solution is to use a custom function like this:

function latLngToPoint(latlng) {
  return map.project(latlng)._subtract(map.getPixelOrigin());
};

var t = d3.geo.transform({
    point: function(x, y) {
      var point = latLngToPoint(new L.LatLng(y, x));
      return this.stream.point(point.x, point.y);
    }
  });

this.path = d3.geo.path().projection(t);

It's similar to leaflet's own latLngToLayerPoint, but without the rounding. (Note that map.getPixelOrigin() is rounded as well, so probably you'll need to rewrite it)

You learn something every day, don't you.

like image 990
Dan Burzo Avatar asked Oct 29 '13 13:10

Dan Burzo


1 Answers

Coincidentally, I updated the tutorial recently to use the new d3.geo.transform feature, which makes it easy to implement a custom geometric transform. In this case the transform uses Leaflet’s built-in projection without any of D3’s advanced cartographic features, thus disabling adaptive resampling.

The new implementation looks like this:

var transform = d3.geo.transform({point: projectPoint}),
    path = d3.geo.path().projection(transform);

function projectPoint(x, y) {
  var point = map.latLngToLayerPoint(new L.LatLng(y, x));
  this.stream.point(point.x, point.y);
}

As before, you can continue to pass a raw projection function to d3.geo.path, but you’ll get adaptive resampling and antimeridian cutting automatically. So to disable those features, you need to define a custom projection, and d3.geo.transform is an easy way to do this for simple point-based transformations.

like image 97
mbostock Avatar answered Sep 24 '22 10:09

mbostock