1

I have a feature with a polygon and a text attribute. The text attribute is displayed via a text-style above the polygons center. Nothing special and so far so good.

The current behavior:
I can move the map so that a part of the polygon is still visible while the label is outside of the viewport (because the label stays at the center of the polygon).

The wanted behavior:
I can move the map so that a part of the polygon is still visible and the label stays inside the viewport (so it changes its position) until the polygon is fully outside of the viewport and not visible any more.

Is it possible to create such behavior?

2

Solved it with the help of @Mike using the TurfJS framework for the intersection calculation. I'm using TypeScript in an Angular application so my code looks something like this:

private layer: ol.layer.Vector;
private style: ol.style.Style[];
public  map : ol.Map; // is set from the outside

constructor() {
  // creating layer and other stuff
  this.layer = ...

  var labelStyle = new ol.style.Style({
    text : new ol.style.Text({
      text: 'foo',
      font: '9pt sans-serif',
      stroke: new ol.style.Stroke({
        color: 'black',
        width: 0.75
      }),
      backgroundFill: new ol.style.Fill({
        color: 'white'
      }),
      overflow: true
    })
  });

  this.style = [labelStyle];

  this.layer.setStyle((feature) => this.myStyleFunction(feature));
}

private myStyleFunction(feature) {
  let geometry = feature.getGeometry();

  // Only render label for the widest polygon of a multipolygon
  if (geometry.getType() === 'MultiPolygon') {
    const polygons = geometry.getPolygons();
    let widest = 0;
    for (let i = 0, ii = polygons.length; i < ii; ++i) {
      const polygon = polygons[i];
      const width = ol.extent.getWidth(polygon.getExtent());
      if (width > widest) {
        widest = width;
        geometry = polygon;
      }
    }
  }

  const mapSize = this.map.getSize();
  const mapExtent = this.map.getView().calculateExtent([mapSize[0], mapSize[1]]);

  const coordinates = geometry.getCoordinates();
  const polygon = turf.polygon(coordinates);

  const extentPolygon = turf.polygon(ol.geom.Polygon.fromExtent(mapExtent).getCoordinates());

  const clippedGeometry = turf.intersect(polygon, extentPolygon);

  if (clippedGeometry) {

    style[0].setGeometry(new GeoJSON().readGeometry(clippedGeometry.geometry));
    style[0].getText().setText('' + feature.get('KN_MAX') + 'm');
  }
  return style;
}

It even works with multi polygons and shows one label per part of the multi polygon.

1

Yes. The code below which still needs some refinement is based on this example https://openlayers.org/en/v4.6.5/examples/vector-label-decluttering.html The geometry function has been moved to the main style function as it must also determine text alignment, but using an extent reduced by the font size to fit labels avoids the need alter the baseline. The layer's render buffer must be reduced to avoid showing labels for countries which are just out of view.

<!DOCTYPE html>
<html>
  <head>
    <title>Vector Label Decluttering</title>
    <link rel="stylesheet" href="https://openlayers.org/en/v4.6.5/css/ol.css" type="text/css">
    <!-- The line below is only needed for old environments like Internet Explorer and Android 4.x -->
    <script src="https://cdn.polyfill.io/v2/polyfill.min.js?features=requestAnimationFrame,Element.prototype.classList,URL"></script>
    <script src="https://openlayers.org/en/v4.6.5/build/ol.js"></script>
    <script src="https://cdn.polyfill.io/v2/polyfill.min.js?features=Set""></script>
  </head>
  <body>
    <div id="map" class="map"></div>
    <script>
      var map = new ol.Map({
        target: 'map',
        view: new ol.View({
          center: [0, 0],
          zoom: 1
        })
      });

      var labelStyle = new ol.style.Style({
        text: new ol.style.Text({
          font: '12px Calibri,sans-serif',
          overflow: true,
          fill: new ol.style.Fill({
            color: '#000'
          }),
          stroke: new ol.style.Stroke({
            color: '#fff',
            width: 3
          })
        })
      });
      var countryStyle = new ol.style.Style({
        fill: new ol.style.Fill({
          color: 'rgba(255, 255, 255, 0.6)'
        }),
        stroke: new ol.style.Stroke({
          color: '#319FD3',
          width: 1
        })
      });
      var style = [countryStyle, labelStyle];

      var vectorLayer = new ol.layer.Vector({
        source: new ol.source.Vector({
          url: 'https://openlayers.org/en/v4.6.5/examples/data/geojson/countries.geojson',
          format: new ol.format.GeoJSON()
        }),
        style: function(feature) {

          var geometry = feature.getGeometry();
          if (geometry.getType() == 'MultiPolygon') {
            // Only render label for the widest polygon of a multipolygon
            var polygons = geometry.getPolygons();
            var widest = 0;
            for (var i = 0, ii = polygons.length; i < ii; ++i) {
              var polygon = polygons[i];
              var width = ol.extent.getWidth(polygon.getExtent());
              if (width > widest) {
                widest = width;
                geometry = polygon;
              }
            }
          }
          // Check if default label position fits in the view and move it inside if necessary
          geometry = geometry.getInteriorPoint();
          var size = map.getSize();
          var extent = map.getView().calculateExtent([size[0]-12,size[1]-12]);
          var textAlign = 'center';
          var coordinates = geometry.getCoordinates();
          if (!geometry.intersectsExtent(extent)) {
            geometry = new ol.geom.Point(ol.geom.Polygon.fromExtent(extent).getClosestPoint(coordinates));
            // Align text if at either side
            var x = geometry.getCoordinates()[0];
            if (x > coordinates[0]) {
              textAlign = 'left';
            }
            if (x < coordinates[0]) {
              textAlign = 'right';
            }
          }

          labelStyle.setGeometry(geometry);
          labelStyle.getText().setText(feature.get('name'));
          labelStyle.getText().setTextAlign(textAlign);
          return style;
        },
        declutter: true,
        renderBuffer: 1  // If left at default value labels will appear when countries not visible
      });

      map.addLayer(vectorLayer);
    </script>
  </body>
</html>
  • 1
    Thanks for the code, it helped a lot. However I found my own solution (see below) with the help of your code. – theo_retiker Apr 25 at 12:55
  • Yes, my code just keep the labels visible. With the shape of some countries there's no guarantee they would stay on the country. Only turf or similar algorithm could do that. – Mike Apr 25 at 14:17

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.