Matrix Routing with ReactJS to Optimize a Shopping Plan
Jayson DeLancey
Posted on November 3, 2021
The annual tradition known as Black Friday leads many to go shopping at retail stores. Getting a route from your current location to another is a straight-forward navigation tool we’ve used many times. If we have multiple stops, how do we plan the most optimal route between them? That’s a job for Matrix Routing.
Project
Morpheus: Unfortunately, no one can be told what the Matrix is. You have to see it for yourself.
For this project, we’ll demonstrate using the HERE Places and Routing APIs with ReactJS to plan a series of stops.
If you need to go to multiple stores such as Walmart, H-E-B, Gamestop and Kohl’s it may not matter which location or order you need to make the stops. To make the best use of your time as seen in the following screen capture, we can search for these stores and see the closest option highlighted in green when taking driving times and traffic into account. As we select a waypoint, the next closest store is highlighted from the remaining groups.
Selecting multiple waypoints leads to an optimized shopping plan. As you build applications, taking into account where somebody is and where they are going can provide a much richer user experience by considering the context of location.
Getting Started
As with other ReactJS tutorials, we’ll start with create-react-app
as a project structure.
We’ll use a few other libraries as well, axios for making HTTP requests, react-bootstrap for ready-made react components of the Bootstrap library, and styled-components because what I’ve always felt like I was missing in my life was the ability to set CSS properties more easily in JavaScript (🖤 web development).
Here are the commands to create the app, install the dependencies, and then start the development web server:
create-react-bootstrap app
cd app
npm install --save styled-components axios react-bootstrap
npm start
- https://github.com/facebook/create-react-app
- https://react-bootstrap.github.io/
- https://www.styled-components.com/
Components
We will develop a few React components that encapsulate the view and behavior of our user interface.
StartLocation.js is a component that will display a form for describing the starting location. We can change the latitude and longitude in the form or click on the globe icon to use our current location.
PlaceSearch.js is a component that displays a form to search for places near our starting location. The results are displayed in a list by distance so that the user can select one.
PlacePlanner.js is a component that is used for planning a route across multiple place searches. It uses the Matrix Routing algorithm to find the next nearest waypoint.
App.js is a component to wrap up everything into an application that includes the StartLocation and PlacePlanner.
App
Starting from the top most component, we define our App which is composed from the StartLocation and PlacePlanner components. We maintain the geocordinates of our origin as state in the App so that when it is changed by the StartLocation component we can keep things consistent in the PlacePlanner by passing them down as properties. You can see the view being defined by the render method:
render() {
return (
<div className="App">
<StartLocation
lat={this.state.start.lat}
lng={this.state.start.lng}
key="MyLocator"
onChange={this.onLocationChanged}
onLocate={this.onLocate}
/>
<Wrapper>
<p>Search for nearby places.</p>
<PlacePlanner
app_id={this.state.here.app_id}
app_code={this.state.here.app_code}
lat={this.state.start.lat}
lng={this.state.start.lng}
/>
</Wrapper>
</div>
);
}
The onLocationChanged()
method passed to the StartLocation component is used for any changes made to the text forms.
onLocationChanged(e) {
e.preventDefault();
let state = this.state;
state['start'][e.target.id] = e.target.value;
this.setState(state);
}
The onLocate()
method is there for handling HTML5 geolocation API to use the current location detected by the browser.
onLocate(e) {
e.preventDefault();
const self = this;
navigator.geolocation.getCurrentPosition(function(position) {
self.setState({
start : {
lat: position.coords.latitude,
lng: position.coords.longitude,
}
});
});
}
StartLocation
The StartLocation is not much more than a simple Bootstrap form for collecting user input since the behaviors are passed in as properties.
render() {
return (
<Wrapper>
<Grid>
<Row>
<Col xs={4} md={4}>
<ControlLabel>Latitude</ControlLabel>
<FormControl
type="text"
bsSize="sm"
id="lat"
key="lat"
value={this.props.lat}
onChange={ this.onChange }
/>
</Col>
<Col xs={4} md={4}>
<ControlLabel>Longitude</ControlLabel>
<FormControl
type="text"
bsSize="sm"
id="lng"
key="lng"
value={this.props.lng}
onChange={ this.onChange }
/>
</Col>
<Col xs={4} md={4}>
<br/>
<Button onClick={this.onLocate}>
<Glyphicon glyph="globe"/>
</Button>
</Col>
</Row>
<Row>
<FormControl.Feedback />
</Row>
</Grid>
</Wrapper>
);
}
In order to render the Glyphicon
you will need to update the public/index.html to pull in the bootstrap css from a CDN. The addition of the <Wrapper>
was just a simple styled-component for additional presentation customization.
const Wrapper = styled.section`
padding: 1em;
background: papayawhip;
`;
PlaceSearch
We’re going to skip over the PlacePlanner component for a moment to take a closer look at the PlaceSearch component first. In this component we start making use of the HERE Location services to search for places.
Digging into the render()
method, we need a form that allows us to enter a search query. The onChange()
and onKeyPress()
are typical behavior of form entry so that state is maintained and the user can either click the search button or press return
to trigger a places search.
<FormGroup><InputGroup>
<FormControl
type="text"
bsSize="sm"
id={"destination" + this.props.idx}
key={"destination" + this.props.idx}
placeholder="Store Name"
onChange={ this.onChange }
onKeyPress={ e => { if (e.key === 'Enter') { this.onSearch(e); }}}
/>
<InputGroup.Addon>
<Glyphicon glyph="search" onClick={ this.onSearch } />
</InputGroup.Addon>
</InputGroup></FormGroup>
Additionally in the render()
method we are displaying the search results but we’ll come back to that. The Places API can be used for finding specific places with a text string. Unlike with the HERE Geocoder, this is not matching by address but by the name of a place and returning a set of results. You can use the HERE Maps API for JS which includes functions for displaying places. You’d use the same trick of window.H
as described in the tutorial on how to Use HERE Interactive Maps with ReactJS to make it work. Since this project isn’t displaying a map and is just a simple GET request, I am using axios.
onSearch(e) {
const self = this;
axios.get(
'https://places.api.here.com/places/v1/discover/search',
{'params': {
'app_id': self.props.app_id,
'app_code': self.props.app_code,
'q': self.state.q,
'size': 10,
'at': self.props.lat + ',' + self.props.lng
}}).then(function (response) {
self.setState({results: response.data.results.items});
self.addPlaces(self.props.idx, response.data.results.items, self.props.lat, self.props.lng);
});
}
A few notes about the parameters. You need the app_id and app_code typically used with any HERE developer account. The text query is given by the q parameter. I’ve limited the size to the 10 closest matches based on distance and given the at as the location from which to do a proximity search.
Once the results are fetched from the request, we call setState
which triggers the component to re-render as part of the typical React lifecycle. The rest of the render() method will use these results to display the search result listings.
We have a helper method called decodeVicinity()
to help process our Places response. The attribute typically has a HTML <br/>
element which we don’t want so can strip it out.
const decodeVicinity = function(raw) {
var e = document.createElement('div');
e.innerHTML = raw;
return e.childNodes[0].nodeValue;
}
In our render()
implementation we also loop over the results to make a list of items. Each item represents a place from our search in a list. In addition to simply listing all the matches there are two cases to handle. First, if the user has made a selection the className can be set to active which will cause it to be highlighted by Boostrap. Second, if the item is the next closest destination we will color it green by using the bsStyle attribute and setting it to success.
// Build up listing of locations that match query
let destinations = [];
if (self.state.results.length > 0) {
self.state.results.forEach(function(item) {
let option = (
<ListGroupItem
id={item.id}
key={item.id}
onClick={self.onSelect}
header={item.title}
className={self.state.selected === item.id ? "active" : ""}
bsStyle={self.props.nearest === item.id ? "success" : "info" }
>
<Fragment>{decodeVicinity(item.vicinity)}</Fragment>
</ListGroupItem>
);
destinations.push(option);
// ({item.position[0]}, {item.position[1]})
});
}
These items are then simply included in a <ListGroup>
.
<ListGroup>
{ destinations }
</ListGroup>
When a user selects one of the destinations, in addition to highlighting it making it active there is some behavior. By making a selection of a waypoint, this should trigger a new search for the next closest destination among remaining groups.
onSelect(e) {
this.setState({'selected': e.currentTarget.id});
this.props.findNearest(this.props.idx);
}
The findNearest()
method is defined in the PlacePlanner component and passed down as a property so we’ll look at that component next.
PlacePlanner
The PlacePlanner component handles the optimization of our path by using the Matrix Routing algorithm across multiple places.
The render()
method makes use of our reusable <PlaceSearch/>
component. A few properties are passed down for its behaviors as well as the methods defined in this component for identifying the next nearest destination and maintaining state of which places have been selected. The full definition of this method can be found in the full source code listing from the github repository.
<PlaceSearch
idx={0}
app_id={ this.state.app_id }
app_code={ this.state.app_code }
lat={ this.props.lat}
lng={ this.props.lng}
nearest={ this.state.nearest.id }
addPlaces={ this.addPlaces }
findNearest={ this.findNearest }
/>
The addPlaces()
method is called from the PlaceSearch component to maintain a list of all potential destinations. That is, from our starting location by searching for “Walmart” we found 10 possible locations of Walmart we could go to. As we search for “H-E-B” we identify 10 more possible locations for a total of 20 different options to choose from. The selected state is being maintained because once we’ve identified a single Walmart to go to, we limit our list of remaining options to the 10 H-E-B locations. That’s what the following snippet demonstrates.
addPlaces(idx, results, lat, lng) {
// Update places with new search results
let places = this.state.places;
places[idx] = results;
// Combine all results across searched places where a selection has
// not yet been made as our options for the next destination
let options = [];
for (var p in places) {
if (typeof this.state.selected[p] === 'undefined') {
for (var o in places[p]) {
options.push({
lat: places[p][o].position[0],
lng: places[p][o].position[1],
id: places[p][o].id,
});
}
}
}
// If there are no more options then we are done searching
if (options.length === 0) {
return;
}
// To be continued
...
}
Given a list of 40 potential options, how do I get started? The HERE Routing API supports Requesting a Matrix of Routes. This means that given N starting locations and M destination locations we can query the cost factor of going to each as a matrix. The cost factor is a representation in our case of the time it would take by driving in traffic to get to a given location. From the destination with the lowest cost factor, we can make a recommendation for the optimized path across multiple places. Once at that next destination, we can further compute from the remaining options the next best location to route to.
In our case, we are looking at a 1:M query as demonstrated in the next snippet:
addPlaces(idx, results, lat, lng) {
...
// continuing from above
// Will build parameters including all of the potential destinations
let params = {
'app_id': this.state.app_id,
'app_code': this.state.app_code,
'mode': 'fastest;car;traffic:enabled',
'matrixAttributes': 'ix,su',
'summaryattributes': 'all',
'start0': lat + ',' + lng,
}
for (var i = 0; i < options.length; i++) {
params['destination' + i] = options[i].lat + ',' + options[i].lng;
}
// Calculate matrix routing among options to make a recommendation
const self = this;
axios.get(
'https://matrix.route.api.here.com/routing/7.2/calculatematrix.json',
{'params': params}).then(function(response) {
const matrix = response.data.response.matrixEntry;
let nearest = matrix[0].summary;
nearest['id'] = options[0].id;
for (var i = 0; i < matrix.length; i++) {
if (matrix[i].summary.costFactor < nearest.costFactor) {
nearest = matrix[i].summary;
nearest.id = options[i].id;
}
}
self.setState({
nearest: nearest
})
});
this.setState({places: places});
}
We are using the location id from our options so that the property can be used in our PlaceSearch component for highlighting.
Summary
Neo: I’m going to show them a world without you. A world without rules and controls, without borders or boundaries. A world where anything is possible. Where we go from there is a choice I leave to you.
This is not a ready-made production application but hopefully gives you a flavor of how and why to start using some of the HERE Location Services like Places and Routing in your applications. An obvious next step would be to display a map like described in Use HERE Interactive Maps with ReactJS to place markers and the route.
You can find source code listings for the components mentioned in this post on GitHub.
Posted on November 3, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.