stadiamaps / cartogrify

The cartographic style transmogrifier
BSD 3-Clause "New" or "Revised" License
11 stars 0 forks source link

Investigate label placement/collision issues #4

Open ianthetechie opened 3 years ago

ianthetechie commented 3 years ago

This may be a core Tangram issue, but currently generated styles seem to have huge areas where labels are initially rendered but then disappear, presumably due to Tangram's label placement or collision constraints. Additionally, the precedence/priority does not seem to be quite right. This is probably an implementation error on my part.

Here is the source GL JSON style that exhibits the issue: https://tiles.stadiamaps.com/styles/alidade_smooth_dark.json (you can run a server on localhost to test; see https://docs.stadiamaps.com/ for auth info). Below is an example of such a "hole" at a particular zoom level in Germany.

image
nvkelso commented 3 years ago

That looks like an oriented buffer around the A3 text label. But the other road labels don't have that issue. I don't remember seeing a bug like this while at Mapzen working on Tangram-based styles, you might have found something novel! :)

burritojustice commented 3 years ago

I took a look at the yaml that was generated and noticed this:

  highway_name_major:
    data: {layer: transportation_name, source: openmaptiles}
    draw:
      text:
        blend_order: 37
        buffer: 350px
        font: {family: Open Sans Regular, fill: '#ccc', priority: 962, size: 13, transform: uppercase}
        move_into_tile: false
        style: overlay_text

