Mapbox-gl-js: Best option for preventing labels from touching edge of map

Created on 31 Mar 2018  路  14Comments  路  Source: mapbox/mapbox-gl-js

Hello!

I am trying to prevent map labels from getting placed such that they get cut off from the edge of the map for my project (IRLMap.com). In the screenshot below, I show examples of labels that are getting cut off marked with 馃毇.

My first idea was to create a rectangular polygon at the perimeter of my map, but doing so did not appear to shift the underlying labels. My second idea comes from the insight that I notice my points DO shift underlying labels out of the way: add a series of invisible points at the perimeter of the map, but this feels hacky and strange.

What is the best approach for preventing labels from touching the edge of my map?

screen shot 2018-03-30 at 6 15 06 pm

cross-platform feature

Most helpful comment

@ChrisLoer Got it working. Thank you so much for your help! My updated implementation and resulting image are below. I removed the red line because it was only for testing purposes.

var viewport = map.getBounds()

      map.addSource('viewport-line', {
        type: 'geojson',
        data: {
          type: 'Feature',
          properties: {},
          geometry: {
            type: 'LineString',
            coordinates: [
              [viewport._sw.lng, viewport._sw.lat],
              [viewport._sw.lng, viewport._ne.lat],
              [viewport._ne.lng, viewport._ne.lat],
              [viewport._ne.lng, viewport._sw.lat],
              [viewport._sw.lng, viewport._sw.lat]
            ]
          }
        }
      })

      var width = 10
      var data = new Uint8Array(width*width*4)
      map.addImage('pixel',{width: width, height: width, data: data})

      map.addLayer({
        id: 'viewport-line-symbols',
        type: 'symbol',
        source: 'viewport-line',
        layout: {
          'icon-image': 'pixel',
          'symbol-placement': 'line',
          'symbol-spacing': 5
        }
      })

Before:
screen shot 2018-04-25 at 1 26 55 pm

After:
screen shot 2018-04-25 at 1 26 21 pm

Much better. Thanks again!

All 14 comments

Seems like what we'd really need here is a new style-spec property to control whether symbols are clipped or omitted when they cross the viewport boundary. cc @ChrisLoer @ansis

Yeah, the best solution we know of right now is pretty hacky, and it sounds like you've already figured it out -- it's some version of:

  • Generate a line geometry that goes around the edge of the viewport. Unfortunately if the map is moving, that means you have to continually re-generate the geometry as the map moves (by doing something like listening for camera position changes and updating geometry in response).
  • Add a repeating symbol layer that goes along the line to trigger collisions that cross the line. One thing I've seen before is to add an invisible icon with short symbol-spacing.

If it's any consolation, I know of at least one instance in which that hack is successfully used in production with millions of map views! 馃槄

We haven't considered a specific option for making the edge of the viewport trigger collisions before. If it turns out to be an important use-case, it would be pretty straightforward to implement technically. What's we've usually talked about doing is a more general-case "implement collision detection for arbitrary geometries" (as in https://github.com/mapbox/mapbox-gl-js/issues/4704#issuecomment-319192573).

BTW, just looking at your image, it seems like you'd actually only want labels to collide with the _bottom_ edge of the map? That level of specificity seems to me like an argument that we should try to make any support for this as general-purpose as possible.

@ChrisLoer To clarify, the image was cropped. I personally want to prevent collision with any edge.

@ChrisLoer can you kindly point me to an example of implementing a "repeating symbol layer that goes along [a] line?" My google searching is coming up blank.

