OpenLayers – How to Snap to a GeoJSON-VT Layer (Vector Tile Layer)

geojsonjavascriptlarge datasetsopenlayerssnapping

I'm using geojson-vt to render a large geojson (originated from OSM) file. This file contains all road from a state (extracted with overpass-turbo way[highway="*"]). I'm buinding a domain-specific application that needs to enable the user to add new roads to the existing geojson. However, I'm unable to snap the drawing cursor to the geojson-vt layer.

To illustrate that, consider a simplified demo with this issue (see fiddle below). This demo includes the geojson of countries limitsand add a drawing cursor that should snap to their boundaries.

JSFiddle: http://jsfiddle.net/mju68Lr4/

  var url = 'https://openlayers.org/en/v3.20.1/examples/data/geojson/countries.geojson';
    fetch(url).then(function(response) {
      return response.json();
    }).then(function(json) {
      var tileIndex = geojsonvt(json, {
        extent: 4096,
        debug: 1
      });
      var vectorSource = new ol.source.VectorTile({
        format: new ol.format.GeoJSON(),
        tileGrid: ol.tilegrid.createXYZ(),
        tilePixelRatio: 16,
        tileLoadFunction: function(tile, tileCoord) {
          var format = tile.getFormat();
          var data = tileIndex.getTile.apply(tileIndex, tileCoord);

          var features = format.readFeatures(
            JSON.stringify({
              type: 'FeatureCollection',
              features: data.features
            }, replacer));
          tile.setLoader(function() {
            tile.setFeatures(features);
            tile.setProjection(tilePixels);
          });
        },
        tileUrlFunction: function(tileCoord) {
          return [tileCoord[0], tileCoord[1], -tileCoord[2] - 1];
        }
      });
      var vectorLayer = new ol.layer.VectorTile({
        source: vectorSource
      });
      map.addLayer(vectorLayer);
      var snap = new ol.interaction.Snap({
          source: vectorSource
      });
      map.addInteraction(snap);

    });

I expected that after adding the geojson-vt (tile layer) I would be able to snap to it, as follow (inside the callback):

  var snap = new ol.interaction.Snap({
      source: vectorSource
  });
  map.addInteraction(snap);

However, I'm getting the following error output (in chrome console):

Uncaught (in promise) TypeError: this.source_.getFeatures is not a function
    at ol.interaction.Snap.getFeatures_ (ol-debug.js:70564)
    at ol.interaction.Snap.setMap (ol-debug.js:70669)
    at ol.Map.<anonymous> (ol-debug.js:41259)
    at ol.Collection.boundListener (ol-debug.js:3441)
    at ol.Collection.ol.events.EventTarget.dispatchEvent (ol-debug.js:3859)
    at ol.Collection.insertAt (ol-debug.js:12466)
    at ol.Collection.push (ol-debug.js:12490)
    at ol.Map.addInteraction (ol-debug.js:41313)
    at (index):106

Am I missing something? It seems that snap is not being able to handle the vector tile layer elements since they are "dynamic". Hence, is it possible to fix this issue and snap the drawing cursor to the geojson-vt layer (a tile layer)? If so, how can I do that?

Best Answer

This works, but it does change the feature geometry projection from tile pixel to EPSG:3857 to do it.

  var url = 'https://openlayers.org/en/v3.20.1/examples/data/geojson/countries.geojson';
  fetch(url).then(function(response) {
    return response.json();
  }).then(function(json) {
    var tileIndex = geojsonvt(json, {
      extent: 4096,
      debug: 1
    });
    var vectorFeatures = new ol.Collection();
    var vectorSource = new ol.source.VectorTile({
      format: new ol.format.GeoJSON(),
      tileGrid: ol.tilegrid.createXYZ(),
      tilePixelRatio: 16,
      tileLoadFunction: function(tile) {
        var format = tile.getFormat();
        var tileCoord = tile.getTileCoord();
        var data = tileIndex.getTile(tileCoord[0], tileCoord[1], -tileCoord[2] - 1);

        var features = format.readFeatures(
            JSON.stringify({
              type: 'FeatureCollection',
              features: data ? data.features : []
            }, replacer));

        var z = tileCoord[0];
        var z2 = 1 << tileCoord[0];
        var tx = tileCoord[1];
        var ty = -tileCoord[2]-1;
        var extent = 4096;

        var transform = function(coordinates, output, dimensions) {
          var dims = dimensions || 2;
          for (var i=0; i<coordinates.length; i+=dims) {
            var x = (coordinates[i] / extent + tx) / z2;
            var y = (coordinates[i+1] / extent + (ty)) / z2;
            var c = [((x - 0.5) * 2) * 6378137 * Math.PI, -((y - 0.5) * 2) * 6378137 * Math.PI];
            coordinates[i] = c[0];
            coordinates[i+1] = c[1];
          }
          return coordinates;
        }

        features.forEach( function(feature) {
          feature.getGeometry().applyTransform(transform);
        });
        console.log(features);
        vectorFeatures.extend(features);

        tile.setLoader(function() {
          tile.setFeatures(features);
          tile.setProjection(ol.proj.get('EPSG:3857'));
        });
      },
      tileUrlFunction: function(tileCoord) {
        return [tileCoord[0], tileCoord[1], -tileCoord[2] - 1];
      }
    });
    var vectorLayer = new ol.layer.VectorTile({
      source: vectorSource
    });
    map.addLayer(vectorLayer);
    var draw = new ol.interaction.Draw({
      type: 'LineString'
    });
    map.addInteraction(draw);
    var snap = new ol.interaction.Snap({
      features: vectorFeatures
    });
    map.addInteraction(snap);
    map.addControl(new ol.control.MousePosition());
  });