In the source .json you have this:

      "layout": {
        "symbol-avoid-edges": true, 
        "symbol-placement": "line", 
        "symbol-spacing": 350,

I suspect you're trying to avoid label repetition -- if so, you'll want to use repeat_distance instead of a buffer.

https://tangrams.readthedocs.io/en/latest/Syntax-Reference/draw/#repeat_distance

bcamper commented 3 years ago

Good eye @burritojustice! Yes, buffer: 350px will ensure there is at least 350px of space around each label, hence that one lonely A3 in the middle 🤣

repeat_distance is indeed useful for preventing the same label text from repeating too closely. It doesn't affect where the labels are placed though, just if they are shown or hidden (the default value is usually fine too though).

For label placement along a line, the parameters you want that correspond to MBGL are documented here: https://tangrams.readthedocs.io/en/master/Syntax-Reference/draw/#placement

So I believe the equivalent would be:

draw:
  text:
    placement: spaced
    placement_spacing: 350px
    ...

There are also some more esoteric options like https://tangrams.readthedocs.io/en/master/Syntax-Reference/draw/#placement_min_length_ratio.

Some examples from Mapzen basemaps (with zoom stops on some of those params): https://github.com/tangrams/bubble-wrap/blob/ab18c141a1f782f113c449e2df86ed966598eb75/bubble-wrap-style.yaml#L1601-L1602 https://github.com/tangrams/bubble-wrap/blob/ab18c141a1f782f113c449e2df86ed966598eb75/bubble-wrap-style.yaml#L1556-L1567

ianthetechie commented 3 years ago

Wow, thanks a lot guys for taking the time to look through these! Yes, you're right. That was totally an implementation error on my end. Using placement: spaced and placement_spacing: 350px does the trick.

I may circle back next week if I'm unable to roughly match the label precedence, but I did find docs on that so I'll check those first.

Cheers!

ianthetechie commented 3 years ago

I think I've fixed most of the issues I had with priority/precedence of text labels (I accidentally attached the priority to the font block instead of text). However, there's something a bit amiss still. The following sequence illustrates how labels that are supposed to be a lower (higher numerically in Tangram) priority can crowd out ones that are supposed to be more important. It looks like the highway_name_motorway labels can sometimes crowd out place_city_large (screenshots show the difference between 2 scroll wheel clicks of zoom).

image image

I've included the two layers below. Maybe someone can spot what I missed.

  highway_name_motorway:
    data:
      layer: transportation_name
      source: openmaptiles
    draw:
      text:
        blend_order: 38
        font:
          family: Open Sans Regular, Noto Sans Arabic Regular, Noto Sans Ethiopic
            Regular, Noto Sans Thai Regular, Noto Sans Khmer Regular, Noto Sans Lao
            Regular, Noto Sans Myanmar Regular, Noto Sans Tamil Regular, Noto Sans
            Bengali Regular, Noto Sans Devanagari Regular, Noto Sans Hebrew Regular,
            Noto Sans Armenian Regular, Noto Sans Georgian Regular, Noto Sans JP Regular,
            Noto Sans SC Regular, Noto Sans TC Regular, SeoulNamsan M, Noto Sans Regular
          fill: hsl(214, 11%, 65%)
          size: 14
          stroke:
            color: hsl(0, 0%, 20%)
            width: 2.0px
        move_into_tile: false
        placement: spaced
        placement_spacing: 350px
        priority: 961
        style: overlay_text
        text_source: ref
    filter:
      all:
      - $geometry: line
      - class: motorway
  place_city_large:
    data:
      layer: place
      source: openmaptiles
    draw:
      icons:
        blend_order: 44
        order: 44
        size: 40.0%
        sprite: circle-alt-11
        text:
          anchor: right
          blend_order: 44
          font:
            family: Open Sans Regular, Noto Sans Arabic Regular, Noto Sans Ethiopic
              Regular, Noto Sans Thai Regular, Noto Sans Khmer Regular, Noto Sans
              Lao Regular, Noto Sans Myanmar Regular, Noto Sans Tamil Regular, Noto
              Sans Bengali Regular, Noto Sans Devanagari Regular, Noto Sans Hebrew
              Regular, Noto Sans Armenian Regular, Noto Sans Georgian Regular, Noto
              Sans JP Regular, Noto Sans SC Regular, Noto Sans TC Regular, SeoulNamsan
              M, Noto Sans Regular
            fill: '#9aa2ac'
            size:
            - - 4
              - '11'
            - - 12
              - '18'
            - - 14
              - '22'
            stroke:
              color: hsl(0, 0%, 20%)
              width: 2.0px
            transform: uppercase
          move_into_tile: false
          priority: 955
          style: overlay_text
          text_source: 'function() { return `${feature["name:latin"] || ''''}

            ${feature["name:nonlatin"] || ''''}`.trim() }'
    filter:
      all:
      - $geometry: point
      - all:
        - not:
            capital: 2
        - rank:
            max: 4
        - class: city
nvkelso commented 3 years ago

What happens when you remove the blend_order bits?

What does style: overlay_text reference, is there a link to the full scene file?

An aside: That's an insane number of font family fallbacks! Does OpenSans not provide those codepoints by default?

ianthetechie commented 3 years ago

Without blend_order, the ordering ends up wrong and the text is (partially) buried. My understanding is that blend_order is necessary when drawing with transparency. In Mapbox there is no such use of Photoshop style blend modes, but it looks like these are necessary in order to draw any colors with alpha (which our styles use heavily at the moment).

overlay_text is one of the "magic" blend combination styles that Tangram creates for you. It's a text style that sets the blend mode to overlay. I suppose I could change this to blend: overlay but both appear to have the same effect.

Here's a scene file; looks like GitHub won't accept yaml so I zipped it. scene.yaml.zip

Finally, here's the back story on the fonts ;) Open Sans does actually include these glyphs (if you get the massive version of it at least). We used the lighter version of Open Sans for most glyphs. Then we tracked down what we thought were better looking fonts for less common scripts. For example, the Korean Government has a font that looks pretty great and is really familiar to anyone in Korea so we use that for Korean glyphs to improve the typography. One unsolved mystery to me is that the glyphs actually render (for Korean, for example) even if I only specify Open Sans as the font, but Tangram (correctly?) picks the SeoulNamsan M glyph when rendering Korean text. So either it is automatically falling back to system fonts like Helvetica for these glyphs or I misunderstand how it works. At any rate, the fonts are indeed being selected according to the correct order and match what I expect in all languages that I can read / care about the typography at least ;)

Also note that Mabox's font rendering is a bit different... the TL;DR is that you have a "font stack" and the order here gets converted into a request for glyph ranges (of 256 glyphs each). The server then returns the glyphs in the range you requested with the font precedence you requested as a protobuf packed SDF (signed distance field) glyph package which is then rendered by Mapbox GL (JS). It doesn't use any actual "font" technologies at all; it's a rather different way of rendering fonts so you can get away with specifying a bunch of weird fonts like this and it doesn't affect performance. I'll of course consider making a custom "superfont" instead to save renderer work, but hopefully this bit of trivia was interesting ;)

ianthetechie commented 3 years ago

Update: Yeah, the (normal at least) Open Sans definitely doesn't have CJK glyphs. Since Tangram seems to leverage browser font rendering at some level, it must have been falling back to Helvetica before I added the other fonts. That's why we have Noto etc. because that fills out the rest of the languages. This is likely then less necessary in Tangram as it can fall back to browser defaults or whatever, but I think adding them is helpful since it makes the rendering more consistent / explicit and many machines don't have fonts with CJK glyphs out of the box.

Also, for the scene file I attached you'll notice I have 2 fonts hardcoded for now. The rest will be fleshed out in #3.

image
nvkelso commented 3 years ago

It looks like the blend_order is changing for each "label" layer, but I think you generally want them to be in the same "render pass" instead?

BTW, in the draw blocks you should be able to just say the style name directly instead of setting it later? So draw: { inlay_lines: ... } and then you wouldn't need to restate some of the other properties, too?

Nice, that makes sense about how you're setting up the fonts. Since the font family set is always the same, you can make it into a globals variable and then reference it each time instead of repeating the list?

Nit: Consider moving the filter section above the draw section so it's easier to reason about what is being shown before how it should be shown? Either work, but if you're reading a bunch of scene file's it'll make it easier?

ianthetechie commented 3 years ago

Hmm, good point. I will try to simplify that blend_order. You're right; all labels should end up at the top and priority should take care of precedence alone. I'll simplify that by setting them all to a massive value. That doesn't seem to change the result, but makes logical sense.

Good call on just moving the style up a level rather than wasting an extra key. I forgot about that feature (even though I used it elsewhere...).

Good suggestion on moving the filter higher. pyyaml appears to sort keys by default, but I think I can override this.