Hands on

Transportation Routing and Directions in an Angular Application with the HERE Routing API

By Nic Raboy | 17 September 2018

When I think of maps and location services, the first thing that comes to mind is GPS routing and navigation. Having a map is great for finding places and other points of interest, but navigating between points of interest is awesome. However, how might you tackle such a task when it comes to finding the fast route between two points? Do you have to create your own graph of points and traverse through them manually? This isn’t an issue when taking advantage of the HERE Routing API.

We’re going to see how to create an Angular application that displays a map and allows users to pick two points and find the fastest traveling path between them while showing address directions along the way. Things you’d expect in your car navigational unit or on your mobile device.

The goal of this project is to create something like the following image:

angular-routing-example

As you can see in the above image, we have two input fields for a starting and destination position. The map on the screen shows a line which represents the fastest path of travel when using a car. Next to the map we have navigation instructions for traversing the path demonstrated on the map. If we wanted to, we could collect the current position of our device and display it as a marker somewhere on the path to demonstrate progress.

Starting with the Angular CLI

Assuming that you have the Angular CLI installed and configured, we are going to create a new Angular project. If you haven’t already, I strongly encourage you to take a look at my previous tutorial titled, Display HERE Maps within your Angular Web Application as it goes into further detail around creating an actual map. Most of our focus in this tutorial will be around displaying routing information on the map.

From the Angular CLI, execute the following:

ng new here-project

The above command will create a new project for us. Before we get into any actual development, we need to include the HERE JavaScript libraries in our project. Open the project’s src/index.html file and include the following:

<!doctype html>
<html lang="en">
    <head>
        <meta charset="utf-8">
        <title>HERE with Angular</title>
        <base href="/">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <link rel="icon" type="image/x-icon" href="favicon.ico">
    </head>
    <body>
        <app-root></app-root>
        <script src="https://js.api.here.com/v3/3.0/mapsjs-core.js" type="text/javascript" charset="utf-8"></script>
        <script src="https://js.api.here.com/v3/3.0/mapsjs-service.js" type="text/javascript" charset="utf-8"></script>
    </body>
</html>

The above two JavaScript libraries act as a baseline for displaying a map and using the HERE services. Other APIs will have other requirements when it comes to the JavaScript components.

Now that we have the dependencies in place, we can focus on the development of our map.

Creating a Map Component with Directions Box

Like I mentioned previously, when it comes to building our map, a lot of the code will come from my previous tutorial on the topic. However, there will be some modifications as we progress.

The first step towards displaying a map is to create a dedicated component within Angular. From the CLI, execute the following:

ng g component here-map

The above command will create a component with an HTML view and a TypeScript file for all the logic. Before we get into the UI portion, let’s create our baseline logic. Open the project’s src/app/here-map/here-map.component.ts file and include the following:

import { Component, OnInit, OnChanges, SimpleChanges, ViewChild, ElementRef, Input } from '@angular/core';

declare var H: any;

@Component({
    selector: 'here-map',
    templateUrl: './here-map.component.html',
    styleUrls: ['./here-map.component.css']
})
export class HereMapComponent implements OnInit, OnChanges {

    @ViewChild("map")
    public mapElement: ElementRef;

    @Input()
    public appId: any;

    @Input()
    public appCode: any;

    @Input()
    public start: any;

    @Input()
    public finish: any;

    @Input()
    public width: any;

    @Input()
    public height: any;

    public directions: any;

    private platform: any;
    private map: any;
    private router: any;

    public constructor() { }

    public ngOnInit() {
        this.platform = new H.service.Platform({
            "app_id": this.appId,
            "app_code": this.appCode
        });
        this.directions = [];
    }

    public ngAfterViewInit() {
        let defaultLayers = this.platform.createDefaultLayers();
        this.map = new H.Map(
            this.mapElement.nativeElement,
            defaultLayers.normal.map,
            {
                zoom: 4,
                center: { lat: "37.0902", lng: "-95.7129" }
            }
        );
    }

    public ngOnChanges(changes: SimpleChanges) { }

}

The above code is lengthy, but not a whole lot is happening and it is definitely not complete. The first thing to take note of is how we’re declaring the following line:

