Hands on

Who wants ice cream!? — A HERE Maps API for JavaScript Tutorial (Part 4: Advanced Routing)

By Richard Süselbeck | 27 October 2016

Who wants ice cream?! — In this series of blog posts we are going to develop a small web application called “Gelary” using HERE maps and services. Gelary aims to disrupt the ice-cream market by enabling ice-cream producers to deliver their sweet goods directly to their customers, wherever they are.

The final application will resemble a mobile dashboard view for the employees of our little start-up which they can either take on the road or use at Gelary HQ to plan their work. It will consist of a map and a floating control panel behind a menu button. The map is used to visualise a route, starting from the user's current position along a series of customers, and the traffic situation along the route. The control panel will enable our delivery drivers to search for nearby ice cream shops, display turn-by-turn directions as well as to calculate the best pick-up location for a group of selected customers.The app will also be able to handle order changes: e.g. if a customer cancels or updates his or her position, the route will automatically be recalculated.

To keep things simple our application will use plain Javascript (ES5) and front-end technologies and will target mobile and desktop browsers alike.

What will we learn in this tutorial?

In the previous post we finally got to work with the routing APIs. We started by calculating a route between two locations and drew the result as PolyLine onto our map.

Today we'll start by refactoring our code a bit and then improve our routing functionality. We will display multiple routes as well some detailed information for each and enable the user to select the best one. Let's dive in!

Refactoring

As our little application is getting a more and more involved our codebase grows as well. In order to be able to move forward without losing our sanity we will want to take a break every now and then to clean up after ourselves a bit. Let's do this right now.

We have already have moved most of the code related to routing into a class called HERERoute which lives in a separate file called route.js.

The rest of our code seems to describe the following items:

  • Map
  • Utilities

We'll assume for the time being that it makes sense to split our code accordingly into files and objects or classes.

Map

Let's start with the map. We create a new file called map.js inside our scripts folder, include it in index.html just before app.js and add the following code:

  function HEREMap (mapContainer, platform, mapOptions) {
  this.platform = platform;
  this.position = mapOptions.center;
   
  var defaultLayers = platform.createDefaultLayers();
   
  // Instantiate wrapped HERE map
  this.map = new H.Map(mapContainer, defaultLayers.normal.map, mapOptions);
   
  // Basic behavior: Zooming and panning
  var behavior = new H.mapevents.Behavior(new H.mapevents.MapEvents(this.map));
   
  // Watch the user's geolocation and display it
  navigator.geolocation.watchPosition(this.updateMyPosition.bind(this));
   
  // Resize the map when the window is resized
  window.addEventListener('resize', this.resizeToFit.bind(this));
  }
   
  HEREMap.prototype.updateMyPosition = function(event) {
  this.position = {
  lat: event.coords.latitude,
  lng: event.coords.longitude
  };
   
  // Remove old location marker if it exists
  if (this.myLocationMarker) {
  this.removeMarker(this.myLocationMarker);
  }
   
  this.myLocationMarker = this.addMarker(this.position);
  this.map.setCenter(this.position);
  };
   
  HEREMap.prototype.addMarker = function(coordinates) {
  var marker = new H.map.Marker(coordinates);
  this.map.addObject(marker);
   
  return marker;
  };
   
  HEREMap.prototype.removeMarker = function(marker) {
  this.map.removeObject(marker);
  };
   
  HEREMap.prototype.resizeToFit = function() {
  this.map.getViewPort().resize();
  };
view raw gelary-part4-1.js hosted with ❤ by GitHub

It appears that we have changed a lot, but on a closer look we have only organised the map related functionality a bit better:

  • Constructor (HEREMap()) — Our class essentially wraps the HERE map class for our use case. In the constructor we're creating the map object, defining a bunch of instance variables and event listeners for resizing of the window and the change of the user position.
  • updateMyPosition — This method is called when the browser detects a change in the user's position. It will centre the map and render the user position marker accordingly.
  • addMarker(coordinates) — Adds a new marker at the provided coordinates.
  • removeMarker(marker) — For good measure, let's add a method to remove a marker from the map as well.
  • resizeToFit() — This method is used to update the centre point and size of the map if the browser window is resized.

With the map class in place we can now replace most of the initialisation code in app.js with a single line:

  var map = new HEREMap(mapContainer, platform, mapOptions);
view raw gelary-part4-2.js hosted with ❤ by GitHub

