perliedman / leaflet-routing-machine

Control for routing in Leaflet
https://www.liedman.net/leaflet-routing-machine/
Other
1.06k stars 347 forks source link

Leaflet Routing Machine: How to edit a route before rendering #664

Open antonioOrtiz opened 2 years ago

antonioOrtiz commented 2 years ago

I've created a circle on the map and in that circle I've figured a point with the highest elevation and one with the lowest. (I used this leaflet-plugin to get me that data)

Those two point are my starting and ending points in Leaflet-Routing Machine.

In the Leaflet Routing Machine documentation there is a sub-object called an interface.

LRM API screen grab

When clicking one of them called IRoute it takes you to:

enter image description here

There is a property called coordinates

An array of L.LatLngs that can be used to visualize the route; the level of detail should be high, since Leaflet will simplify the line appropriately when it is displayed

So what I would like is to add some custom points in the route, The end result would be each point from the starting point would be lower in elevation from the last until the end point.

I did a console of my L.Routing.Control instance and see this:

enter image description here

I see the coordinates prop but not sure if that's even the right prop?

So essentially I want to add custom markers/latlang to the route generated.

Thanks in advance!

curtisy1 commented 2 years ago

If you want the markers only for the waypoints you set yourself, then the plan takes a createMarker option where you can return a custom marker

const control = L.Routing.Control({
    plan = L.Routing.Plan({
        createMarker = () => {
            return L.Marker(...)
        },
    }),
});

The coordinates are all the points the routing engine found. So let's say you want to go from A to Z, then the routing engine might suggest you to travel via coordinates B, C, etc. What is returned there is entirely up to the routing engine and plugin implementation. The OSRM plugin for example, does it this way. If you want to work with these more than just your waypoints, it's probably a good idea to implement some form of custom routing backend, where you extend the existing one and modify the result to your liking.