declare var H: any;

Because we don’t have any TypeScript definitions, we need to declare the JavaScript library so that the TypeScript compiler will ignore it and not throw any errors instead.

Inside the HereMapComponent class, we have a bunch of variables with an @Input(). These variables represent potential attributes for the HTML component, kind of like the traditional class or id attributes that you’d find on components. Most of our focus will be on the start and finish variables as they will hold our coordinates.

Inside the ngOnInit lifecycle hook, we initialize our platform with the app id and app code for our project. To obtain an app id and app code, you’ll need a developer account with HERE. An account can be created for free from the HERE Developer Portal. The directions variable that is initialized in the ngOnInit will eventual have the turn by turn directions for our route.

Because the map is a UI component, we cannot begin working with it until the ngAfterViewInit method has triggered. Once triggered, we can create a new map and center it on some coordinates. You’ll notice that we have an ngOnChanges method. We’ll be using the ngOnChanges hook for when component attribute values change in realtime. This will be useful for when the user enters new coordinate information and we need the map to reflect the change.

With the base TypeScript in place, let’s take a look at the HTML component for our map. Open the project’s src/app/here-map/here-map.component.html and include the following:

<div #map [style.width]="width" [style.height]="height" style="float: left"></div>
<ol style="float: left; background-color: #FFF; width: 35%; min-height: 530px; margin-left: 20px; margin-top: 0">
    <li *ngFor="let direction of directions">
        <p [innerHTML]="direction.instruction"></p>
    </li>
</ol>

Ignoring my terrible CSS skills, we have two parts to this component. We have the map itself as referenced by the #map template variable and we have an ordered list which loops through our directions variable. We are using the [innerHTML] attribute because the instructions that the HERE Routing API returns is HTML data.

With the core map logic in place, we can focus on routing information and navigation instructions.

Using the HERE Routing API for Navigation

Remember how I mentioned the ngOnChanges hook? This is going to be a critical function towards our routing data and updating the map. However, before we get there, we need to create another function. Open the project’s src/app/here-map/here-map.component.ts file and include the following route function:

public route(start: any, finish: any) {
    let params = {
        "mode": "fastest;car",
        "waypoint0": "geo!" + this.start,
        "waypoint1": "geo!" + this.finish,
        "representation": "display"
    }
    this.map.removeObjects(this.map.getObjects());
    this.router.calculateRoute(params, data => {
        if(data.response) {
            this.directions = data.response.route[0].leg[0].maneuver;
            data = data.response.route[0];
            let lineString = new H.geo.LineString();
            data.shape.forEach(point => {
                let parts = point.split(",");
                lineString.pushLatLngAlt(parts[0], parts[1]);
            });
            let routeLine = new H.map.Polyline(lineString, {
                style: { strokeColor: "blue", lineWidth: 5 }
            });
            let startMarker = new H.map.Marker({
                lat: this.start.split(",")[0],
                lng: this.start.split(",")[1]
            });
            let finishMarker = new H.map.Marker({
                lat: this.finish.split(",")[0],
                lng: this.finish.split(",")[1]
            });
            this.map.addObjects([routeLine, startMarker, finishMarker]);
            this.map.setViewBounds(routeLine.getBounds());
        }
    }, error => {
        console.error(error);
    });
}

Most, not all, of the above code was taken from the official documentation. When calling the route method, we are expecting a source coordinates and destination coordinates to be supplied. These coordinates should be a string and delimited by comma.

When we have the coordinate information, we can create our routing parameters. For this particular example we’re going to navigate by car and only have a starting and destination endpoint.

After we define our parameters we need to clear the map. Every time we change the data, we don’t want old markers or navigation paths to linger on the map. Once the map is clear, we can calculate the route. Assuming that an actual route exists, we can get the maneuver information for navigation instructions and start constructing our path on the map. A scenario where a route doesn’t exist might be if I want to drive from California to Berlin, it just isn’t going to happen.

To create a line with the HERE API, we need to push each of the latitude and longitude coordinates into a LineString variable. With those coordinates we can create a Polyline and give it a color as well as a width. While not necessary, we could create markers to represent our starting and ending points on the map. With all of our objects (line, markers, etc.) we can add them to the map and re-center the map to best fit the routing path.

