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.1k stars 2.21k forks source link

Add text clipping option on symbol layers #4064

Open pixnlove opened 7 years ago

pixnlove commented 7 years ago

Hi everybody !

I have a problem with my symbol layer : my text is not hidden by other symbols, how to do like on the second screenshot (screenshot from https://roadtrippers.com who is using Mapbox too)? FYI, my layer looks like :

var layer = {
        "id": "marker",
        "type": "symbol",
        "source": "markers",
        "layout": {
          "text-font": ["Lato Black"],
          "text-padding": 20,
          "text-anchor": "top",
          "text-offset": [0, -2.3],
          "text-size": 14,
          "text-allow-overlap": true,
          "text-field": "{index}",
          "text-justify": "center",
          "icon-offset": [0, -22],
          "icon-image": "marker",
          "icon-allow-overlap": true
        },
        "paint": {
          "text-color": "#FFFFFF"
        }
     };

issue_mapbox

mollymerp commented 7 years ago

Hi @pixnlove, I would try setting "text-allow-overlap": false and "text-optional": true. Also, Roadtrippers is using the Marker class, not a symbol layer.

This doesn't appear to be a bug or feature request, and unfortunately we don't have the bandwidth to offer support on Github. I suggest you post future questions like this on StackOverflow.

pixnlove commented 7 years ago

Thanks for your help @mollymerp. I think it's a feature request but maybe I'm doing something wrong. When I'm adding these two properties, I'm losing some text values and text index behaves badly. My map looks like now:

capture d ecran 2017-02-01 a 18 05 24

And my layer is now:

var layer = {
        "id": "marker",
        "type": "symbol",
        "source": "markers",
        "layout": {
          "text-font": ["Lato Black"],
          "text-padding": 20,
          "text-anchor": "top",
          "text-offset": [0, -2.3],
          "text-size": 14,
          "text-allow-overlap": true,
          "text-field": "{index}",
          "text-justify": "center",
          "icon-offset": [0, -22],
          "icon-image": "marker",
          "icon-allow-overlap": true
        },
        "paint": {
          "text-color": "#FFFFFF"
        }
     };

Using marker like Roadtrippers is not very powerful with a large dataset, I would like to do the same thing with a symbol layer but you seem to consider the text as an other layer. It will be cool to have a layout property "text-clip-icon".

mollymerp commented 7 years ago

@pixnlove I see what you mean. I think this might be a technical limitation of our current implementation of symbol layers – we draw all icons in a layer, and then all text on top of that, so there is no maintained relationship between a single icon and its text label in the render order. I agree this would be a great enhancement, but it would require a significant refactor.

pixnlove commented 7 years ago

@mollymerp, do you have some news about this feature request ?

mollymerp commented 7 years ago

@pixnlove this is not on our immediate roadmap unfortunately.

nrako commented 7 years ago

I have the same undesired behavior but with a symbol layer. Using an icon with some inner text, I'm using that workaround with text-optional. But that isn't a really neat result. I wish the text and icon/marker wouldn't appear to be drawn on two different layer.

screen shot 2017-04-10 at 09 34 06 An example here, 10 collides with 9 and therefore is hidden, but 9 is still above the icon of 10 πŸ˜‘.

jfirebaugh commented 7 years ago

Equivalent issue for native: https://github.com/mapbox/mapbox-gl-native/issues/8235.

wthorp commented 6 years ago

I'll go ahead and upvote this feature.

sjorsrijsdam commented 6 years ago

For those interested, we also ran into this issue and we "solved" it by generating icons on the fly on a off-screen canvas and using token substitution to match the correct image with the correct point.

Roughly what we did is:

prices.forEach((price) => {
    const imageData = createIcon(price);
    map.addImage('house-' + price, imageData);
});

map.addLayer({
    layout: {
        'icon-name': 'house-{price}'
    }
});
nrako commented 6 years ago

Oh that's a very interesting approach! Thanks @sjorsrijsdam

nrako commented 6 years ago

@sjorsrijsdam – I was looking at the API doc for map#addImage and I noticed the following :

An Map#error event will be fired if there is not enough space in the sprite to add this image.

Then I found this information:

Sprites can have a maximum size of 1024x1024 pixels (2048x2048 for high DPI displays) – that means the whole sprite containing all icons must be smaller than 1024x1024 pixels.

Did you ever encountered that issue, what type of scale do you have experience with? Could you confirm that 1024x1024 is the limit?

nrako commented 6 years ago

Not sure this limit is actually enforced... I can't spot anything on Map.addImage on Style.addImage nor the ImageManager.addImage

πŸ€”

sjorsrijsdam commented 6 years ago

@nrako We never ran into limits of the sprite size. We did have to abandon this idea though because of performance problems on mobile. Every time you would move or zoom the map, Mapbox would need to transfer the icon data of each icon to the WebGL context. That turned out to be a bottle neck.

nrako commented 6 years ago

@sjorsrijsdam – Thank you for sharing your experience. I'm not totally sure I understand why Mapbox would need to transfer all images data to the WebGL context on all "camera change" – my quick research on that topic was vain. I might just try and inspect that by myself.

Could you share what approach you ended up taking?

jfirebaugh commented 6 years ago

Mapbox GL JS does not transfer all images data to the WebGL context on every camera change -- only when a tile is loaded or the image is changed.

sjorsrijsdam commented 6 years ago

We ended up using just a single image for all items on the map and used a popup to show the price. I probably should have added that we also fetched new data for that layer on every moveend en zoomend. So, it was probably the setData call that would trigger a regeneration of tiles.

Sirpion commented 6 years ago

At the moment it turns out that there are 3 possible options (to create behavior as in the image in the first post): 1. Use the markers https://bl.ocks.org/stevage/23d881a66e2bcca385d8cc074691b674 2. Generating icons on the fly https://jsfiddle.net/gxc5ca9d/15/ 3. Adding each icon to a new layer https://codepen.io/Sirpion/pen/MXLvbO/

Suppose there is a need to create> 400 labels with texts that will be moved on the map in real time. Which of these options will cause less damage to performance? Or there were new options to implement this behavior (without hiding the text in a collision)?

andrewharvey commented 6 years ago

One solution to this may be to group these layers in a way that tells the renderer to draw these on a per feature level first. ie. feature A layer 1,2,3 . then render feature B layer 1,2,3, then composite those two.

kapone3047 commented 5 years ago

This is a ridiculously simple and common use case that I can't believe is not supported. Have people found any practical workarounds for this?

mushon commented 4 years ago

Almost 3 years after this issue was opened, I still can't believe there is no proper solution from Mapbox to this basic issue. This is causing us major grief and is a huge blindspot for Mapbox

Screen Shot 2019-10-27 at 17 45 10

Please tell me this is already fixed and I'm just looking in the wrong place

vjeranc commented 4 years ago

There seems to be a symbolTextAndIcon shader present. It draws the text and the icon at appropriate times but is not used for the purposes of our symbol layers.

https://github.com/mapbox/mapbox-gl-js/commit/1215bf138dc4982d8bcbc9c61dd3995ae4df3c0f

I've tried it out, drawing an image in the text-field and it does work. Although, it would be nice that something like below works:

'text-field': ['format',
          ['get', 'number_field'], { text-offset: [0, 0.5] },
          "\n", {},
          ['image', ['get', 'icon_field']], { text-offset: [0, 0.5] }
        ],

I guess, given the shader code one could make their own renderer/painter/customlayer for drawing text and icon in a single pass.

gotestrand commented 3 years ago

Hi, any news on this feature request? Any possible workaround?

gitcatrat commented 3 years ago

Sorry, repeating the question: has anyone found reasonable workaround?

lukashass commented 3 years ago

@gotestrand @gitcatrat We ended up generating icons on the fly with #map.event:styleimagemissing as suggested above.

Have a look at Map.vue#L258. We did not notice any performance problems, but keep in mind our icons are limited to only around 200 different ones. You can see the result of this at https://kiel-live.github.io/map

gitcatrat commented 3 years ago

@lukashass Not really an option because text-field contains a float that can be anything from 0 to millions.

tristyntech commented 3 years ago

I don't know if this will work for everyone, but this worked for me. On the bottom you can see a number of the circles do NOT have the text overlay. On the top you can see it is fixed. The way I did it was: 1) set the text symbol layers 'text-padding': 0. The text padding defaults to 2, so it makes the text clustering a little over-sensitive 2) 2 and 3 digit numbers have a wider radius than the single digit numbers (obviously). So for the 2 digit numbers I made the text smaller than the single digit numbers. And I made the 3 digit numbers slightly smaller than that, using an expression. I wish that I was able to keep the text the same size but this was my work around. And that's it. Hope it helps someone.

Screen Shot 2021-01-26 at 1 07 55 AM Screen Shot 2021-01-25 at 2 56 05 PM
xabbu42 commented 3 years ago

this seems to be similar to the issue #10002 and could also be helped by #10123

julianmlr commented 3 years ago
      let markers = {};

      this.realEstates.features.forEach(realEstate => {
        let el = document.createElement('div');
          el.innerHTML = "<span style=\"font-family:quicksand\" class='text-body-1'>" + this.$n(realEstate.properties.price, 'currencyNoCents') + "</span>" ;
        el.className = 'rounded-label';
        markers[realEstate.id] = new Mapbox.Marker(el)
            .setLngLat(realEstate.geometry.coordinates)
            .addTo(this.map);
      })

.rounded-label { background: white; padding: 0.25em 0.5em; font-size: 16pt; border-radius: 1em; border:1px solid lightgrey;

}

Here another version of this here https://bl.ocks.org/stevage/23d881a66e2bcca385d8cc074691b674 Makers really have a nice behavior. Maybe this helps you.

image

pratik-kanthi commented 2 years ago

@julianmlr markers aren't as performant as symbols when you have to draw hundreds of them

Merynek commented 1 year ago

Any updates? MapBox still has this issue, really?

vjeranc commented 1 year ago

@Merynek

mapbox-gl-js isn't under an open/permissive license anymore. Any contributions coming from Mapbox will probably align with their business goals.

Any other company is probably using the older version that is under a permissive license and changes made to that would be in some public fork. Not sure if it exists.

https://github.com/maplibre/maplibre-gl-js This seems to be a fork.

And here's the same issue https://github.com/maplibre/maplibre-gl-js/issues/49

The reason why this is still not implemented is because it requires significant nontrivial changes to the shaders that render elements.

gtsl-2 commented 10 months ago
  1. Set geojson to connect all the id's that go into cluster and set them to the id of clusterProperties.

    cluster: true,
    clusterMaxZoom: 20,
    clusterRadius: 100,
    clusterProperties: {
      id: ["concat", ["get", "id"]],
    1. Next, for each rendering, if the source's multiple cluster ids overlap, only the first one should be used. Then, Marker is created from feature3 data.
    map.current.on("render", async function renderListener() {
     const features = map.current.querySourceFeatures("spotsources");
     const features2 = Object.values(
        features.reduce((acc, item) => {
          acc[item.properties.id] = item;
          return acc;
        }, {})
      );
    γ€€function filterArray(array) {
        var result = [];
        for (let i = 0; i < array.length; i++) {
          var isSubstring = false;
          for (let j = 0; j < array.length; j++) {
            if (i !== j) {
              if (array[j].properties.id.includes(array[i].properties.id)) {
                isSubstring = true;
                break;
              }
            }
          }
          if (!isSubstring) {
            result.push(array[i]);
          }
        }
        return result;
      }
      const features3 = filterArray(features2);

    This way, there is absolutely no overlap of multiple clusters!

elliotclements commented 3 weeks ago

Anyone found a solution to this? I was hoping this would be an easy fix.

We're not using clustering and are visualizing several assets on the map. When assets overlap we have issues where the text disappears.

We have 2x Layers. 1 layer for the background circles, and 1 layer as a symbol which holds the text. Both layers are set to the same data source.

Is there a concept of hierarchy whereby assets stack behind one another if they're overlapping.

Screenshot 2024-09-04 204616

tristan-garaud commented 3 weeks ago

Having the same issue. This is insane that it has been running for the past 7 years... . I hope I'm missing something here.

alexshalamov commented 1 week ago

@tristan-garaud @elliotclements One possible solution is to use symbol-sort-key to enforce the feature rendering order. Please check this render test for reference.

tristan-garaud commented 1 week ago

@alexshalamov Oh wow, I was sure that I try this approach without success. Did it again with the example and it worked. Don't know what I missed the first time but thanks, it works.

elliotclements commented 1 week ago

Thanks Alex! I've been looking for this solution for months. I combined it with an SDF image to allow me to dynamically change the colour πŸ‘Œ