mapbox / mapbox-gl-js

Interactive, thoroughly customizable maps in the browser, powered by vector tiles and WebGL
https://docs.mapbox.com/mapbox-gl-js/
Other
11.2k stars 2.22k forks source link

Best option for preventing labels from touching edge of map #6432

Open astrojams1 opened 6 years ago

astrojams1 commented 6 years ago

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

anandthakker commented 6 years ago

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

ChrisLoer commented 6 years ago

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:

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).

ChrisLoer commented 6 years ago

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.

astrojams1 commented 6 years ago

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

astrojams1 commented 6 years ago

@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.

ChrisLoer commented 6 years ago

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

astrojams1 commented 6 years ago

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

ChrisLoer commented 6 years ago

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

astrojams1 commented 6 years ago

@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!

manuelroth commented 5 years ago

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

brianjacobs-natgeo commented 5 years ago

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')
...
ryanhamley commented 5 years ago

@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.

janeschindler commented 4 years ago

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

janeschindler commented 4 years ago

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