@astrojams1 Sorry, I don't know of any public examples, but the steps should be something like:

  • Add a GeoJSON source with a "LineString" geometry representing the boundary you want to establish
  • Add an icon layer that references that source, with symbol-placement: line, symbol-spacing: 5 (that's a guess for what will work well?).
  • Use an icon-image that points to a 0 pixel or transparent icon in your style's sprite sheet.
  • Hook map move events to update your GeoJSON source (unfortunately this is a pretty inefficient way to accomplish sticking to the viewport bounds, but hopefully it'll be workable).

Thank you for that, @ChrisLoer. I tried implementing a quick proof-of-concept using your steps. Here is what I came up with:

var viewport = map.getBounds()

      map.addSource('viewport-corners', {
        type: 'geojson',
        data: {
          type: 'Feature',
          properties: {},
          geometry: {
            type: 'LineString',
            coordinates: [
              [viewport._sw.lng, viewport._sw.lat],
              [viewport._sw.lng, viewport._ne.lat],
              [viewport._ne.lng, viewport._ne.lat],
              [viewport._ne.lng, viewport._sw.lat],
              [viewport._sw.lng, viewport._sw.lat]
            ]
          }
        }
      })

      map.addLayer({
        id: 'viewport-line',
        type: 'line',
        source: 'viewport-corners',
        layout: {
          'line-join': 'round',
          'line-cap': 'round'
        },
        paint: {
          'line-color': 'red',
          'line-width': 10
        }
      })

      map.addLayer({
        id: 'viewport-line-symbols',
        type: 'symbol',
        source: 'viewport-corners',
        layout: {
          'icon-image': 'harbor_icon',
          'icon-size': 1,
          'symbol-placement': 'line',
          'symbol-spacing': 5
        }
      })

The red line renders as expected, but I don't see the repeating symbol (see image below). FYI, I am using the light-v9 map style.

screen shot 2018-04-25 at 11 21 02 am

That looks about right to me. I don't know if "harbor_icon" is part of your style, but it's not part of light-v9. I tried adding your code to a map but used "bus-11" instead (this icon's not invisible, you'll have to add your own, one option is https://www.mapbox.com/mapbox-gl-js/example/add-image/).

Anyway, your code seems to work as expected with that change. You can use map.showCollisionBoxes = true to see the mechanics of which things collide against other things (also, once you actually load an invisible icon, turning on the collision boxes will allow you to see where the invisible icon is actually being placed).

screenshot 2018-04-25 12 21 59

@ChrisLoer Got it working. Thank you so much for your help! My updated implementation and resulting image are below. I removed the red line because it was only for testing purposes.

var viewport = map.getBounds()

      map.addSource('viewport-line', {
        type: 'geojson',
        data: {
          type: 'Feature',
          properties: {},
          geometry: {
            type: 'LineString',
            coordinates: [
              [viewport._sw.lng, viewport._sw.lat],
              [viewport._sw.lng, viewport._ne.lat],
              [viewport._ne.lng, viewport._ne.lat],
              [viewport._ne.lng, viewport._sw.lat],
              [viewport._sw.lng, viewport._sw.lat]
            ]
          }
        }
      })

      var width = 10
      var data = new Uint8Array(width*width*4)
      map.addImage('pixel',{width: width, height: width, data: data})

      map.addLayer({
        id: 'viewport-line-symbols',
        type: 'symbol',
        source: 'viewport-line',
        layout: {
          'icon-image': 'pixel',
          'symbol-placement': 'line',
          'symbol-spacing': 5
        }
      })

Before:
screen shot 2018-04-25 at 1 26 55 pm

After:
screen shot 2018-04-25 at 1 26 21 pm

Much better. Thanks again!

We at @nzzdev would greatly appreciate an official style-spec property for this usecase. We will also have to implement this workaround for now.

Is there an official way to do this at this point, or is adding the invisible box the best bet?

I'm currently combining the code in the previous comment with this to prevent the edge crashing after map interaction

map.on('moveend', function () {
    if (map.getLayer('viewport-line-symbols')) map.removeLayer('viewport-line-symbols');
    if (map.getSource("viewport-line")) map.removeSource('viewport-line')
    if (map.hasImage("pixel")) map.removeImage('pixel')
...

@brianjacobs-natgeo at this time, we do not have an official method or option to enable this. The invisible border is still the best way to achieve this functionality. we don't have this on our roadmap right now so there's no timeline for a built-in way to handle this.

I also need this feature and will use this workaround. Thanks for keeping it on your radar.

I will ultimately use this workaround to deal with the issue of labels overlapping an external map legend. I made an issue for that too. https://github.com/mapbox/mapbox-gl-js/issues/9249

Was this page helpful?
0 / 5 - 0 ratings

Related issues

infacq picture infacq  路  3Comments

Scarysize picture Scarysize  路  3Comments

aderaaij picture aderaaij  路  3Comments

PBrockmann picture PBrockmann  路  3Comments

stevage picture stevage  路  3Comments