TransWikia.com

Interpolate drawn line in Leaflet

Geographic Information Systems Asked by AustrianTrails on April 7, 2021

Is it possible to interpolate points between multiple points on a drawn line in leaflet? I am creating a site where users can draw running routes and see an elevation chart, but currently only where the users clicks is processed – this makes a sharp looking elevation chart. I was looking to interpolate between points so the elevation chart has more points to work with and therefore looks smoother.

I have been looking at Leaflet.GeometryUtil and interpolateOnLine, but it is a bit above me and after a few failed attempts – I’m not sure how to go about it.

Where the drawing takes place:

map.on(L.Draw.Event.CREATED, function(event) {
    var layer = event.layer;
    console.log("layer", layer.getLatLngs());
    
    
    drawnItems.addLayer(layer);
    drawData = layer.toGeoJSON();

    var profile = drawData.geometry.coordinates;

    //reverse coordinates
        function reverseCoordinates(_arr){
            let i;
            let number = []
            let xx
            for(i=0;i<_arr.length;i++){
                xx = _arr[i].reverse()
                number.push(xx)
            }
            return number;
        }
        fetch('https://open.mapquestapi.com/elevation/v1/profile?key=tHXSNAGXRx6LAoBNdgjjLycOhGqJalg7&shapeFormat=raw&latLngCollection='+reverseCoordinates(profile))
          .then(r => r.json()) 
          .then(data => {
            var latlngs = [];
                for (var i=0; i<data.shapePoints.length; i=i+2) {
                    //console.log(data.shapePoints[i])
                    latlngs.push([data.shapePoints[i+1], data.shapePoints[i], data.elevationProfile[i/2].height]);
                }

            geojson = {
                "type": "Feature",
                "geometry": {
                    "type": "LineString",
                    "coordinates": latlngs
                }
            };

One Answer

GeometryUtil is a good option. Right away in your .CREATED event, you grab the latlngs. That is where you want to use interpolateOnLine. interpolateOnLine will get interpolated points for you, but you need to use the .predecessor value of the result to know how to inject that interpolated point back into your original array of latlngs. I found myself in this situation before, and I came up with this function:

/**
 * Calculate size of each segment and inject a midpoint when a segment grows too large
 * @param {array} latlngArray
 * @param {number} maxSegmentSize - segments larger than this will be interpolated
 */
export function interpolateLargeSegments(latlngArray, maxSegmentSize){

   const interpolatedLatLngs = [...latlngArray]

   var addedPoints = []

   // get interpolated points, push to arr addedPoints
   for (let i = 0; i < interpolatedLatLngs.length; i++){

      var nextPointIndex = i === interpolatedLatLngs.length - 1 ? 0 : i + 1
      var currentPoint = interpolatedLatLngs[i]
      var nextPoint = interpolatedLatLngs[nextPointIndex]
      var distanceToNext = map.distance(currentPoint, nextPoint)

      if (distanceToNext > maxSegmentSize) {
         var addedPoint = {
            index: i,
            point: L.GeometryUtil.interpolateOnLine(map, [currentPoint, nextPoint], 0.5).latLng
         }
         addedPoints.push(addedPoint)
      }

   }

   // loop through arr addedPoints, fold points into array at appropriate place 
   // (their own index + the index of their preceeding point + 1)
   for (let i = 0; i < addedPoints.length; i++){
      interpolatedLatLngs.splice( addedPoints[i].index + i + 1, 0, addedPoints[i].point)
   }

   return interpolatedLatLngs

}

This may be more technical than you were looking for, but feel free to treat this function as a black box if you want. It takes your array of leaflet latlngs, looks for segments that are smaller than the maxSegmentSize, and interpolates them, placing a point at 50% of the distance between those points. I found this helpful because you may not want certain segments to be interpolated if your user is drawing them close together already, or if they're drawing a polyline with 100 points and your height profile already has good resolution. If the resultant latlngs array still has points that are farther apart than you'd like, just call this function again on the result of what it returned the first time. You can probably put it on a loop that hits a stop when none of the segments are larger than the maxSegmentSize. There are other ways you can modify this to suit your needs.

You can use this function (or some version of it) right at the start of your .CREATED event. You would use this on your layer's latlngs:

// 
map.on(L.Draw.Event.CREATED, function(event) {

  const layer = event.layer;
  const latlngs = layer.getLatlngs()

  const interpolatedLatLngs = interpolateLargeSegments(latlngs, someThreshholdValue)

Now you need to make a choice, the choice that TomazicM asked you about in his comment. Do you want to use these new interpolated latlngs to draw your path on the map, or do you want to use the original drawn values for rendering the line, but use the interpolatedLatLngs to create your height profile? In the event that you want to use the new interpolatedLatLngs to both draw your path and get your heights, you can do this:

// still in map.on(L.Draw.Event.CREATED, () => { ... })

  const interpolatedPolyline = L.polyline(interpolatedLatLngs, options)
  drawnItems.removeLayer(layer) // remove original drawn layer
  drawnItems.addLayer(interpolatedPolyline); // add new interpolated layer
  drawData = interpolatedPolyline.toGeoJSON();

  var profile = drawData.geometry.coordinates;

  ...

If you want to leave the drawn path as the user drew it, but use the interpolatedLatLngs for your height profile, leave the original layer alone:

// still in map.on(L.Draw.Event.CREATED, () => { ... })

  // leave your original layer
  drawnItems.addLayer(layer);

  // but create your GeoJSON from the interpolated latlng array
  const interpolatedPolyline = L.polyline(interpolatedLatLngs, options)
  drawData = interpolatedPolyline.toGeoJSON();

  ...

This is how I would approach this problem. As I'm not familiar with leaflet Draw, I can't make a working example, but if you want to create a working example in a codesandbox / codepen, I could try to implement this (or another) solution in real code if you like.

Side note: I'm finding it very strange that you're converting your polyline into GeoJSON, and then using a reverseCoordinates function on the geometry feature of that GeoJSON. Why? You're essentially ending up with your latlngs array again. Why bother converting to GeoJSON and back again? Food for thought.

Answered by Seth Lutske on April 7, 2021

Add your own answers!

Ask a Question

Get help from others!

© 2024 TransWikia.com. All rights reserved. Sites we Love: PCI Database, UKBizDB, Menu Kuliner, Sharing RPP