add geotranslate method to projections and tests #330

Closed
wants to merge 1 commit into
from

Projects

None yet

3 participants

@larskotthoff

Geotranslate works like translate but takes projection coordinates as input. It
is also scale independent, i.e. there is no need to adjust geotranslate after
changing the scale. The primary use of this function is to move the map to a
specific place.

Unfortunately this introduces some repeated code to maintain the translation internally independent of the scale. I didn't find a good way of avoiding this though and would be grateful for any pointers.

@larskotthoff larskotthoff add geotranslate method to projections and tests
Geotranslate works like translate but takes projection coordinates as input. It
is also scale independent, i.e. there is no need to adjust geotranslate after
changing the scale. The primary use of this function is to move the map to a
specific place.
f2cbc01
@mbostock
D3 member

Can you give me a code example of how you could use this?

@larskotthoff

Sure. To display a world map that always starts at the top left corner, you would do

proj.geotranslate([-180, 90]);

You could do the same for a country by extracting the min x and the max y coordinates and doing

proj.geotranslate([xmin, ymax]);

Or you could center a feature with

proj.geotranslate([x, y]);
proj.translate([width/2, height/2]);
@jasondavies
D3 member

This sounds similar to the d3.geo.rotate idea in #318, which performs an arbitrary number of rotations on a set of input coordinates about the x-, y- or z- axes (assuming spherical coordinates). The difference is that no modifications are needed to individual projections (and it supports an additional rotation axis).

So if you want a projection to centre on a specific point, you can rotate the input coordinates such that your point of interest becomes [0, 0]. I think this is same thing, right?

@larskotthoff

Yes, that does look very similar! I actually like that better because it doesn't require two internal arrays. This would be scale-independent as well by the looks of it?

@jasondavies
D3 member

Yes, it's a projection from spherical coordinates back onto spherical coordinates, so you can use it to "preprocess" points before passing them to a projection that maps onto Cartesian coordinates (at this point you can apply a scaling transformation).

@larskotthoff

I've had a closer look at #318, but I can't figure out how it should be used. So d3.geo.rotate() takes coordinates (that you can adjust by calling .x etc), but how do I pass it to a projection? I have tried things like

var proj = d3.geo.equirectangular(),
      rot = d3.geo.rotate(),
      path = d3.geo.path().projection(function(d) { return proj(rot(d)); });
rot.x(90);

but that doesn't seem to work for values other than 0.

@jasondavies
D3 member

That seems to work fine for me, is there something unexpected happening? Note that d3.geo.rotate takes rotation angles, not coordinates. So the x-axis is the axis going from the centre of Earth through longitude, latitude: 0, 0. So rotating about this axis only by 90 degrees will result in the poles being at the left and the right prior to being projected.

If you want to rotate the Earth to particular coordinates, you'll want to rotate about the z-axis (longitude) and y-axis (latitude).

@larskotthoff

Ah ok. I didn't get the meaning of the dimensions :)
What I'm trying to do as a hello world like thing is to simply put -180, 90 in the upper left corner, like I did with my changes. rot.z(180) works as expected, but I can't get the latitude right. rot.y(value) rotates the projection, but doesn't really move it. How would I do a vertical translation of the projection?

@jasondavies
D3 member

Ah, I don't think geo rotations will help you. Apologies for misunderstanding what you were trying to do!

@larskotthoff

There should be some common functionality though. At least rotating the z-axis does exactly what I want to do for the longitude (if no rotation is applied along the other axes). I'll have a look if this can be merged somehow.

Another random thought -- I played around with this and the equirectangular projection. After some rotations, it wasn't an equirectangular projection anymore. Maybe this funtionality could extended and used to derive new projections by rotation and/or transformation matrices?

@jasondavies
D3 member

Yes, that's because a z-axis rotation is simply a longitudinal translation. :) I don't see any particular reason to merge the functionality as 3D rotations using matrices are quite different from what you're doing.

