Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
antonioOrtiz opened this issue Jul 4, 2022 · 9 comments
Open

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

antonioOrtiz opened this issue Jul 4, 2022 · 9 comments
Labels

Comments

@antonioOrtiz
Copy link

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
Copy link
Collaborator

curtisy1 commented Jul 4, 2022

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
Copy link
Author

antonioOrtiz commented Jul 9, 2022

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
Copy link
Collaborator

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
Copy link
Author

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

@antonioOrtiz
Copy link
Author

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
Copy link
Collaborator

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
Copy link
Author

antonioOrtiz commented Jul 18, 2022

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
Copy link
Collaborator

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
Copy link
Collaborator

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
    );
  }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

2 participants