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.
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!
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:
We'll assume for the time being that it makes sense to split our code accordingly into files and objects or classes.
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:
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:
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:
With the drawRoute method in place we can now call it from within our updateMyPosition method:
Once our user's location is available we'll render the route.
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:
Of course we also still have to update the function calls to reflect the change within maps.js:
And with this change in place our app.js file looks once again nice and tidy:
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.
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:
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:
This change allows us to significantly simplify the onSuccess handler:
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:
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:
So far so good — let's refresh our browser. We should now see three routes instead of one.
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:
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:
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.
Let's refresh our browser and take a look at our app. Already looking a lot better!
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.
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:
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.
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:
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:
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:
Refresh your browser and you should see one of the three routes selected in bright red.
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:
Let's take a look by refreshing the browser.
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:
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:
So far so good. In our new class we define a render function to create the list:
As you can see, we're calling a function called renderRouteElement in our loop. Let's continue by defining that:
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:
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:
Great — let's continue working on the renderRouteTitle method back in the HERERoutePanel class:
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:
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:
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.
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?
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:
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:
Refresh the browser, select a route et voila! There are your directions for the selected route. Pretty sweet, don't you think?
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!