An example of how extending could work (if you're not using typescript) can be found in the mapbox implementation

antonioOrtiz commented 1 year ago

Hi there, Thanks for replying! I appreciate it! Sorry for the tardy reply! I tried what you recommended, but it caused nothing (regarding Markers) to render. This is my Routing Machine:

   const instance = L.Routing.control({
    createMarker: function (i, wp, nWps) {
      if (i === 0) {
        return L.marker(wp.latLng, {
          icon: startIcon,
          draggable: true,
          keyboard: true,
          alt: 'current location'
        })
      }
      if (i === nWps - 1) {
        return L.marker(wp.latLng, {
          icon: finishIcon,
          draggable: true,
          alt: 'current destination'
        })
      }
    },

    plan: new L.Routing.plan({
      createMarker: function (i, wp, nWps) {
        if (i !== 0 || i !== nWps - 1) {
          console.log('wp', wp)
        }
      },
    }),

    waypoints: [
      L.latLng(
        startingPoints[0]?.highestEl?.latlng?.lat,
        startingPoints[0]?.highestEl?.latlng?.lng),
      L.latLng(
        startingPoints[0]?.lowestEl?.latlng?.lat,
        startingPoints[0]?.lowestEl?.latlng?.lng
      ),
    ],
  });

I figured I do a conditional in the plan to see all the Markers which are not the first or last, and then what I would do is push in those coordinates which make the route.

But now that I think about it, won't this create markers using the plan prop? What I would like is to change the lat and lng of the points in between the starting and ending point thereby augmenting the route.

This is my repo if you'd like to see it all the code!

curtisy1 commented 1 year ago

But now that I think about it, won't this create markers using the plan prop

You're absolutely right, not sure how I got to the createMarker function. Sorry about that!

What I would like is to change the lat and lng of the points in between the starting and ending point thereby augmenting the route

Then I believe your only option would be a custom router implementation. In general it could look something like this

const customCallback = (callback) => (context, error, routes) => {
    for (const route of routes) {
        route.coordinates = [...route.coordinates, L.latLng(12.33, 49.6753)];
    }

    callback(context, error, routes);
};

const customRouter = OSRMv1.extend({
        initialize: function(options) {
            L.Routing.OSRMv1.prototype.initialize.call(this, options);
        },

        route: function(waypoints, callback, context, options) {
            const originalCallback = options.callback;
            L.Routing.OSRMv1.prototype.route.call(waypoints, customCallback(originalCallback), this, options);
        }
    });
antonioOrtiz commented 1 year ago

Hey thanks for posting this my friend. I am going to give a whirl now!

antonioOrtiz commented 1 year ago

Hi there again. I tried to integrate your example like so:

   const routingControl = L.Routing.control({
      addWaypoints: false,

      collapsible: true,
      draggableWaypoints: true,

      lineOptions: {
        styles: [{ color: 'chartreuse', opacity: 1, weight: 5 }]
      },
      position: 'bottomright',

      router: L.Routing.OSRMv1.extend({
        initialize: function (options) {
          L.Routing.OSRMv1.prototype.initialize.call(this, options);
        },

        route: function (waypoints, callback, context, options) {
          const originalCallback = options.callback;
          L.Routing.OSRMv1.prototype.route.call(waypoints, customCallback(originalCallback), this, options);
        }
      }),
      routeWhileDragging: true,

      show: true,
      showAlternatives: false,

      waypoints
    }).addTo(map)

But I am getting :

TypeError: this._router.route is not a function

I am trying to debug it but wondering if there is documentation for adding this?

curtisy1 commented 1 year ago

You're on the right track. Unfortunately, there isn't any great example other than the existing implementations. The trick to all of them is that they extend Leaflet's L.Class interface. This is more or less equivalent to plain JS classes where you need to initialize them by calling new ....

So instead of

     router: L.Routing.OSRMv1.extend({
        initialize: function (options) {
          L.Routing.OSRMv1.prototype.initialize.call(this, options);
        },

        route: function (waypoints, callback, context, options) {
          const originalCallback = options.callback;
          L.Routing.OSRMv1.prototype.route.call(waypoints, customCallback(originalCallback), this, options);
        }
      }),

you'll have to do

     const router = L.Routing.OSRMv1.extend({
        initialize: function (options) {
          L.Routing.OSRMv1.prototype.initialize.call(this, options);
        },

        route: function (waypoints, callback, context, options) {
          const originalCallback = options.callback;
          L.Routing.OSRMv1.prototype.route.call(waypoints, customCallback(originalCallback), this, options);
        }
      });

   const routingControl = L.Routing.control({
      addWaypoints: false,

      collapsible: true,
      draggableWaypoints: true,

      lineOptions: {
        styles: [{ color: 'chartreuse', opacity: 1, weight: 5 }]
      },
      position: 'bottomright',

      router: new router(),
      routeWhileDragging: true,

      show: true,
      showAlternatives: false,

      waypoints
    }).addTo(map)
antonioOrtiz commented 1 year ago

Hi there thanks for the information! And the help with this. So it is fair to say, this is just adding props to a function to override the L.Routing.OSRMv1

So when you call .extend and pass that object, you are re-writing props?

Like if you console logged L.Routing.OSRMv1 it would return...

{
  initialize: function (options) {
    L.Routing.OSRMv1.prototype.initialize.call(this, options);
  },

  route: function (waypoints, callback, context, options) {
    const originalCallback = options.callback;
    L.Routing.OSRMv1.prototype.route.call(waypoints, customCallback(originalCallback), this, options);
  }
}

However using what you recommended I am getting this error.

Screen Shot 2022-07-18 at 12 14 14 PM

leaflet-routing-machine.js?0f18:17955 Uncaught TypeError: Cannot read properties of undefined (reading 'routingOptions')
    at Array.route (leaflet-routing-machine.js?0f18:17955:25)
    at NewClass.route (index.js?3b09:31:7)
    at NewClass.route (leaflet-routing-machine.js?0f18:16151:1)
    at NewClass.onAdd (leaflet-routing-machine.js?0f18:15919:1)
    at NewClass.addTo (leaflet-src.js?af6a:4783:1)
    at eval (index.js?3b09:93:8)
    at invokePassiveEffectCreate (react-dom.development.js?ac89:23487:1)
    at HTMLUnknownElement.callCallback (react-dom.development.js?ac89:3945:1)
    at Object.invokeGuardedCallbackDev (react-dom.development.js?ac89:3994:1)
    at invokeGuardedCallback (react-dom.development.js?ac89:4056:1)
    at flushPassiveEffectsImpl (react-dom.development.js?ac89:23574:1)
    at unstable_runWithPriority (scheduler.development.js?bcd2:468:1)
    at runWithPriority$1 (react-dom.development.js?ac89:11276:1)
    at flushPassiveEffects (react-dom.development.js?ac89:23447:1)
    at eval (react-dom.development.js?ac89:23324:1)
    at workLoop (scheduler.development.js?bcd2:417:1)
    at flushWork (scheduler.development.js?bcd2:390:1)
    at MessagePort.performWorkUntilDeadline (scheduler.development.js?bcd2:157:1)
export function RoutingMachine({ startingPoints }) {
  const map = useMap();

  const customCallback = (callback) => (context, error, routes) => {
    for (const route of routes) {
      route.coordinates = [...route.coordinates, L.latLng(12.33, 49.6753)];
    }

    callback(context, error, routes);
  };

  const router = L.Routing.OSRMv1.extend({
    initialize: function (options) {
      L.Routing.OSRMv1.prototype.initialize.call(this, options);
    },

    route: function (waypoints, callback, context, options) {
      console.log("waypoints ", waypoints);
      console.log("callback ", callback);
      console.log("context", context);
      console.log("options ", options);
      const originalCallback = options.callback;

      console.log("originalCallback ", originalCallback);
      L.Routing.OSRMv1.prototype.route.call(
        waypoints,
        customCallback(originalCallback),
        this,
        options
      );
    },
  });

  useEffect(() => {
    if (!map) return;

    const waypoints = [
      L.latLng(
        startingPoints[0]?.highestEl?.latlng?.lat,
        startingPoints[0]?.highestEl?.latlng?.lng
      ),
      L.latLng(
        startingPoints[0]?.lowestEl?.latlng?.lat,
        startingPoints[0]?.lowestEl?.latlng?.lng
      ),
    ];

    const routingControl = L.Routing.control({
      addWaypoints: false,

      collapsible: true,
      createMarker: function (i, wp, nWps) {
        if (i === 0) {
          return L.marker(wp.latLng, {
            icon: startIcon,
            draggable: true,
            keyboard: true,
            alt: "current location",
          });
        }
        if (i === nWps - 1) {
          return L.marker(wp.latLng, {
            icon: finishIcon,
            draggable: true,
            alt: "current destination",
          });
        }
      },
      draggableWaypoints: true,

      fitSelectedRoutes: true,

      geocoder: L.Control.Geocoder.nominatim(),

      lineOptions: {
        styles: [{ color: "chartreuse", opacity: 1, weight: 5 }],
      },
      position: "bottomright",

      routeWhileDragging: true,
      router: new router(),

      show: true,
      showAlternatives: false,

      waypoints,
    }).addTo(map);

    return () => map.removeControl(routingControl);
  }, [map]);

  return null;
}

Also and finally I thought you would be doing something like this with the new router()

const routerInstance = new router()

And then in the Routing control using it like this:

 const routingControl = L.Routing.control({
     ...other props...
      router: routerInstance.route({...pass some kind of option object}),
     ...other props...
    }).addTo(map);

This is the repo if you need it...

Again thanks for the help!

curtisy1 commented 1 year ago

So when you call .extend and pass that object, you are re-writing props?

You could say so. In React terms, extending would probably be like writing a higher order component (HOC).

However using what you recommended I am getting this error.

My bad, I forgot Leaflet requires context as the first argument (unless you bind it to a specific instance?).

L.Routing.OSRMv1.prototype.route.call(
        this, // this one is important. See https://leafletjs.com/examples/extending/extending-1-classes.html
        waypoints,
        customCallback(originalCallback),
        this,
        options
      );

That should work. Also running your project, I noticed you'll have to swap the arguments in the custom callback

const customCallback = (callback) => (context, routes, error) => {
    if(!routes) return
    for (const route of routes) {
        route.coordinates = [...route.coordinates, L.latLng(12.33, 49.6753)];
    }

    callback(context, error, routes);
};

Also and finally I thought you would be doing something like this with the new router() const routerInstance = new router() And then in the Routing control using it like this: const routingControl = L.Routing.control({ ...other props... router: routerInstance.route({...pass some kind of option object}), ...other props... }).addTo(map);

const routerInstance = new router() Would work, since it creates an instance of the custom router class

routerInstance.route({...pass some kind of option object}), This returns nothing, so we can't just pass it to our LRM and say it's a router. LRM takes care of the routing itself, if the waypoints/coordinates change. It only needs a router with a route function to do that. You could set autoRoute to false and call routerInstance.route() manually in a useEffect of some sort if you wanted to

curtisy1 commented 1 year ago

While writing this, I realized the whole extend thing isn't really necessary anymore since JS supports native classes now. So here's a shorter, simpler version of the extend thing using ES6

const customCallback = (callback) => (context, routes, error) => {
  if (!routes) {
    return;
  }

  for (const route of routes) {
    route.coordinates = [...route.coordinates, L.latLng(12.33, 49.6753)];
  }

  callback(context, error, routes);
};

class CustomRouter extends L.Routing.OSRMv1 {
  constructor(options) {
    super(options); // super is L.Routing.OSRMv1
  }

  route(waypoints, callback, context, options) {
    console.log("waypoints ", waypoints);
    console.log("callback ", callback);
    console.log("context", context);
    console.log("options ", options);
    const originalCallback = options.callback;

    super.route(
      waypoints,
      customCallback(originalCallback),
      this,
      options
    );
  }
}