Mkov
Mkov

Reputation: 199

Snapping a draggable point to a vector tile set line in Mapbox GL JS using turf.js

I'm trying to snap a draggable point to a vector tileset that consists of lines, but I'm not sure if it's possible using a Mapbox vector tileset.

It's essentially the equivalent of this point snapping example that uses turf.js https://jsfiddle.net/andi_lo/nmc4kprn/5/ which is outlined in the following stack overflow post: Mapbox Icons/Markers "BearingSnap" or Snap to Position

I've amended a basic Mapbox draggable point example to query the rendered features contained in the tileset. I'm just not sure how to incorporate the measuring and snapping functionality into it. I can see in the console log that the coordinates for the feature that I intersect are returned. Any ideas?

mapboxgl.accessToken = 'pk.eyJ1Ijoic2luc3ctc2NpIiwiYSI6ImNqajd6MHYyZjEyZzUzcnBlNnM1OHFmdXoifQ.ZBT_-d26dSFur2oWzXAQvA';

var map = new mapboxgl.Map({
  container: 'map',
  style: 'mapbox://styles/sinsw-sci/cjl1x0v4489j32qp2nd9swywc',
  center: [151.206, -33.865],
  zoom: 17
});


var canvas = map.getCanvasContainer();


var geojson = {
  "type": "FeatureCollection",
  "features": [{
    "type": "Feature",
    "properties": {},
    "geometry": {
      "type": "Point",
      "coordinates": [151.206, -33.865]
    }
  }]
};


function onMove(e) {
  var coords = e.lngLat;

  // Set a UI indicator for dragging.
  canvas.style.cursor = 'grabbing';

  // Update the Point feature in `geojson` coordinates
  // and call setData to the source layer `point` on it.
  geojson.features[0].geometry.coordinates = [coords.lng, coords.lat];
  map.getSource('point').setData(geojson);

  var features = map.queryRenderedFeatures(e.point, {
    layers: ['snapTo']
  });

  // console.log(features);

  // Change point and cursor style as a UI indicator
  // and set a flag to enable other mouse events.
  if (features.length) {
    console.log(features);
    canvas.style.cursor = 'move';
    isCursorOverPoint = true;
    map.dragPan.disable();
  } else {
    map.setPaintProperty('point', 'circle-color', '#3887be');
    canvas.style.cursor = '';
    isCursorOverPoint = false;
    map.dragPan.enable();
  }


}

function onUp(e) {
  var coords = e.lngLat;
  canvas.style.cursor = '';

  // Unbind mouse/touch events
  map.off('mousemove', onMove);
  map.off('touchmove', onMove);
}

map.on('load', function() {

  // Add a single point to the map
  map.addSource('point', {
    "type": "geojson",
    "data": geojson
  });

  map.addLayer({
    "id": "point",
    "type": "circle",
    "source": "point",
    "paint": {
      "circle-radius": 10,
      "circle-color": "#3887be"
    }
  });


  map.addSource('snap', {
    type: 'vector',
    url: 'mapbox://mapbox.mapbox-streets-v7'
  });



  map.addLayer({
    id: 'snapTo',
    type: 'line',
    source: 'snap',
    'source-layer': 'road',
    'paint': {
      "line-color": "#2AAAFF",
      "line-opacity": 0.5,
      'line-width': 1
    }
  });








  // When the cursor enters a feature in the point layer, prepare for dragging.
  map.on('mouseenter', 'point', function() {
    map.setPaintProperty('point', 'circle-color', '#3bb2d0');
    canvas.style.cursor = 'move';
  });

  map.on('mouseleave', 'point', function() {
    map.setPaintProperty('point', 'circle-color', '#3887be');
    canvas.style.cursor = '';
  });

  map.on('mousedown', 'point', function(e) {
    // Prevent the default map drag behavior.
    e.preventDefault();

    canvas.style.cursor = 'grab';

    map.on('mousemove', onMove);
    map.once('mouseup', onUp);
  });

  map.on('touchstart', 'point', function(e) {
    if (e.points.length !== 1) return;

    // Prevent the default map drag behavior.
    e.preventDefault();

    map.on('touchmove', onMove);
    map.once('touchend', onUp);
  });
});
<!DOCTYPE html>
<html>

<head>
  <meta charset='utf-8' />
  <title>Snap point to vector tileset</title>
  <meta name='viewport' content='initial-scale=1,maximum-scale=1,user-scalable=no' />
  <script src='https://api.tiles.mapbox.com/mapbox-gl-js/v0.48.0/mapbox-gl.js'></script>
  <link href='https://api.tiles.mapbox.com/mapbox-gl-js/v0.48.0/mapbox-gl.css' rel='stylesheet' />
  <style>
    body {
      margin: 0;
      padding: 0;
    }
    
    #map {
      position: absolute;
      top: 0;
      bottom: 0;
      width: 100%;
    }
  </style>
</head>

<body>
  <div id='map'></div>
</body>

</html>

Upvotes: 2

Views: 2247

Answers (1)

Steve Bennett
Steve Bennett

Reputation: 126527

It seems possible to me.

You'll need to make two major changes from that Point example.

First, use queryRenderedFeatures() to fetch all the source vector features that are candidates for snapping to. You will probably want to pass a bounding box around the current mouse location, to limit how far you look for candidates. You will also want to pass a filter for the right layer, and probably limit it to ["==", "$type", "LineString"]

Second, while iterating over every returned line feature, use Turf's nearestPointOnLine() to both calculate the distance to each line, and find the actual nearest point on that line. Something like:

var nearestPoint;
turf.featureEach(snapTo, (feature) => {
    var point = turf.nearestPointOnLine(feature, turf.point([coords.lng, coords.lat]));
    // if the distance of the dragging point is under a certain threshold
    if (!nearestPoint || point.properties.dist < nearestPoint.properties.dist) {
      nearestPoint = point;
    }
  });

if (nearestPoint) {
      // do whatever you do, now that you have the closest point
}

Upvotes: 3

Related Questions