Hands on

Who wants ice cream!? — A HERE Maps API for JavaScript Tutorial (Part 6: Reacting to outside influences)

By Richard Süselbeck | 15 November 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 did a fair bit of refactoring, if you haven't read it yet, you might want to take a look at it first before going on.

In order to make our application actually useful, we will have to provide our trusty delivery drivers an easy and convenient way to locate an ice-cream shop along their way to the customer. Once the driver has selected a route, we will use the Places API to search for ice-cream parlours in the vicinity of the route and display them on the map. We will further enable the driver then to select a shop and add it as waypoint to the selected route. Sound good? — Let's dive in and get those drivers on the road!

Places API

Onwards and upwards! At the core of our new feature you‘ll find yet another awesome data API called "Places";. The Places API allows us to request Points of Interest (POI) using a search query string and formatted location information. Here's an example from the developer documentation:

  var search = new H.places.Search(platform.getPlacesService());
   
  function onResult(data) {
  // Do something with data …
  }
   
  function onError(data) {
  // Handle an error
  }
   
  // Search for hotels in Chinatown, San Francisco
  var params = {
  'q': 'hotel',
  'at': '37.7942,-122.4070'
  };
   
  search.request(params, {}, onResult, onError);
view raw gelary-part6-1.js hosted with ❤ by GitHub

Looks easy enough, right? Indeed it is! — Let's start by wrapping this functionality in our own class. First we create as usual a new file called places.js in our scripts folder and include a reference in index.html somewhere before app.js:

  <script src="scripts/places.js"></script>

Then we move on to defining our class. Here's our constructor:

  function HEREPlaces (map, platform, onClickPlace) {
  this.map = map;
  this.onClickPlace = onClickPlace;
  this.placeSearch = new H.places.Search(platform.getPlacesService());
   
  this.searchResults = [];
  }
view raw gelary-part6-3.js hosted with ❤ by GitHub

So far so simple. First we assign a bunch of instance variables including a callback called onClickPlace, grab an instance of H.places.Search and create an array which will eventually hold our search results.

Let's continue by defining our instance methods:

searchPlaces:

  HEREPlaces.prototype.searchPlaces = function(query) {
  this.getPlaces(query, function(places) {
  this.updatePlaces(places);
  }.bind(this));
  };
view raw gelary-part6-4.js hosted with ❤ by GitHub

The first obvious candidate is the method used to search for places by providing a query object — most likely the most used instance method on this class. This method in turn refers to a second method called getPlaces which again handles the actual request and iterates over the result. Let's take a look under the hood:

getPlaces:

  HEREPlaces.prototype.getPlaces = function(query, onSuccessCallback) {
  var onSuccess,
  onError;
   
  onSuccess = function(data) {
  if (data.results && data.results.items) {
  var places = data.results.items.map(function(place) {
  place.coordinates = { lat: place.position[0], lng: place.position[1] };
   
  return place;
  });
   
  onSuccessCallback(data.results.items);
  } else {
  onError(data);
  }
  };
   
  onError = function(error) {
  console.error('Error happened when fetching places!', error);
  };
   
  this.placeSearch.request(query, {}, onSuccess, onError);
  };
view raw gelary-part6-5.js hosted with ❤ by GitHub

The structure of this method looks a bit familiar, doesn't it? Essentially we're defining simply the two required callbacks for our request and submit it to the API. In the case of a successful query we iterate over the response set and formate the coordinates of each POI for ease of use before passing the result on to the function defined as onSuccessCallback parameter. Nice!

clearSearch:

In addition to our search method we add a way to clear the results and remove markers added to the map.

  HEREPlaces.prototype.clearSearch = function() {
  this.searchResults.forEach(function(marker) {
  this.map.removeObject(marker);
  }.bind(this));
   
  this.searchResults = [];
  };
view raw gelary-part6-6.js hosted with ❤ by GitHub

updatePlaces:

  HEREPlaces.prototype.updatePlaces = function(places) {
  var markerOptions = {
  icon: new H.map.Icon(Utils.icons.iceCream.url, Utils.icons.iceCream.options)
  };
   
  this.clearSearch();
   
  this.searchResults = places.map(function(place) {
  var marker = new H.map.Marker(place.coordinates, markerOptions);
  this.map.addObject(marker);
   
  marker.addEventListener('tap', this.onClickPlace);
   
  return marker;
  }.bind(this));
  };
view raw gelary-part6-7.js hosted with ❤ by GitHub

Finally we need a way to actually create the markers representing the result of a successful query and render them on the map. To ensure a clean slate, let's also call clearSearch before doing so.
As we are planning on enabling the user to select a shop via it's marker in order to add it to the route by clicking it, this is also the place where we bind the onClickPlace callback to the tap event handler for each marker.

That's it! Our HEREPlaces class is all done — you can check it out in it's entirety on GitHub.

