Map Tile Seams

Jan 21, 2020

Sometimes there are artifacts on web maps that let you immediately see "hey, it's a tile border!" There are many different versions of them, but root of them all is the same - you've selected a subset of data that leads to a different rendering than it would be if you've rendered all of the world at once and then only clipped the image.

This becomes worse in epoch of vector tiles: you can't go that way, you have to clip your data first, and only then let the client render it.

1. Tiles are rendered independently, and all decisions in the objects that cross the boundary should be the same.

One of such decisions is area-based polygon sorting - if you have a bush in a lake in a forest, and you don't have holes in your objects, you can still order them by area before rendering, and it will just work. No need to separate layers, no need to guess z-indexes for objects.

But: the area has to be of original object. If you clipped your polygon into two tiles, and implemented the area calculation in the renderer - you'll see small pieces of larger polygons disappearing under larger pieces of smaller polygons, exposing seams.

2. You want to render an outline for polygon. You clipped the polygon to tile, and now you see all the seams in color of your outline.

There are two ways to fix it: one was implemented in Kothic.js, just never render the polygons that coincide with tile boundaries. Disadvantage is that you need custom line drawing code, and can't render the tile outlines if one day you need to.

Other way it's implemented in ST_MVT export in PostGIS: if you know a width of your outline, enlarge the clipping box by that number of pixels and clip. This way when such tile will be rendered, the line will still be rendered, but never actually shown on screen, as it's outside the tile boundary.

UPD: there's a third way: export outline as a separate line and treat as if it was line.

If you don't know how much to buffer the clipping box for such poly, take a wild guess and say "nobody's ever going to use more than 20 px outlines with these tiles".

2. A line has two linecaps, one on each end. Whenever you clip it, it will show a linecap. A wide-ish road clipped on the boundary exactly will show everyone that you clipped it with a rounded triangle inset into the road. You want that to happen out of your public's eyes, so you clip it with a box larger than your tile by at least the road's rendered width.

So "clip lines with 20+px".

3. Your lines may have labels. Labels are cursed and there are lots of heuristics to deal with them.

One effect that you'll get if you label your lines from (2) along their curve is that some of texts are going to cross the tile boundary, and on the next tile they'll continue from an earlier point in text, sometimes matching up and creating gibberish "Main St|ain Street".

Three things to do to fight these:

- teach your rendering engine to chop the geometry further, so labels never leave tile. That was done in Kothic JS by filling the text collision buffer with 1-pixel polygons along tile edges. That still exposes your tile boundaries in densely labeled areas, by absence of labels.

- out second, clipped geometry for each label. That's simulating previous thing for simpler rendering engines.

- generate special linear geometries for labels and never clip those. That's less explored, but most flexible way. Allows you to label tricky objects like curved Lombard Street with straight labels by ST_Simplify-ing them with much higher epsilon, or generate bezier curves to bend labels along 90-degree turns like some navigation systems do. These things can't be clipped when generating a tile, but can be pre-split at algorithm-defined spots.

4. You have icons. You don't want an icon chopped, so you need to render it in adjacent tile too. To do it, you put it there. Clipping the point layers is just "select 10px more".

5. You have point labels. That's the trickiest part - an area-less point becomes quite large box in label collision calculation, decisions on the position and text sizes in all tiles should be the same (that's why fallback positions aren't implemented in vector tile rendering engines yet), and both sides of the same label in neighboring tiles have to match.

That's when everyone says "enough, too hard" and starts doing simple solutions, like "pull all labels on screen into a buffer, deduplicate, render as one". That's slow, and doesn't fix other issue: text flickering on drag. When a new tile comes to the screen, such buffer will re-calculate and some labels will decide to disappear in favor of new ones.

So, to reduce flicker, you want all of your labels to be aware of all the labels in neighbouring tiles that can make labels in current tile disappear. This can be analysed precisely (which is yet to see in open source projects), or covered with a band-aid: just include everything in huge radius for label layers. If you don't care about your tile size, measure your longest label's width, and set clipping buffers for labeled point geometries to half of that value plus 1 pixel. Or guess it as 256px.

And now: any larger buffer is usually visually better or exactly the same as smaller one. If you are lazy and don't care for the tile size, just set your buffer size to a reasonably large 256-512px and it will get you through the day.