Using the d3.geo.tile plugin, examples all have a difference between the scale of the map projection and the scale passed to the tile() function. In particular, tile_scale = projection_scale * 2 * Math.PI
. See examples here and here.
What's the meaning of this 2 * PI multiplication?
To understand why there is an additional factor of 2π applied to the scales when it comes to using the d3.geo.tile plugin, it is important to know the difference between the projection used by d3.geoMercator()
and the one used when plotting the raster tiles.
D3
D3 implements the standard Mercator projection using the formulae:
This projection is using the unit circle's radius of R = 1 centering on the Greenwich meridian. Up to this point there is no scaling nor any translation applied. The projection will produce output values from the following intervals:
In D3 v4 this is implemented as
export function mercatorRaw(lambda, phi) {
return [lambda, log(tan((halfPi + phi) / 2))];
}
D3 v3 used a slightly different notation which more closely resembles the above mentioned formulae while, of course, being equivalent to the newer implementation:
function d3_geo_mercator(λ, φ) {
return [λ, Math.log(Math.tan(π / 4 + φ / 2))];
}
The intervals' values are often obscured by the fact that d3.geo.mercator()
uses default values for translation and scaling. You have to explicitely set those to .translate([0,0]).scale(1)
to get the above mentioned intervals.
Tiles
Tiles on the other hand use a slightly modified version of the Mercator projection known as Web Mercator. The relevant formulae are
This projection, at its core, uses the same calculations as the standard Mercator projection. This can easily be seen from the terms colored in black on the right sides of the above equations, which are exactly the same as those used by the Mercator projection. Just the output intervals are adjusted to better suit the needs of screen display by applying some scaling and translation.
First of all, the projection is cut off for latitudes both north and south exceeding
Thereby projecting the entire map onto a square. Afterwards, there are three adjustments (matched by color in the above equations):
- Move the origin to the upper left corner (0, 0) — adjustment by π in blue.
- Normalize the intervals to [0, 1] — divide by 2π in green
- Scale up to fit an area of 256 × 256 — multiply by 256 in red.
This will yield output values from the intervals
It's this difference in scales of the projections involved, which is the reason why you need to multiply / divide by 2π. If you want to use both d3.geo.mercator()
as well as the tiles in the same map, you need to modify the scales to match each other.
Imposing the same restrictions for latitude on the standard Mercator projection as is the case for the Web Mercator projection you end up with the following intervals:
Standard Mercator Projection
Projects to the intervals
having length = 2π.
Web Mercator Projection
Projects to the intervals
having length = 256.
This is corrected when setting the projection's scale like
var projection = d3.geoMercator()
.scale((1 << 12) / 2 / Math.PI );
Using simple exponentiation math the scaling factor can be rewritten as
(1 << 4) * (1 << 8) / 2 / Math.PI
and further
(1 << 4) * 256 / ( 2 * Math.PI )
The first term (1 << 4)
equals 24 which is the actual zoom factor to use: 16. This will get tiles of zoom level 4. The next term
is the correction factor to adjust the scales which includes the 2π your question was all about.
Some of the d3
projections, including d3.geo.mercator
(used in these examples) have a natural scale with longitude ranging from -π
to π
:
var s = d3.geo.mercator().translate([0,0]).scale(1);
s([180, 0]); // [3.141592653589793, 0]
s([-180, 0]); // [-3.141592653589793, 0]
This is an easy default scale, because as you can see in the d3.geo.mercator
projection (Wikipedia formula), the projected x
value scales linearly with the longitude angle. This makes perfect sense if you think about the Web Mercator projection as unrolling and flattening a perfect sphere - 1 degree around the sphere's circumference is going to equal 1 unit of x
distance. So the D3 implementation doesn't scale longitude at all - it just returns it as the x
value (see code). The longitude is input to this function in radians, so it has a natural range from -π
to π
, i.e. a 2π
angle describing the full circumference of a circle.
But the main point is that the scale factors for each scale are effectively arbitrary, and not consistent across projections.
So basically, 2 * Math.PI
represents the max range of the scale, and dividing by this factor means normalizing this range to 1
for easier math.