user9379320
user9379320

Reputation:

D3 Geo map zoom is not working correctly (Canvas)

I am trying to zoom a map in canvas.

var projection = d3.geoMercator()
projection.fitExtent([[margin.left, margin.top], [width, height]], land);
var path = d3.geoPath().projection(projection).context(context);

When I apply the translate and scale in to the canvas context, it works perfectly. But it does not return the correct latitude and longitude when i call var latlong = projection.invert(d3.mouse(this)); since the projection is not translated accordingly.

var zoom = d3.zoom()
    .scaleExtent([1, Infinity])
    .on("zoom", zoomByContext);

function zoomByContext() {
    var transform = d3.event.transform;
    context.clearRect(0, 0, width, height);
    context.save();
    context.translate(transform.x, transform.y);
    context.lineWidth = 0.5 / transform.k;
    context.scale(transform.k, transform.k);
    renderFeature();
    context.restore();
}

So I tried reprojecting the projection like blow. But it goes to top left corner when I zoom with the below code.

var zoom = d3.zoom()
    .scaleExtent([1, Infinity])
    .on("zoom", zoomByProjection);

function zoomByProjection() {
    var transform = d3.event.transform;
    projection.translate([transform.x, transform.y]);
    projection.scale(scale * transform.k);
    renderFeature();
}

And I have called the zoom like this

canvas.call(zoom, d3.zoomIdentity
.translate(projection.translate())
.scale(projection.scale()));

Upvotes: 1

Views: 855

Answers (1)

Andrew Reid
Andrew Reid

Reputation: 38171

For the first approach you need to invert the zoom prior to inverting the xy coordinates into long lat coordinates:

var transform = d3.zoomTransform(this);
var xy = transform.invert(d3.mouse(this));         
var longlat = projection.invert(xy);

We get the mouse position in pixel coordinates, invert it to zoom coordinates, and then invert that to geographic coordinates.

This should demonstrate the above:

var width = 960;
var height = 500;

var canvas = d3.select("canvas");
var context = canvas.node().getContext("2d")
var projection = d3.geoMercator();
var path = d3.geoPath(projection,context);
	

d3.json("https://unpkg.com/world-atlas@1/world/110m.json", function(error, world) {
  if (error) throw error;
  
renderFeature();

var zoom = d3.zoom()
    .scaleExtent([1, Infinity])
    .on("zoom", zoomByContext);

canvas.call(zoom);
	
  function zoomByContext() {
    var transform = d3.event.transform;
    context.clearRect(0, 0, width, height);
    context.save();
    context.translate(transform.x, transform.y);
    context.lineWidth = 0.5 / transform.k;
    context.scale(transform.k, transform.k);
    renderFeature();
    context.restore();
  }
  function renderFeature() {
    context.beginPath();
    path(topojson.mesh(world));
    context.stroke();	
  }
  
  canvas.on("click", function() {
	var transform = d3.zoomTransform(this);
	var xy = transform.invert(d3.mouse(this));         
	var longlat = projection.invert(xy);
    console.log(longlat);
  })

  
  
});
<canvas width="960" height="500"></canvas>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="https://unpkg.com/topojson-client@3"></script>

The second approach is a little trickier, if the projection's translate is [0,0] the approach you have will work, however this is rarely the case. The default value is [480,250] (assumes a canvas that is 960x500), and fitSize and fitExtent don't position the feature by modifying rotate and center, but rather translate. So you need to account for the initial translate when modifying the projection (just as you have done with scale):

var transform = d3.event.transform;
projection.translate([transform.x+translate[0]*transform.k, transform.y+translate[1]*transform.k]);
projection.scale(scale * transform.k);

Here translate is an array holding the initial translate values

And here's a demonstration that should demonstrate the above:

var width = 960;
var height = 500;

var canvas = d3.select("canvas");
var context = canvas.node().getContext("2d")
var projection = d3.geoMercator().center([105,3]).scale(1200).translate([2000,0]);
var path = d3.geoPath(projection,context);
var scale = projection.scale();
var translate = projection.translate();



d3.json("https://unpkg.com/world-atlas@1/world/110m.json", function(error, world) {
  if (error) throw error;

renderFeature();
  
var zoom = d3.zoom()
    .scaleExtent([0.1, Infinity])
    .on("zoom", zoomByProjection);

canvas.call(zoom);
	
function zoomByProjection() {
    context.clearRect(0, 0, width, height);
    var transform = d3.event.transform;
    projection.translate([transform.x+translate[0]*transform.k, transform.y+translate[1]*transform.k]);
    projection.scale(scale * transform.k);
    renderFeature();
}
  
  function renderFeature() {
    context.beginPath();
    path(topojson.mesh(world));
    context.stroke();	
  }

});
<canvas width="960" height="500"></canvas>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="https://unpkg.com/topojson-client@3"></script>

Upvotes: 2

Related Questions