Before we get into the ngOnChange stuff, let’s wire up the route function so it works. First we need to revise the ngOnInit method to the following:

public ngOnInit() {
    this.platform = new H.service.Platform({
        "app_id": this.appId,
        "app_code": this.appCode
    });
    this.directions = [];
    this.router = this.platform.getRoutingService();
}

In the above method we are just initializing the router. Then in the ngAfterViewInit method we need to call the route method:

public ngAfterViewInit() {
    let defaultLayers = this.platform.createDefaultLayers();
    this.map = new H.Map(
        this.mapElement.nativeElement,
        defaultLayers.normal.map,
        {
            zoom: 4,
            center: { lat: "37.0902", lng: "-95.7129" }
        }
    );
    this.route(this.start, this.finish);
}

The start and finish variables that are passed will be whatever data is bound to the component attributes. Finally we can take a look at the ngOnChange method:

public ngOnChanges(changes: SimpleChanges) {
    if((changes["start"] && !changes["start"].isFirstChange()) || (changes["finish"] && !changes["finish"].isFirstChange())) {
        this.route(this.start, this.finish);
    }
}

Remember, the ngOnChange hook is called any time one of the component attribute values has changed. So if the start or finish coordinates change, this method will execute. When the method is called, we first need to figure out if one of our start or finish attributes triggered it. If yes, we can call our route function again to clear and update the map.

Developing a Functional Angular Application for Routes and Navigation

If I haven’t lost you yet, there are a few more things we need to take care of. While we have a functional map and instructions component, it isn’t being used as of now in our application. We need to make use of it.

Because we are going to be working with forms, we need to enable such functionality in Angular. Open the project’s src/app/app.module.ts file and include the following:

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule } from "@angular/forms";

import { AppComponent } from './app.component';
import { HereMapComponent } from './here-map/here-map.component';

@NgModule({
    declarations: [
        AppComponent,
        HereMapComponent
    ],
    imports: [
        BrowserModule,
        FormsModule
    ],
    providers: [],
    bootstrap: [AppComponent]
})
export class AppModule { }

In the above code you’ll notice that we’ve imported the FormsModule and added it to the imports section of the @NgModule block. It is simple and a little annoying that we have to do it at all, but it is necessary for Angular.

The next step is to add some brief logic for initializing our form variables. Open the project’s src/app/app.component.ts file and include the following:

import { Component, OnInit } from '@angular/core';

@Component({
    selector: 'app-root',
    templateUrl: './app.component.html',
    styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {

    public start: string;
    public finish: string;

    public constructor() {
        this.start = "37.7397,-121.4252";
        this.finish = "37.6819,-121.7680";
    }

    public ngOnInit() { }

}

While of no direct relevance to the variable naming in the map component, the start and finish variables will be bound to our form. Open the project’s src/app/app.component.html file and include the following HTML markup:

<div style="padding: 10px 0">
    <div>
        <label style="display: inline-block; width: 60px; color: #FFF">Start</label>
        <input type="text" [(ngModel)]="start" />
    </div>
    <div>
        <label style="display: inline-block; width: 60px; color: #FFF">Finish</label>
        <input type="text" [(ngModel)]="finish" />
    </div>
</div>
<here-map appId="APP-ID-HERE" appCode="APP-CODE-HERE" [start]="start" [finish]="finish" width="60%" height="530px"></here-map>

There are a few things to note in the above markup, ignoring my lack of CSS skills. Each of the two inputs are bound to our variables and those same variables are bound to the map component via the attributes. As the data in the forms change, so will the data in the map because of the ngOnChange method. In this example, I’ve chosen not to include my app id and app code values. Make sure to update those with your own.

Conclusion

You just saw how to use the HERE Routing API to find the quickest path of travel between two positions on a map using Angular. In this example we took a map, rendered a line as well as markers on the path, and then printed out the turn by turn directions on how to traverse that path.

As previously mentioned, if you were using a device that had GPS, you could add a marker on the map to represent your current position in relation to the starting and ending points.