Reputation: 346
I am trying to create a tile map with d3 tile
and d3 zoom
. I have set both the zoom.extent
and zoom.translateExtent
to [[0,0], [width,height]]
. but when I zoom, the map jumps to the left top corner.
Here is the sample code
const tile = d3.tile()
.extent([[0, 0], [width, height]])
.tileSize(512);
const zoom = d3.zoom()
.translateExtent([[0, 0], [width, height]]) // issue in this line
.extent([[0, 0], [width, height]])
.scaleExtent([1 << 10, 1 << 15])
.on("zoom", () => zoomed(d3.event.transform));
// applied zoom like this
svg
.call(zoom)
.call(zoom.transform, d3.zoomIdentity
.translate(width / 2, height / 2)
.scale(-initialScale)
.translate(...projection(initialCenter))
.scale(-1));
function zoomed(transform) {
projection
.scale(transform.k / (2 * Math.PI))
.translate([transform.x, transform.y]);
}
Here is the complete demo in Observable
Upvotes: 1
Views: 1205
Reputation: 38171
The key challenge here is understanding what you are constraining. The original scale of the projection is 1/(2π): world is projected to a space one pixel by one pixel (2π per pixel or 360 degrees per pixel). This allows for easier integration with d3-zoom, but is certainly not intuitive.
D3 zoom is used to modify the projection and has a scale value of along the lines of 1<<n
. This represents the width of the world after the zoom is applied. If the base projection uses a width of 1 pixel for the world, the zoom stretches it across 1<<n
pixels. 1<<10 being 1024 for example.
Now, the tricky part is that the zoom translate extent is applied at a zoom scale of 1. So, we need to apply the translate extent to the world as if it was shown within a 1x1 pixel box, even though, given a lower scale extent value of 1024 (1<<10), we never actually show the world as occupying one pixel.
So, our translate extent should be within a box bounded by the two corners: [-1,-1] and [1,1]. This helps explain the comment by Cornel. But can we do this programmatically? Yes.
This is what drove me bonkers about this library: the zoom extent is not registered in something straightforward such as geographic coordinates, or screen coordinates, but zoom units relative to a 1x1 pixel world, introducing a new coordinate system to keep track of which could instead be tracked behind the scenes. This is certainly not intuitive and partially drove me to leave this module behind (explaining the link in my comment, d3-slippy is still pretty experimental, but should show what could be done - btw, the constrain method in the link would only work if you convert everything over to it).
So, let's programmatically get the zoom translate extent of the screen as shown.
First, after we call the zoom and and apply the initial transform, the projection is updated to reflect the new zoom level and translate, so we can use projection.invert() to get the geographic coordinates of the top left and bottom right of the screen.
Second, we can (re)create a projection for our one pixel world and push the geographic coordinates of the top left and bottom right through it to get their projected coordinates within a one pixel world.
Third, we take the top left and bottom right projected coordinates within a 1 pixel world and we pass them to zoom.translateExtent:
// Call zoom, same as before:
svg
.call(zoom)
.call(zoom.transform, d3.zoomIdentity
.translate(width / 2, height / 2)
.scale(-initialScale)
.translate(...projection(initialCenter))
.scale(-1));
// Now we can work on setting the zoom translate extent:
// Zooomed projected limits of screen in lat long:
var top = projection.invert([0,0])[1];
var left = projection.invert([0,0])[0];
var bottom = projection.invert([width,height])[1];
var right = projection.invert([width,height])[0];
// original projection:
var baseProjection = d3.geoMercator()
.translate([0,0])
.scale(1/Math.PI/2);
// Projected limits of screen in projected unzoomed coordinates:
var topLeft = baseProjection([left,top])
var bottomRight = baseProjection([right,bottom])
// Set the zoom translate extent:
zoom.translateExtent([topLeft,bottomRight])
Upvotes: 2