Neat! — The keen observer might have noticed, though, that we're missing a crucial piece of previously implemented functionality now — routing. Let's fix that by adding one more method to our new map class:

  HEREMap.prototype.drawRoute = function(fromCoordinates, toCoordinates) {
  var routeOptions = {
  mode: 'fastest;car',
  representation: 'display',
  waypoint0: locationToWaypointString(fromCoordinates),
  waypoint1: locationToWaypointString(toCoordinates)
  };
   
  this.routes = new HERERoute(this.map, this.platform, routeOptions);
  };
view raw gelary-part4-3.js hosted with ❤ by GitHub

With the drawRoute method in place we can now call it from within our updateMyPosition method:

  if (!this.route) {
  this.drawRoute(this.position, HEREHQcoordinates);
  }
view raw gelary-part4-4.js hosted with ❤ by GitHub

Once our user's location is available we'll render the route.

Utilities

Our map utilises a small helper function called locationToWaypointString to format a latitude/longitude pair for the use with our APIs. Let's create a Utils object to hold this and similar functions in the future and then move it into it's own file scripts/utils.js. Don't forget to include it before the route.js in our index.html file:

  var Utils = {
  locationToWaypointString: function(coordinates) {
  return 'geo!' + coordinates.lat + ',' + coordinates.lng;
  }
  };
view raw gelary-part4-5.js hosted with ❤ by GitHub

Of course we also still have to update the function calls to reflect the change within maps.js:

  var routeOptions = {
  mode: 'fastest;car',
  representation: 'display',
  waypoint0: Utils.locationToWaypointString(fromCoordinates),
  waypoint1: Utils.locationToWaypointString(toCoordinates)
  };
view raw gelary-part4-6.js hosted with ❤ by GitHub

And with this change in place our app.js file looks once again nice and tidy:

  var mapContainer = document.getElementById('map-container');
   
  var platform = new H.service.Platform({
  app_id: ‘…’, // // <-- ENTER YOUR APP ID HERE
  app_code: ‘…’, // <-- ENTER YOUR APP CODE HERE
  });
   
  var HEREHQcoordinates = {
  lat: 52.530974,
  lng: 13.384944
  };
   
  var mapOptions = {
  center: HEREHQcoordinates,
  zoom: 14
  };
   
  var map = new HEREMap(mapContainer, platform, mapOptions);
view raw gelary-part4-7.js hosted with ❤ by GitHub

If you refresh your browser everything should work just as it did before.

Eventually it will make sense to also simplify our HERERoute class a bit, create a specialised class for markers and extract some of the configuration into a separate object but let's save that for after the new routing features are implemented.

Multiple routes

A delivery driver likely knows his or her city better than anyone and we should account for that. Instead of dictating the one and only route the driver must take, let's offer a set of multiple routes to chose from. As it turns out, the HERE APIs make this very easy.

By passing an integer via the alternatives key when requesting routing information from the API we will be provided additional options. In our case, simply update the drawRoute method of the HEREMap class:

  HEREMap.prototype.drawRoute = function(fromCoordinates, toCoordinates) {
  var routeOptions = {
  mode: 'fastest;car',
  representation: 'display',
  alternatives: 2,
  waypoint0: Utils.locationToWaypointString(fromCoordinates),
  waypoint1: Utils.locationToWaypointString(toCoordinates)
  };
   
  this.routes = new HERERoute(this.map, this.platform, routeOptions);
  };
view raw gelary-part4-8.js hosted with ❤ by GitHub

Now the API will return us a primary route as well as 2 alternatives — 3 routes in total.

When refreshing the browser, nothing should have changed as we're currently simply selecting the first route from the response set for our visualisation anyway.

Let's update that class and enable it to handle multiple routes.

First-off, the code which currently draws the route will need to be called repeatedly now so let's move it into it's own local function beneath the onError callback:

  var drawRoute = function(route) {
  var routeShape = route.shape;
  var strip = new H.geo.Strip();
   
  routeShape.forEach(function(point) {
  var parts = point.split(',');
  strip.pushLatLngAlt(parts[0], parts[1]);
  });
   
  var routeLine = new H.map.Polyline(strip, {
  style: { strokeColor: ‘blue’, lineWidth: 3 }
  });
   
  map.addObject(routeLine);
  // map.setViewBounds(routeLine.getBounds());
  };
view raw gelary-part4-9.js hosted with ❤ by GitHub

This change allows us to significantly simplify the onSuccess handler:

  var onSuccess = function(result) {
  if (result.response.route) {
  var routes = result.response.route;
  routes.forEach(drawRoute);
  }
  };

Setting view bounds

Very tidy indeed! You might have noticed that we have commented the call to the map's setViewBounds method out. That's because this line would resize the map only according to the last route in the set which might lead to less than optimal results in certain situations.