It sounds like you were producing an oblique equirectangular projection, so it's not a completely new projection per se. The whole point of d3.geo.rotate is to produce oblique versions of an arbitrary projection, which I think is quite fun!

@larskotthoff

Well, my point was that to get from e.g. a Mercator projection to an equirectangular projection, you can apply a transformation to the latitudinal coordinates. You can use a transformation of the same kind to shift the projection though to center a location. Similarly, you can get to an azimuthal projection by rotation.

So my idea was to define all the projections in terms of a basic projection. The same methods used to define these projections could then be used to center locations, rotate, etc. The advantage would be that new projections could be defined with far less code. This should definitely be a separate pull request though (and would probably require quite some work to get it right).

@jasondavies
D3 member

Converting from Mercator to equirectangular isn't possible using only rotational transformations. Even though they look similar, they're actually quite different! Azimuthal projections are even more different, and you can't just obtain one by rotations alone.

The idea of d3.geo.rotate is that you can take any projection that maps [lon, lat] -> [x, y] (typically with [0, 0] as the origin, at the centre) and convert it to an oblique projection by first rotating the globe arbitrarily such that some other location is at [0, 0], and optionally, it can be rotated around the x-axis too. So it does indeed save having to do this for each individual projection (although for azimuthal ones it's more efficient to use the azimuthal formulas directly).

@larskotthoff

Let me try to clarify with some code. Assume I have something like

var proj = d3.geo.equirectangular().scale(1).translate([0,0]),
            f = function(c) { return c; },
            path = d3.geo.path().projection(function(d) { return proj(f(d)); });

If I define f as

f = function(c) { return [c[0] + 180, c[1] - 90]; };

I get my translation of the projection. If I define it as

f = function(c) {
                var d3_geo_radians = Math.PI / 180,
                    x = c[0],
                    y = (Math.log(Math.tan(Math.PI / 4 + c[1] *
                                d3_geo_radians / 2)) / d3_geo_radians);
                return [x, Math.max(-180, Math.min(180, y))];
            };

I get a Mercator projection instead of the equirectangular one. Similarly, if I define it as

f = function(c) {
                var d3_geo_radians = Math.PI / 180,
                    x1 = c[0] * d3_geo_radians,
                    y1 = c[1] * d3_geo_radians,
                    cx1 = Math.cos(x1),
                    sx1 = Math.sin(x1),
                    cy1 = Math.cos(y1),
                    sy1 = Math.sin(y1),
                    cc = null,
                    k = 1,
                    x = k * cy1 * sx1,
                    y = k * (cy1 * cx1 - sy1);
                return [x, y];
            };

I get an azimuthal projection. You could also define functions to do rotations etc. It seems to me that this would make it easier to define new projections, as less code needs to be written. It also provides a uniform interface for translation, rotation, other projections etc.

@jasondavies
D3 member

I think we are mixing three different types of operations here:

  1. Operations that map spherical coordinates -> spherical coordinates e.g. d3.geo.rotate.
  2. Geographic projections that map spherical coordinates -> Cartesian coordinates in pixel space e.g. d3.geo.mercator.
  3. Transformations in pixel space, mapping Cartesian coordinates -> Cartesian coordinates e.g. scaling and translations.

Your "geotranslate" operation falls under no. 3, as it's a way to set a translation offset in pixels (even though you allow the offset to be specified in spherical coordinates -- note that the translation is applied after the projection has occurred). Instead of modifying each projection to support this, you can also just do:

var w = 960,
    h = 500;
xy.translate([0, 0]);
var centre = xy([0, 52]);
xy.translate([w/2 - centre[0], h/2 - centre[1]]);

This will place the UK at the centre of the resulting projection. You could do something similar to place [-180, 90] at the top-left corner.

I think defining new projections is already fairly simple, as all you need to do is define the projection function itself, and an invert function. Admittedly the code for setting scale and translate could all be separated into a reusable helper, to make it even easier.

@larskotthoff

I think that's probably where we disagree. All the 3 types of operations you've mentioned are the same to me -- they transform coordinates. For example, translate and geotranslate are fundamentally the same. The only difference is that one is applied before a projection and the other one after. Similar for projections themselves. There's no reason why you couldn't chain them to achieve weird and wonderful effects.

I agree that everything can be done already, but maybe we could make it easier to use. Remember when I asked the question about making a specific part of the world fullscreen? With something like geotranslate this would have been a lot more straightforward.

I think essentially my idea boils down to this. Instead of the current API where you create projections, set parameters and maybe combine them with rotations, why not have a chain of functions that are applied one after the other? Something like

var p = d3.geo.projectionChain();
p.push(function(c) { return [c[0] + 180, c[1] - 90]; });
p.push(d3.geo.mercator());
p.push(d3.geo.rotate().z(10));
p.push(function(c) { return [c[0] - 640, c[1] + 480]; });

This might make it more difficult to get started, but it would certainly allow for more flexibility. For example, one could have a map with a section of it magnified by just adding an item to the queue for the magnified bit. If the other elements are the same, panning and zooming the original map would also change the magnified bit without any additional code.

@jasondavies
D3 member

Both geotranslate and translate perform a translation after the projection to Cartesian coordinates, that's why they are both the same type of operation.

There is an issue with your example where your first operation is:

function(c) { return [c[0] + 180, c[1] - 90]; }

For example, consider what happens when you project the South pole, [0, -90]. You get [180, -180], which is outside the usual latitude range for spherical coordinates (it's still technically valid, but you'll get strange results i.e. the Southern hemisphere will be flipped and overlay the Northern hemisphere, which will be shifted to the Southern).

Then you apply your mercator projection (resulting in Cartesian coordinates), and then apply a rotation, treating these as spherical coordinates! Rotating about the z-axis is going to be fairly benign, but depending on the range of the Cartesian coordinates, a more complex rotation will result in something strange, with potentially overlapping coordinates (because it maps from spherical to spherical and you're giving it Cartesian).

By the way, I think it might be confusing to use the equirectangular projection as an example, because it is a linear transformation something like f(λ, ϕ) = (λ/360, ϕ/180), so it's easy to confuse spherical with Cartesian coordinates. It might be easy to think you can just chain different geographic projections if you start with equirectangular. But we have to remember that geo projections operate on spherical coordinates (angles), and not Cartesian coordinates, so while you can technically chain them together, it doesn't mean you should. :)

Having a chain of operations (in the right order!) certainly does sound appealing, but I think it's straightforward to do so by composing functions in the usual way. So if you wanted some kind of magnifying operation, this sounds like it would operate in pixel space, so you could do:

var magnify = …,
    mercator = d3.geo.mercator(),
    rotate = d3.geo.rotate().x(…).z(…);
function(d) { return magnify(mercator(rotate(d))); }

And if your map could be panned, you would just call mercator.translate(…) and the magnifier would be kept in the middle, say, with no extra work. Likewise for geo rotations, etc.

I think we both agree that cool things can be created by composing various operations together, but I think it only makes sense if you do them in the right order i.e. (spherical coordinates -> …) -> (Cartesian coordinates -> …). Unless of course you're trying to create art! :)

@larskotthoff

Yes, I think we're agreed on that :) The way to break things in interesting ways would certainly be increased by the composing functions approach. My main motivation for bringing this up is that it would remove the amount of duplicate code. The scale and translate functions in all projections are virtually identical. And the geotranslate stuff introduces even more duplication, and that's apart from having to maintain a weird auxiliary array of projected offsets!

There are certainly issues with translating things that then go out of bounds. I'm not entirely sure what we could do about that, but I tend to think that documenting it and not adding any safeguards to the code would be best. After all, you can mess up projections already by calling functions with unexpected values.

@mbostock
D3 member

Superseded by d3.geo.projection center (and rotate), part of the new geo.projection work to be released in 3.0. See #846 for details.

@mbostock mbostock closed this Oct 6, 2012
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment