Skip to content
This repository has been archived by the owner on Nov 7, 2018. It is now read-only.

d3.geo.tile should actually do geo stuff #37

Closed
shawnbot opened this issue Jan 24, 2013 · 19 comments
Closed

d3.geo.tile should actually do geo stuff #37

shawnbot opened this issue Jan 24, 2013 · 19 comments

Comments

@shawnbot
Copy link

d3.geo.tile holds promise, but right now it's seriously lacking for all but demo maps because there's no built-in support for working with geographic coordinates. E.g.:

var tile = d3.geo.tile()
  .center([-76.3429, 38.7351]) // geographic center
  .zoom(12); // in tile coordinates

Are there plans to support this? I'm wondering aloud if @tmcw, @migurski, or @RandomEtc might be working on this already.

@tmcw
Copy link
Contributor

tmcw commented Jan 24, 2013

We've implemented all of the basic APIs, zoom, center, extent, etc in iD, but they're not generalized currently: https://github.com/systemed/iD/blob/master/js/id/renderer/map.js#L213 (demo: http://geowiki.com/iD/ )

(iD's background layer also supports backbuffering and such)

@mbostock
Copy link
Member

On the contrary my dear fellow, the built-in support for geographic coordinates is a d3.geo.projection! Here's your example using the Mercator projection:

var projection = d3.geo.mercator()
    .center([-76.3429, 38.7351]) // geographic center
    .scale(1 << 12) // zoom level in tile coordinates
    .translate([width / 2, height / 2]);

var tile = d3.geo.tile()
    .scale(projection.scale())
    .translate(projection([0, 0]))
    .size([width, height]);

And here's a live example: http://bl.ocks.org/4150951

@tmcw
Copy link
Contributor

tmcw commented Jan 24, 2013

Trying to switch to .center() with iD (after getting the aha that translate() is non-geographical) and it's pretty fearsome resolving zoomPan's talking only in translate and projection talking in geographical units and not being able to get 'non-geographical' translates together. Is there any chance of the zoom behavior talking in geographical units or the projection code being able to talk in translates?

For instance, right now, projection(foo) can give you the screen coords of a feature, but not the 'from origin translate' of that feature. The approach so far has been to keep the zoom behavior & projection in sync (via scale & translate). Using center, this is a lot less clear (relevant code: https://github.com/systemed/iD/blob/7cda89b75753b9ad4d300e267bae5b0f5f86995c/js/id/renderer/map.js#L135 ).

@migurski
Copy link

No kidding @tmcw that code is pretty fearsome. I’m sitting this one out, no plans to use d3.tile.

@mbostock
Copy link
Member

Let the zoom behavior drive the projection, rather than creating a feedback loop through projection.invert. For example, with Mercator, first create the projection:

var projection = d3.geo.mercator();

Then, on zoom, update the scale and translate (but don’t change the center, and don’t use projection.invert):

projection
    .scale(zoom.scale())
    .translate(zoom.translate());

And that’s it. Live example, showing the mouse location: http://bl.ocks.org/4132797/dc4d80534125f326349e7fefa05430293e4f5bf8

@tmcw
Copy link
Contributor

tmcw commented Jan 24, 2013

Ah, okay - that's essentially the approach we were doing before, though it makes functions that set center & zoom programmatically significantly more complex: https://github.com/systemed/iD/blob/master/js/id/renderer/map.js#L213

@mbostock
Copy link
Member

It gets a lot simpler if you temporarily set projection.center to compute the appropriate translate, and then reset it. For example, to zoom to San Francisco:

projection
    .scale(1 << 19)
    .center([-122.4183, 37.7750]) // temporarily set center
    .translate([width / 2, height / 2])
    .translate(projection([0, 0])) // compute appropriate translate
    .center([0, 0]); // reset

zoom
    .scale(projection.scale())
    .translate(projection.translate());

@migurski
Copy link

Probably time for a pyramid-aware “Coordinate” abstraction between geography and pixels.

@mbostock
Copy link
Member

@migurski If you are referring to pixels in screen space, the abstraction is d3.geo.projection. If you are referring to the quadtree tile pyramid, the abstraction is d3.geo.tile, except there’s no reason to expose the tile coordinates outside of d3.geo.tile (since you have d3.geo.projection for screen coordinates). Seems simpler to avoid exposing a third coordinate system unless necessary.

@migurski
Copy link

I’m referring to the quadtree, and there’s definitely reason to expose the third coordinate system. Being able to “speak tile” and have English-looking words for concepts like tile-to-the-right, tile-one-zoom-out-from-this-one and so on is huge, huge, huge when you need to sync image and non-image layers. UTFGrid interaction layers, vector data loading, and multi-zoom pyramid loading to eliminate the flash of unloaded tiles are three examples. The density of the code snippets in this thread is another: the tile concept is implied in the serial calls to translate() and the recursive (?) use of projection, but it would be easier to see and reason about if exposed as a first-class concept with words and stuff.

@mbostock
Copy link
Member

I completely agree there is a use for tile coordinates when you need to speak tiles, but that is not the case in the simple examples above. There is no tile coordinate system implied in my earlier snippet; there is only the geographic coordinate system and pixels. It is d3.geo.tile that internally maps pixels to tile coordinates, but it could apply any number of strategies to do so (why not hex tiles?) and my point is that it is not required to expose the tile coordinate system in order to use d3.geo.tile.

But, as you say, d3.geo.tile could expose the mapping from pixels to tiles, as there are useful applications of the tile coordinate system outside of simply loading them. And in essence it does that already, because d3.geo.tile returns the set of tile coordinates that are visible in the viewport. This interface could be extended to expose the direct mapping between pixels and tiles, similar to projection.

What I am arguing for, albeit poorly, is that I would like to avoid coupling d3.geo.projection to tile coordinates, and likewise avoid coupling d3.geo.tile to geographic coordinates. d3.geo.projection should handle geography ↔ pixels, and d3.geo.tile should handle pixels ↔ tiles.

@migurski
Copy link

Where I see the tile system implied is in the bit-shifted zoom levels passed to scale() and the calls to Math.pow in the iD code. In the mercator code, D3 already transforms the raw projection by 1/(2π) so the tile math starts there, and ends up sort-of scattered throughout the chain. I agree that it’s not required to expose the coordinates to use tile, I just think it would be better and more composable implementation if it was exposed legibly.

In Modest Maps, I chose a coupling that’s actually quite similar to the one you’re trying to avoid, so this might be a matter of taste. =) MMaps projections know about tile coordinates but not about pixels, so they have an explicit post-projection affine transformation, typically from ±π to 0…1 for spherical mercator. Maps know about tile coordinates and pixels but not geography, so they defer to the projection to perform that translation. Tile providers link the two together.

I’ve found it very helpful for additional work to have that available.

@migurski
Copy link

Yeah so I kinda went and scratched a whole bunch of itches at once with this: https://github.com/migurski/Squares#readme

Includes the first-class quadtree I've been talking about, mostly ported from @RandomEtc’s 2011 D3 + tiles exercise.

@mbostock
Copy link
Member

You might be interested in the recent wheel event fixes to d3.behavior.zoom; see d3/d3#1050. I wonder if you can use d3.behavior.zoom in Squares?

@migurski
Copy link

I’ll have a look, thanks. The DIV-based scroll thingie does have quite different rates in FFox vs. Chrome, with Safari someplace in the middle.

@migurski
Copy link

I think d3.behavior.zoom is going to be a dead-end for me. Based on the example I could find, which I only sort-of understand, it looks like something that you call() individually for each element that gets appended in an enter selection? I need it to be something applied to the map as a whole. I’ll instead port the non-DIV approach from the patch.

@mbostock
Copy link
Member

I would look at the d3.geo.tile example: http://bl.ocks.org/4132797

And no, you don’t need to add it for each element that gets appended, you only need to add it to the parent container.

@migurski
Copy link

I tried both combinations, and got no movement either time. I suspect that I'm not putting the pieces together correctly, so I’m not going to pursue D3 behaviors any further for the time being.

I also tried porting over the new mousewheel support from the commit you linked to above (migurski/Squares@1d04b8f0). It sort-of works, but Safari reports 10x the movement that Chrome does, so the zooming behavior appears wildly erratic, much moreso than with the hidden div. I'll leave it the way it was previously.

@tmcw
Copy link
Contributor

tmcw commented Mar 12, 2013

Anyway, I've got to 👍 the idea of coordinates as a plugin and would be happy to write it. At the moment, a basic 'd3 + tiles demo' with zoom in and zoom out buttons is very non-obvious.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Development

No branches or pull requests

4 participants