A simple way around this is to add all routes to a group (H.map.group) and then use the bounds of the group to update the map.
This requires some changes to the onSuccess handler:

  var onSuccess = function(result) {
  if (result.response.route) {
  var routes = result.response.route;
  var routeLines = routes.map(drawRoute);
  var routeLineGroup = new H.map.Group({ objects: routeLines });
  map.addObject(routeLineGroup);
  map.setViewBounds(routeLineGroup.getBounds());
  }
  };

Additionally we need to update the drawRoute method as well.

As we're no longer adding the individual routes to the map but rather the group, we have to remove the following line:

  map.addObject(routeLine);

Furthermore we're now calling drawRoute using JavaScript's Array#map method which requires us to return the routeLine in order to add them to the group. Hence we have to end the function with

  return routeLine;

So far so good — let's refresh our browser. We should now see three routes instead of one.

multiple_routes.png

Visual tweaks: custom markers

So far we're using default markers for the current position of the user and we're not even rendering any marker for origin and destination. Let's change that! In our first tutorial we have learned how to use custom markers. Why not put this knowledge to good use now?

As we need three different icons for origin, destination and the user's current position we'd like to be able to pass a type identifier when calling our HEREMap#addMarker method. Here's an example:

  this.addMarker(coordinates, 'destination');

As each icon might have a different size and anchor point we will need to define a dictionary to hold this information. By defining an empty markerOptions object up-front and checking whether the requested icon has been defined in the dictionary we can ensure that the default icon is used if necessary. Here's the whole method including these updates:

  HEREMap.prototype.addMarker = function(coordinates, icon) {
  var markerOptions = {};
   
  var icons = {
  iceCream: {
  url: './images/marker-gelato.svg',
  options: {
  size: new H.math.Size(26, 34),
  anchor: new H.math.Point(14, 34)
  }
  },
  origin: {
  url: './images/origin.svg',
  options: {
  size: new H.math.Size(30, 36),
  anchor: new H.math.Point(12, 36)
  }
  },
  destination: {
  url: './images/destination.svg',
  options: {
  size: new H.math.Size(30, 36),
  anchor: new H.math.Point(12, 36)
  }
  }
  };
   
  if (icons[icon]) {
  markerOptions = {
  icon: new H.map.Icon(icons[icon].url, icons[icon].options)
  };
  }
   
  var marker = new H.map.Marker(coordinates, markerOptions);
  this.map.addObject(marker);
   
  return marker;
  };

To be fair, our function is starting to gain a bit of weight, right? Let's take this as an indicator for the need of refactoring and return to it in the next post.

Visual tweaks: opacity

In many cases, parts of the routes might overlap and become essentially indistinguishable from one another. Let's reduce the line width and change the color used when rendering the Polyline to include some transparency to make up for it. As the strokeColor attribute may be defined using CSS syntax, we're able to utilise the rgba function here.

  var routeLine = new H.map.Polyline(strip, {
  style: { strokeColor: 'rgba(0, 85, 170, 0.5)', lineWidth: 3 }
  });

Let's refresh our browser and take a look at our app. Already looking a lot better!

custom_markers.png

Route selection

Now that we have our three route options showing up, we should enable the user to select the best one and get some more information on it from the routing API.

Let's start by defining a callback in our HERERoute class which will be triggered every time a route is selected.

  var selectedRoute;
   
  var onRouteSelection = function(route) {
  console.log('A route has been selected.', route);
  selectedRoute = route;
  };

We're using the selectedRoute variable to keep track of which route is currently selected, if any.

The call to console.log represents a stand-in for functionality implemented in the future.

Now we will want to somehow indicate which route is selected on the map. This poses a problem, as we're currently keeping track of routes and their respective PolyLine representations separately. Let's store them in the same place for easier use. This requires an update to the onSucess callback in our HERERoute class:

  var onSuccess = function(result) {
  if (result.response.route) {
  var routeLineGroup = new H.map.Group();
   
  var routes = result.response.route.map(function(route) {
  var routeLine = drawRoute(route);
  routeLineGroup.addObject(routeLine);
   
  return {
  route: route,
  routeLine: routeLine
  };
  });
   
  map.addObject(routeLineGroup);
  map.setViewBounds(routeLineGroup.getBounds());
  }
  };

From here on out the routes variable will hold an array of objects, essentially pairing the data representation of the route as returned from the API with their visual representation on the map. Still following? Great! Let's move on then.

Indicating selection