So far so good but we are not yet triggering any searches anyhow. Let's change that!

Map

In the constructor for our HEREMap class we're also instantiatiating our shiny new class HEREPlaces class:

this.places = new HEREPlaces(this.map, this.platform, this.onChangeViaPoint.bind(this));

As you can see we are referencing a new method called onChangeViaPoint as callback. Let's define that next:

  HEREMap.prototype.onChangeViaPoint = function(reroutePoint) {
  var viaPoint = reroutePoint.target.getPosition();
   
  this.drawRoute(this.position, HEREHQcoordinates, viaPoint);
  };
view raw gelary-part6-9.js hosted with ❤ by GitHub

Here we grab the position of the provided reroutePoint and pass it onto our draw method as third parameter. So far, so mysterious. Let's take a look at the updated this.drawRoute method to understand what's going on here:

  HEREMap.prototype.drawRoute = function(fromCoordinates, toCoordinates, reroutePoint) {
  var startPoint = Utils.locationToWaypointString(fromCoordinates);
  var endPoint = Utils.locationToWaypointString(toCoordinates);
  var viaPoint = reroutePoint && Utils.locationToWaypointString(reroutePoint);
   
  var routeOptions = {
  mode: 'fastest;car',
  representation: 'display',
  routeattributes: 'waypoints,summary,shape,legs',
  waypoint0: startPoint,
  };
   
  if (viaPoint) {
  this.updateMarker('viaPoint', reroutePoint);
   
  routeOptions.waypoint1 = viaPoint;
  routeOptions.waypoint2 = endPoint;
  } else {
  routeOptions.alternatives = 2;
  routeOptions.waypoint1 = endPoint;
  }
   
  this.updateMarker('origin', fromCoordinates);
  this.updateMarker('destination', toCoordinates);
   
  this.router.drawRoute(routeOptions);
  this.places.clearSearch();
  };

We have extended the signature of our method to accept an additional parameter — reroutePoint. That's going to be our waypoint for the selected ice-cream parlour.  Obviously this point won't always be available so we have to check for it's existence and update the route options conditionally. The Route API won't accept the alternatives option if more than two waypoints are defined.

Once the route is drawn we clear the places search to reset it.

If you take a close look at our constructor you'll notice that we have updated also the signature of our HERERouter initialiser to accept a third parameter:

  this.router = new HERERouter(this.map, this.platform, this.onChangeSelectedRoute.bind(this));

Looks like we'll still have to define another method before heading over there, though — onChangeSelectedRoute:

  HEREMap.prototype.onChangeSelectedRoute = function(route) {
  var middlePointOfTheRoute = Utils.locationToString(route.routeLine.getBounds().getCenter());
   
  this.searchForIcecreamShop(middlePointOfTheRoute);
  };

Aha! This new method is called when the route selection changes and is passed the newly selected route as parameter. Using the getBounds() and getCenter() methods we extract the center point for the route and use it to search for ice-cream parlours close by. searchForIcecreamShop sounds pretty promising but looks like we're still one method short:

  HEREMap.prototype.searchForIcecreamShop = function(coordinates) {
  var query = {
  'q': 'ice cream',
  'at': coordinates
  };
   
  this.places.searchPlaces(query);
  }

At last! This is the place where we're passing our query onto our very own HEREPlaces class — looking for some sweet delight!

Router

Remember that we had changed the signature of our HERERouter constructor? Seems like we're not quite done yet. Let's now head over there and update that class as well to suit our new requirements.

As we already know, the constructor now accepts a third parameter called onRouteChange. The passed function is assigned to an instance variable:

  this.onRouteChange = onRouteChange;

The place to trigger this callback is clearly the onRouteSelection method:

  HERERouter.prototype.onRouteSelection = function(route) {
  if (this.selectedRoute) {
  this.selectedRoute.routeLine.setStyle(this.routeLineStyles.normal).setZIndex(1);
  }
  route.routeLine.setStyle(this.routeLineStyles.selected).setZIndex(10);
  this.selectedRoute = route;
   
  this.map.setViewBounds(route.routeLine.getBounds());
  this.onRouteChange(route);
  };

Once a route has been selected we'll trigger the callback. Oh, and while we're at it we'll also adjust the view bounds of the map to suit the selected route.

The time has come to try what we have created. Refresh your browser and select a route. If everything works as it should you should see a whole bunch of ice-cream cones along the selected route. Click one and watch your route adjust to include the selected location. Awesome!

reroute.png

You'll notice that we're now using ice-cream cone icons for both your location and the ice-cream parlours. That's very confusing. Let's start the next post with a few visual tweaks, shall we?

Wrapping up

Wow, it's been a wild ride but the hard work has paid off. Our application is actually starting to be useful.

In the next post we'll bring Traffic data into our app for an even better routing solution. Stay tuned!