Search code examples
javascriptopenlayersgeocodingopenlayers-6vector-tiles

OpenLayers Inconsistent Hit Detection on MVT Layer Hover Selection


The objective

I'm trying to replicate the "singleselect-hover" feature in this example from the OpenLayers site.

The issue

When I tried to use this implementation, the hit detection was very poor with vtLayer.getFeatures(event.pixel). The documentation for the function states:

The hit detection algorithm used for this method is optimized for performance, but is less accurate than the one used in map.getFeaturesAtPixel()

Indeed, when I switched to map.getFeaturesAtPixel, the performance increased, but the features still does not work entirely as expected.

When I move my pointer over a vector boundary from the outside, it (usually) behaves as expected:

Expected behavior

However, when I move to an adjacent boundary and then back, the feature no longer works:

Unexpected behavior

My code:

proj4.defs(
  'EPSG:6931',
  '+proj=laea +lat_0=90 +lon_0=0 +x_0=0 +y_0=0 +ellps=WGS84 +towgs84=0,0,0,0,0,0,0 +units=m +no_defs'
);
register(proj4);
const projection = get('EPSG:6931');

const osm = new OSM();

const map = new Map({
  target: something,
  layers: [osm],
  view: new View({
    projection: projection,
    zoom: 5,
  }),
})

// Vector Styles
const unselectedStyle = new Style({
  stroke: new Stroke({
    color: 'rgba(50,50,50,0.9)',
    width: 1.2,
  }),
});
const selectedStyle = new Style({
  stroke: new Stroke({
    color: 'white',
    width: 2,
  }),
  fill: new Fill({
    color: 'rgba(0,0,0,0.3)',
  }),
});

const createVtLayer = () => {
  const vtSource = new VectorTileSource({
    tileGrid: new TileGrid({
      extent: [
        -9009964.761231285, -9009964.761231285, 9009964.761231285,
        9009964.761231285,
      ],
      tileSize: 256,
      resolutions: [70390.34969711941],
    }),
    projection: projection,
    format: new MVT({ idProperty: 'some_id' }),
    url:
      geoserverUrl +
      '/gwc/service/tms/1.0.0/' +
      mvtLayerName +
      '@EPSG%3A' +
      projection.getCode().split(':')[1] + // EPSG number of current projection
      '@pbf/{z}/{x}/{-y}.pbf',
  });

  return new VectorTileLayer({
    zIndex: 1,
    source: vtSource,
    style: unselectedStyle,
  });
};

const vtLayer = createVtLayer();

// Local lookup for highlighted features
let selection = null;
const selectionLayer = new VectorTileLayer({
  zIndex: 2,
  source: vtLayer.getSource(),
  style: feature => feature === selection && selectedStyle,
});

if (map && vtLayer && selectionLayer) {
  // Add layers to map once loaded into state
  map.addLayer(vtLayer);
  map.addLayer(selectionLayer);

  // Update styling of selectionLayer on mouse hover
  map.on(['pointermove', 'click'], event => {
    // Send vector metadata to parent component on mouse click
    if (event.type === 'click' && selection) {
      onFeatureSelect(selection);
    }
    map.forEachFeatureAtPixel(
      event.pixel,
      feature => {
        selection = feature;
      }, {
        layerFilter: layer => layer === vtLayer,
        hitTolerance: 1,
      }
    );
    selectionLayer.changed()
  });
}

What I've tried so far

I've tried adjusting the renderBuffer and renderMode parameters in the VectorTile layer, as well as adjusting the hitTolerance option in map.forEachFeatureAtPixel, and I haven't had any luck yet. When I logged the feature id from the feature parameter in forEachFeatureAtPixel, I noticed something strange--when the unexpected behavior occurs, after dragging the pointer over an adjacent boundary line, the selected variable rapidly switches between the two features and assigns the undesired one, until I touch the pointer to a non-adjacent boundary line. Modifying the hitTolerance only causes the selected feature to switch more frequently.

console logging selected features and feature id from forEachFeatureAtPixel parameter

Theories and questions

I'm thinking that maybe my adjacent vectors are overlapping each others boundaries? Or maybe there is a problem with the way the vectors are loaded as MVTs?


Solution

  • Adding an invisible Fill() to the unselectedStyle allowed the layer to be hit-detected and solved my issue!

    // Vector Styles
    const unselectedStyle = new Style({
      stroke: new Stroke({
        color: 'rgba(50,50,50,0.9)',
        width: 1.2,
      }),
      fill: new Fill({
        color: 'rgba(0,0,0,0)',
      }),
    });
    

    Thanks Mike!