With all necessary pieces in place we can now continue to specify a new PolyLine style to highlight the fact that a given route has been selected. As it's starting to look like we might be using these style definitions in multiple places from here on out, let's define them in a dictionary at the top of our class:

  var routeLineStyles = {
  normal: { strokeColor: 'rgba(0, 85, 170, 0.5)', lineWidth: 3 },
  selected: { strokeColor: 'rgba(255, 0, 0, 0.7)', lineWidth: 7 }
  };

This should allow us to highlight the route as a nice, fat red line on the map.

Let's update the onRouteSelection function to make use of this style. The map rendering engine allows us to modify the style of PolyLines using the setStyle method:

  var onRouteSelection = function(route) {
  console.log('A route has been selected.', route);
  if (selectedRoute) {
  selectedRoute.routeLine.setStyle(routeLineStyles.normal);
  }
   
  route.routeLine.setStyle(routeLineStyles.selected); selectedRoute = route;
  };

In order to test the new functionality you could temporarily add a call to onRouteSelection and pass the first route as argument at the bottom of our onSuccess callback:

  onRouteSelection(routes[0]);

Refresh your browser and you should see one of the three routes selected in bright red.

route_selected_zindex.png

Depending on which route is selected you might notice that it is actually rendered beneath one of the other routes which is not really looking all that great. Thankfully the map rendering engine also gives us a tool to deal with that problem. Using the PolyLines' setZIndex method we can ensure that our selected route is always rendered on top. Oh, and did I mention that you can chain all method calls for PolyLine? — Nice!

Armed with this new knowledge we can update the onRouteSelection function once more. Let's set the z-index for our selected route to something sufficiently high — like 10:

  var onRouteSelection = function(route) {
  console.log('A route has been selected.', route);
  if (selectedRoute) {
  selectedRoute.routeLine.setStyle(routeLineStyles.normal).setZIndex(1);
  }
   
  route.routeLine.setStyle(routeLineStyles.selected).setZIndex(10);
  selectedRoute = route;
  };

Let's take a look by refreshing the browser.

route_selected_zindex.png

Sweet — today we are really making some progress. But we're not quite done yet!

Route selection panel

So far we have no way to select either of the routes manually. Let's change that by introducing a small floating panel, listing all routes and their main properties such as duration and distance. By clicking on either of these elements we'll want to select the corresponding route and indicate our selection.

First, let's clean up our test code, though, and remove the call to onRouteSelection from our onSuccess callback.

It looks like we'll need a bit of code for our panel so let's not clutter our HERERoute class any further and create a new class called HERERoutesPanel in a file named routes_panel.js. By now you know the drill — "don't forget to include the file in your index.html before app.js".

Additionally we'll need a container in the DOM to render our panel into. As we expect a list of routes, let's add the following snippet of HTML to our index.html file:

  <div id="route-panel">
  <ul></ul>
  </div>

Time to define the HERERoutesPanel class. Let‘s start outside our new class, though by calling our imaginary constructor from the very bottom of the onSuccess callback in our HERERoute class. This will help us to figure out which parameters are needed:

  this.routePanel = new HERERoutesPanel(routes,
  { onRouteSelection: onRouteSelection }
  );

So far so good. In our new class we define a render function to create the list:

  function HERERoutesPanel(routes, options) {
   
  var render = function(routes) {
  var routeList = document.querySelector('#route-panel ul');
  routes.forEach(function(route, i) {
  routeList.appendChild(renderRouteElement(route, i));
  });
  };
   
  }

If you have been working on web front-end projects before you might be thinking — “Why not use React (or any other framework)?” and you would be right. For the code that we're about to write a framework and a templating library would be a big help and likely the right choice. However, it's a bit out of our scope to cover those technologies so we'll have to make due with what we got: good ‘ol vanilla JavaScript, the DOM APIs and a touch of CSS. Feel free to use your favorite framework instead!

As you can see, we're calling a function called renderRouteElement in our loop. Let's continue by defining that:

  var renderRouteElement = function(route, i) {
  var element = document.createElement('li');
   
  var routeSummary = route.route.summary;
  element.innerHTML = renderRouteTitle(routeSummary, i);
   
  element.addEventListener('click', function() {
  if (selectedRoute) {
  selectedRouteElement.classList.remove('selected');
  }
   
  element.classList.add('selected');
  selectedRoute = route;
  selectedRouteElement = element;
   
  if (options.onRouteSelection) {
  options.onRouteSelection(selectedRoute);
  }
  }, false);
   
  return element;
  };

What's going on here? — We're creating a new <li/> element for each route and calling a yet-to-be-defined function called renderRouteTitle to generate the text content for it.
Then we proceed by defining an event handler for click events on our new element. This handler will store the newly selected route and update the UI by adding a selected class for the corresponding element and removing it from any previously selected element.
In order for this all to work, we'll need a local variable to store the selection state — let's add it at the top of the class:

  var selectedRoute;
  var selectedRouteElement;

We're getting there! But we're still missing a bit of code — the renderRouteTitle function has not been implemented or discussed yet. As it turns out, the routing API returns a whole treasure trove of information including a summary object. However, as this is additional data we need to explicitly request it using the routeattributes key in the routeOptions. Let's switch over to our HEREMap class for a moment and account for this new requirement. Within the drawRoute method we add a new line to our routeOptions:

  var routeOptions = {
  mode: 'fastest;car',
  representation: 'display',
  alternatives: 2,
   
  // Our new line:
  routeattributes: 'waypoints,summary,shape,legs',
   
  waypoint0: Utils.locationToWaypointString(fromCoordinates),
  waypoint1: Utils.locationToWaypointString(toCoordinates)
  };

Great — let's continue working on the renderRouteTitle method back in the HERERoutePanel class:

  var renderRouteTitle = function(routeSummary, i) {
  return [
  '<strong>Route ' + (i + 1) + '</strong> (',
  Utils.formatDistance(routeSummary.distance) + ' in ',
  Utils.formatDuration(routeSummary.travelTime) + ')'
  ].join('');
  };

As you can see, the summary contains — among other things — both distance and travel time. All we have to do is a bit of formatting and we're good to go.

Distance is represented in meters and travelTime in seconds so we'll need some utilities to convert them into a more human-readable format. For this purpose we extend our Utils object in utils.js a bit:

  var Utils = {
   
  //
   
  formatDistance: function(distanceInMeters) {
  if (distanceInMeters < 1000) {
  return distanceInMeters + 'm';
  } else {
  return (distanceInMeters / 1000).toFixed(1) + 'km';
  }
  },
   
  formatDuration: function(durationInSeconds) {
  var hours = Math.floor(durationInSeconds / 3600);
  var minutes = Math.floor(durationInSeconds % 3600 / 60);
   
  if (hours > 0) {
  return hours + 'h ' + minutes + 'min';
  } else {
  return minutes + 'min';
  }
  }
  };

Take a deep breath — we're almost done. Let's add a call to our new render function at the bottom of the HERERoutesPanel constructor and pass the routes:

  render(routes);

Now is a good moment to remember that we have just constructed a whole bunch of HTML using good old string concatenation. While there's certainly more elegant ways to do this it will do the job just fine. However, it's still lacking some finishing touches in form of CSS styles. Appearances are a thing of preference and we'd never dare to tell you how your app should look like but for a shortcut you may borrow our styles and paste them into your app.css file.

Now — the moment of truth has arrived. Let's refresh the page and see what we got.

routes_panel.png

Awesome! Things are really coming together now. Try selecting a route as well.

Now that we got this far, don't you also feel like pushing our app just a tiny tad further before calling it a day?

Directions

Did you know that the route API also returns driving directions in plain English? It would be a shame to not make use of this information as it could really help a driver getting his sweet freight faster and safer from A to B.

Let's render a second list beneath our title information for each route which is only shown when the corresponding route is selected. This list will simply represent all maneuvers as returned by the API.

We start by updating our `renderRouteElement` function in the HERERoutesPanel class:

  var renderRouteElement = function(route, i) {
  var element = document.createElement('li');
   
  var routeSummary = route.route.summary;
  element.innerHTML = renderRouteTitle(routeSummary, i);
   
  var maneuvers = route.route.leg[0].maneuver;
  element.innerHTML += renderManeuvers(maneuvers);
   
  //
   
  }

As you can see, maneuvers are returned as part of the leg data for each route. A “leg” is the part of a route between two waypoints but since we're only routing from waypoint A to waypoint B for now we can expect there to be never more than one leg per route.

For the last step we need to implement the renderManeuvers function — easier than it sounds, thanks to the pre-formatted maneuver instruction string:

  var renderManeuvers = function(maneuvers) {
  return [
  '<ol class="directions">',
  maneuvers.map(function(maneuver) {
  return '<li>' + maneuver.instruction + '</li>';
  }).join(''),
  '</ol>'
  ].join('');
  };

Refresh the browser, select a route et voila! There are your directions for the selected route. Pretty sweet, don't you think?

directions.png

Wrapping up

Time finish up. Today we have done a lot of work! We have added support for multiple routes, enabled route selection and are now rendering route details and even turn-by-turn directions. Well done! Remember that you can get the complete code from this (and all other) tutorials on GitHub.

In the next post we'll enable our application to react to changes like changing waypoints. — Stay tuned!