Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for tiles to geoshape mark #8885

Draft
wants to merge 9 commits into
base: main
Choose a base branch
from

Conversation

binste
Copy link

@binste binste commented May 6, 2023

This PR is far from being ready to be merged as many corner cases need to be handled, tests and documentation written, etc. But I have a few questions on how to proceed which are probably easier to answer if you can see the code. The goal is to add a tile property to the geoshape mark which adds tiles from arbitrary tile XYZ tile servers as image marks. See #8767 for more context and all the credits for the calculations goes to @mattijn who provided the initial specs!

Current status

I already got the layering to work so that the following spec (notice the "tile": true in "mark"):

{
  "data": {
    "url": "https://cdn.jsdelivr.net/npm/vega-datasets@v1.29.0/data/world-110m.json",
    "format": {"feature": "countries", "type": "topojson"}
  },
  "mark": {
    "type": "geoshape",
    "fillOpacity": 0.1,
    "stroke": "orange",
    "strokeWidth": 2,
    "tile": true
  },
  "width": 400,
  "height": 400,
  "projection": {
    "type": "mercator",
    "scale": 300,
    "center": [0, 40],
    "rotate": [-10, 0, 0]
  }
}

produces an extended VL spec in the Vega Editor which is very close to what @mattijn originally came up with. The main change is that I rewrote the calculations so that they do not depend on an input zoom level but instead the zoom level is determined based on scale in projection:

{
  "width": 400,
  "height": 400,
  "layer": [
    {
      "mark": {
        "type": "image",
        "clip": true,
        "height": {"expr": "tile_size"},
        "width": {"expr": "tile_size"}
      },
      "encoding": {
        "url": {"field": "url", "type": "nominal"},
        "x": {"field": "x", "scale": null, "type": "quantitative"},
        "y": {"field": "y", "scale": null, "type": "quantitative"}
      },
      "data": {
        "sequence": {"start": 0, "stop": 4, "as": "a"},
        "name": "tile_list"
      },
      "transform": [
        {"calculate": "sequence(0, 4)", "as": "b"},
        {"flatten": ["b"]},
        {
          "calculate": "base_tile_url + zoom_ceil + '/' + (datum.a + dii_floor + tiles_count) % tiles_count + '/' + ((datum.b + djj_floor)) + '.png'",
          "as": "url"
        },
        {"calculate": "datum.a * tile_size + dx + tile_size / 2", "as": "x"},
        {"calculate": "datum.b * tile_size + dy + tile_size / 2", "as": "y"}
      ]
    },
    {
      "projection": {
        "type": "mercator",
        "scale": 300,
        "center": [0, 40],
        "rotate": [-10, 0, 0]
      },
      "data": {
        "url": "https://cdn.jsdelivr.net/npm/vega-datasets@v1.29.0/data/world-110m.json",
        "format": {"feature": "countries", "type": "topojson"}
      },
      "mark": {
        "type": "geoshape",
        "fillOpacity": 0.1,
        "stroke": "orange",
        "strokeWidth": 2
      },
      "encoding": {}
    }
  ],
  "params": [
    {"name": "base_tile_size", "value": 256},
    {"name": "base_tile_url", "value": "https://tile.openstreetmap.org/"},
    {"name": "prScale", "expr": "300"},
    {
      "name": "zoom_level",
      "expr": "log((2 * PI * prScale) / base_tile_size) / log(2)"
    },
    {"name": "zoom_ceil", "expr": "ceil(zoom_level)"},
    {"name": "tiles_count", "expr": "pow(2, zoom_ceil)"},
    {
      "name": "tile_size",
      "expr": "base_tile_size * pow(2, zoom_level - zoom_ceil)"
    },
    {"name": "base_point", "expr": "invert('projection', [0, 0])"},
    {"name": "dii", "expr": "(base_point[0] + 180) / 360 * tiles_count"},
    {"name": "dii_floor", "expr": "floor(dii)"},
    {"name": "dx", "expr": "(dii_floor - dii) * tile_size"},
    {
      "name": "djj",
      "expr": "(1 - log(tan(base_point[1] * PI / 180) + 1 / cos(base_point[1] * PI / 180)) / PI) / 2 * tiles_count"
    },
    {"name": "djj_floor", "expr": "floor(djj)"},
    {"name": "dy", "expr": "round((djj_floor - djj) * tile_size)"}
  ]
}

This VL spec renders fine:

image

Questions

There are two related issues around the parameters in the above spec where I'd be very grateful for guidance from @domoritz or any VL maintainer.

  • The current approach relies on value parameters which in my understanding need to be at the top-level of a specification, i.e. I cannot have them in a LayerSpec. I managed to push them to the top-level of the processed view as in the example above, but this does not work if the above spec is wrapped into an hconcat chart. Any ideas (existing mechanisms?) how I could push these parameters to the top of any spec, independent of how deeply nested the chart spec is which has "tile": true?
  • The parameters do show up in the extended Vega-Lite specs in the Vega Editor. However, this is not the spec which is passed to vega.parse. The spec which is eventually passed lost the parameters section. I think this happens here https://github.com/vega/vega-lite/blob/main/src/compile/compile.ts#L122. spec has the parameters, vgSpec does not. You can see the behavior I described in this screen recording:
Screen.Recording.2023-05-06.at.17.06.10.mov

If it's not possible to do what I want with the parameters, I could maybe rewrite the generated spec so that all expressions are inlined but that would lead to a lot of duplicated calculations and makes the resulting VL and Vega specs less readable in my opinion.

Any help is greatly appreciated! Thanks.

Linked issue: Closes #8767.

@mattijn
Copy link
Contributor

mattijn commented May 7, 2023

Nice work already @binste! Regarding the required lifting of parameters to top-level, I would prefer to move the mechanism we introduced in Altair, vega/altair#2702 to Vega-Lite so this lifting can be done when serializing it (as macro?) into extended Vega-Lite.
In hindsight it would have been better to do that in Vega-Lite directly, but at the moment of writing we (at least I) were not aware of the extended Vega-Lite principle (Vega-Medium?).

This also would mean that Vega-Lite can better controls the naming definition of parameter views.

@binste
Copy link
Author

binste commented May 9, 2023

Sounds good to me! I'll pause the development here and wait for further feedback regarding the parameters question.

@mattijn I noticed that in some settings, there is a slight gap between the tiles:
image

Do you know how the placement of the tiles should be adjusted to close it?

@binste
Copy link
Author

binste commented May 10, 2023

So far I only saw the gap in the Vega Editor, not in VS Code with the same settings. Maybe the placement of the images is sometimes slightly different. If I add 1 pixel to the height and width of the image marks then the gap is also closed in the Vega Editor.

@binste
Copy link
Author

binste commented May 18, 2023

I started working on https://github.com/binste/altair_basemap to implement this on the Altair level. I still think it makes sense to implement it in Vega-Lite but until we can resolve the open issues and questions here it could be nice to have a Python implementation to experiment and to bridge over the time until this PR is merged.

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

Successfully merging this pull request may close these issues.

include tiles option for geoshape mark
2 participants