RouteReuseStrategy for Route Caching in Angular

This post explains how to implement RouteReuseStrategy to support custom control of route snapshot caching in Angular. Typical use case is a list page which a user can search for a list of items and then select and navigates to a particular item’s detail pages. When the user clicks the back button in the browser, the Angular app should return to the list page displaying the same items as before, using the previous search criteria.

Other advantages of caching route for rendering is faster page load and reduce network traffic.

To achieve the above in Angular, we need to implement the RouteReuseStrategy to tell Angular not to destroy a component but to save it for re-rendering. There are few blog posts online already with example implementations. This blog will focus more on describing the mechanics of the interface and its methods

RouteReuseStretagy

Below is a skeleton implementation of a custom RouteReuseStrategy:

import { Injectable } from '@angular/core';
import { RouteReuseStrategy, ActivatedRouteSnapshot, DetachedRouteHandle } from '@angular/router';

@Injectable()
export class AppRouteReuseStrategyService implements RouteReuseStrategy {

     handles: {[key: string]: DetachedRouteHandle} = {};

     constructor() { }

     shouldDetach(route: ActivatedRouteSnapshot): boolean {
           // To Be Implemented
     }

     store(route: ActivatedRouteSnapshot, handle: DetachedRouteHandle): void {
           // To Be Implemented
     }

     shouldAttach(route: ActivatedRouteSnapshot): boolean {
           // To Be Implemented
     }

      retrieve(route: ActivatedRouteSnapshot): DetachedRouteHandle {
          // To Be Implemented
      }

      shouldReuseRoute(future: ActivatedRouteSnapshot, curr: ActivatedRouteSnapshot): boolean {
          // To Be Implemented
      }
}

shouldReuseRoute()

This is the first method to consider. If returns true, none of the other methods will be called. For example, when we are already reusing the current route snapshot. Note the future argument refers to the route that you come from previously. For example if the app navigates from item list page to item details page, curr would refer to the route for the item detail page and future to the route for the item list page.

shouldDetach() and store()

If the method shouldRouteReuse returns false, the method shouldDetach will be called to determine whether the current route snapshot should be detached and stored. If it returns true, the store method will be called. A handle to the detached route snapshot (of type DetachedRouteHandle) is provided as argument to the method so it can store it for later use.

Note if a null handle is provided to the method, it should erase the stored value for the input route. See the API documentation here.

Note once a route snapshot is detached, it is the developer’s responsibility to manage its lifecycle and perform any clean up as needed for proper memory management.

shouldAttach() and retrieve()

Similar to above,

If the method shouldReuseRoute returns false, the method shouldAttach will be called to determine if a cached route should be used. If it returns true, the method retrieve will be called to retrieve the saved handle to the detached route previously stored.

Note the shouldAttach method is also a good place to clean up any stored snapshots. For example when a user has logged out or the snapshot has got staled, in which case we should not be rendering the store snapshot. The method should then return false and the stored handle to route snapshot should be removed from storage.

That’s it. Hope above gives some clarity on what the class RouteReuseStrategy does.

Advertisements

Spring for Apache Kafka Quick Start

In this blog, I setup a basic Spring Boot project for developing Kafka based messaging system using Spring for Apache Kafka. The project also includes basic Spring config required for publishing and listening to messages from Kafka broker.

Project Setup

The following tools and versions are used here:

  1. Maven 3.x
  2. Spring Kafka 1.3.2 (current release version)
  3. Kafka client 0.11.0.2
  4. Spring Boot 1.5.9

The current Spring Boot release version (1.5.9) has Spring Kafka version  1.1.7 as the managed version. I have to override this to use 1.3.2. My Maven pom file fragment as below:

 <parent>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-parent</artifactId>
      <version>1.5.9.RELEASE</version>
      <relativePath /> <!-- lookup parent from repository -->
 </parent>

 <dependencies>
      <dependency>
           <groupId>org.springframework.kafka</groupId>
           <artifactId>spring-kafka</artifactId>
           <version>1.3.2.RELEASE</version>
      </dependency>
      
      <dependency>
           <groupId>org.springframework.kafka</groupId>
           <artifactId>spring-kafka-test</artifactId>
           <version>1.3.2.RELEASE</version>
           <scope>test</scope>
      </dependency>

      <dependency>
           <groupId>org.apache.kafka</groupId>
           <artifactId>kafka-clients</artifactId>
           <version>0.11.0.2</version>
      </dependency>

Producer Config

Spring Boot provides auto configuration for connecting to Kafka but I find it useful to setup the beans myself. Spring Kafka adopts the same approach to Kafka as in other message brokers such as ActiveMQ. For publishing message a template, KafkaTemplate, as to be configured as with JmsTemplate for ActiveMQ.

The following is my Java Config for a KafkaTemplate to publish message to the Kafka broker

@Configuration
public class KafkaProducerConfig {

     @Value("${spring.kafka.bootstrap-servers}") // (1)
     private String brokerAsString;
 
     @Bean
     public ProducerFactory<Integer, String> producerFactory() {
          return new DefaultKafkaProducerFactory<>(producerConfigs());
     }

     @Bean
     public Map<String, Object> producerConfigs() {
          Map<String, Object> props = new HashMap<>();
          props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, brokerAsString);
          props.put(ProducerConfig.RETRIES_CONFIG, 0);
          props.put(ProducerConfig.BATCH_SIZE_CONFIG, 16384);
          props.put(ProducerConfig.LINGER_MS_CONFIG, 1);
          props.put(ProducerConfig.BUFFER_MEMORY_CONFIG, 33554432);
          props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, IntegerSerializer.class);
          props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
          return props;
      }

     @Bean
     public KafkaTemplate<Integer, String> kafkaTemplate() {
          return new KafkaTemplate<Integer, String>(producerFactory());
     }
}

Note:

  1. The broker address is set using the property spring.kafka.bootstrap-servers defined in the application.properties (or yml) file. For example,
// application.properties
spring.kafka.bootstrap-servers=http://localhost:9092

Consumer Config

Consuming messages from Kafka using Spring Kafka is similar to consuming messages from Active MQ using Spring JMS support. We need to define container factory and message listener. Below is my Java Config for message listener factory.

@Configuration
public class KafkaConsumerConfig {
 
     @Value("${spring.kafka.bootstrap-servers}")
     private String brokerAsString;

     @Value("${spring.kafka.consumer.group-id}")
     private String groupId;
 
     @Value("${spring.kafka.consumer.auto-offset-reset}")
     private String autoOffsetReset;
 
     @Bean
     ConcurrentKafkaListenerContainerFactory<Integer, String> kafkaListenerContainerFactory() {
          ConcurrentKafkaListenerContainerFactory<Integer, String> factory = new ConcurrentKafkaListenerContainerFactory<>();
          factory.setConsumerFactory(consumerFactory());
          return factory;
     }

     @Bean
     public ConsumerFactory<Integer, String> consumerFactory() {
         return new DefaultKafkaConsumerFactory<>(consumerConfigs());
     }

     @Bean
     public Map<String, Object> consumerConfigs() {
         Map<String, Object> props = new HashMap<>();
         props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, brokerAsString);
         props.put(ConsumerConfig.GROUP_ID_CONFIG, groupId);
         props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, autoOffsetReset);
         props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, true);
         props.put(ConsumerConfig.AUTO_COMMIT_INTERVAL_MS_CONFIG, "100");
         props.put(ConsumerConfig.SESSION_TIMEOUT_MS_CONFIG, "15000");
         props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, IntegerDeserializer.class);
         props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
         return props;
     }
}

Now we can listen to a Kafka topic by using the annotation @KafkaListener. For example

@Service
public class GreetingsTopicListener {

 private Logger logger = LoggerFactory.getLogger(getClass());
 
 @KafkaListener(topics = "greetings")
 public void listen(ConsumerRecord<?,?> cr) throws Exception {
      logger.info(cr.toString());
 }
}

@KafkaListener will use the default listener container factory defined in class ConsumerConfig above to create the message listener. It is also possible to override this by settig the containerFactory attribute in the annotation. See javadoc for more details.

Creating Topics

It is also possible to automatically add topics to the broker by defining @Beans using the new 0.11.0.x client library class AdminClient as in the Spring Kafka reference documentation

@Configuration
public class KafkaTopicConfig {
 
 @Value("${spring.kafka.bootstrap-servers}")
 private String brokerAsString;
 
 @Bean
 public KafkaAdmin admin() {
   Map<String, Object> configs = new HashMap<>();
   configs.put(AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG, brokerAsString);
   return new KafkaAdmin(configs);
 }

 @Bean
 public NewTopic topic1() {
   return new NewTopic("foo", 10, (short) 2);
 }

 @Bean
 public NewTopic topic2() {
   return new NewTopic("bar", 10, (short) 2);
 }
}

That’s about it. The codes included in this blog should be sufficient for setting up a Spring Boot project for a messaging system using Spring Kafka.

Background geolocation in Ionic app

This blog show an example on how to implement background geolocation in an Ionic app using the background geolocation plugin.  to capture user locations for building location aware applications

Install plugin

First add the cordova plugin to the Ionic app:

cordova plugin add cordova-plugin-mauron85-background-geolocation

Implement background geolocation service

Next, implement an Angular service:

var app = angular.module('starter');
app.factory('BackgroundGeolocationService', ['$q', '$http', function ($q, $http) { 
      var callbackFn = function(location) {
          postLocation(location);
          backgroundGeoLocation.finish();  
      },
      failureFn = function(error) {
          console.log('BackgroundGeoLocation error ' + JSON.stringify(error));  
      },

      //Enable background geolocation  
      start = function () {      
          //save settings (background tracking is enabled) in local storage    
          window.localStorage.setItem('bgGPS', 1);
          backgroundGeoLocation.configure(callbackFn, failureFn, {
               desiredAccuracy: 10,      
               stationaryRadius: 10,
               distanceFilter: 10,
               locationProvider: 'ANDROID_ACTIVITY_PROVIDER',
               interval: 10000,      
               fastestInterval: 5000,      
               stopOnStillActivity: false,
               debug: false,      
               stopOnTerminate: false
         });
         backgroundGeoLocation.start();
      };
     // Send location to a backend server, e.g. for location tracking
     postLocation = function post(location) {
         return $http(      
           {        
              method: 'POST',
              headers: {
                   "Content-Type": "application/json"
                    },
              url: 'http://<location server DNS>/location',
              data: {          
                    lat : location.latitude,
                    lng : location.longitude        
                    }      
           }).then(function (response) {
                   return response.data;      
           });  
       }
  
       return {    
            start: start,
            // Stop data tracking    
            stop: function () {
                window.localStorage.setItem('bgGPS', 0);
                backgroundGeoLocation.stop();    
            }  
      }
}]);

The codes above are largely based on this blog article.  I have modified the plugin configuration (see notes below). Details about each configuration parameter can be found in the plugin documentation. I am setting it up and tested for Android here. A few notes or tips below:

  1. Two location providers are supported in Android – Android_distance_filter_provider and Android_activity_provider. I end up using the 2nd one to adjust the intervals in which the app gets location update by setting the parameters interval and fastinterval (to 10 and 5 seconds respectively).
  2. Debug is your friend. Set this to true will trigger a sound and notification when the app receives a location update.
  3. I use a combination of parameters desiredAccuracy, stationaryRadius and distanceFilter to define how often the app should receive location update events. The values set here seems to strike a good balance between accuracy/frequency and battery usage but its largely depends on the app’s use cases and phones.
  4. The plugin includes a url paramter where you can set to the backend server to post the locations. It is also possible, and more flexible, to just implement your own method and call it in the callback function callbackFn 

To kick start the plugin, call init() method in the app.js :

angular.module('app', ['ionic', ...])
.run(function($ionicPlatform, $http, BackgroundGeolocationService) {
   $ionicPlatform.ready(function() {    
       if(window.StatusBar) {
          StatusBar.styleDefault();
       }
       BackgroundGeolocationService.init();
   });
});

Now the Ionic app can send geolocations events when it is in the background, it is rather straight forward to implement a backend server to track and display users’ current locations. Below is a screenshot of a Java app I build, for example:

blog_track.png

 

Integrate Docker with Maven for Spring Boot projects

This blog will demonstrate how to setup in Maven using a number of plugins to integrate Docker in a Spring Boot Maven project. The objective here is to rebuild the docker image for the project seamlessly whenever Maven is run to build and release a new jar file.

The source codes for this project can be found in here

POM File

The project pom.xml file build life cycle is updated to include 3 plugins as shown below:

<build>
 <plugins>
    <plugin>
       <groupId>org.springframework.boot</groupId>
       <artifactId>spring-boot-maven-plugin</artifactId>
    </plugin>
    <plugin>
       <artifactId>maven-resources-plugin</artifactId>
       <executions>
          <execution>
             <id>copy-resources</id>
             <phase>process-resources</phase>
             <goals>
                 <goal>copy-resources</goal>
             </goals>
             <configuration>
               <outputDirectory>${basedir}/target</outputDirectory>
               <resources>
                  <resource>
                     <directory>src/main/resources/docker</directory>
                     <includes>
                        <include>Dockerfile</include>
                     </includes>
                  </resource>
               </resources>
             </configuration>
           </execution>
        </executions>
     </plugin>
     <plugin>
        <groupId>com.google.code.maven-replacer-plugin</groupId>
        <artifactId>replacer</artifactId>
        <version>1.5.3</version>
        <executions>
            <execution>
               <phase>prepare-package</phase>
               <goals>
                   <goal>replace</goal>
               </goals>
            </execution>
        </executions>
        <configuration>
             <file>${basedir}/target/Dockerfile</file>
             <replacements>
                 <replacement>
                     <token>IMAGE_VERSION</token>
                     <value>${project.version}</value>
                 </replacement>
             </replacements>
         </configuration>
     </plugin>
     <plugin>
         <groupId>com.spotify</groupId>
         <artifactId>dockerfile-maven-plugin</artifactId>
         <version>${version.dockerfile-maven}</version>
    <executions>
       <execution>
       <id>default</id>
       <goals>
          <goal>build</goal>
          <goal>push</goal>
       </goals>
       </execution>
     </executions>
     <configuration>
          <contextDirectory>${project.build.directory}</contextDirectory>
          <repository>image name here</repository>
          <tag>${project.version}</tag>
     </configuration>
    </plugin>
  </plugins>
</build>

The pom file assumes the Dockerfile can be found in the source folder /src/resource/docker. You can define your own Dockerfile as needed for your project. For demo purpose, I am using, with minor modification, the sample Dockerfile found in this blog

FROM frolvlad/alpine-oraclejdk8:slim
VOLUME /tmp
ADD docker-maven-IMAGE_VERSION.jar app.jar
RUN sh -c 'touch /app.jar'
ENV JAVA_OPTS=""
ENTRYPOINT [ "sh", "-c", "java $JAVA_OPTS -Djava.security.egd=file:/dev/./urandom -jar /app.jar" ]

Note the tag IMAGE_VERSION, this will be replaced with the version of the jar file being built

The first step in the build is to copy the Dockerfile above to the /target, i.e. build output, folder using the resource plugin. This is needed for 2 reasons: (1) we need to set the filename to add to the image to match that of the version being built and (2) Docker does not allow in ADD source file outside of the context directory of the Dockerfile so we have to put it in same directory as the jar file.

The second step is to set in the Dockerfile just copied the correct version of the jar file to be included in the docker image. The Maven Replacer plugin is used to replace the tag IMAGE_VERSION in the Dockerfile with the Maven variable project.version.

Finally, we run the Spotify Dockerfile Maven plugin to build/push the docker image. The plugin allows you to set what repository to use. Note we set the tag to be that of the Maven variable project.version as in step 2 to make sure that the image tag matches that of the jar file.

Running Maven

Now whenever the project is build or deploy in Maven using the standard mvn install or mvn deploy, the corresponding docker image will also be build or pushed to the repository. This also works for mvn release:prepare and mvn release:prepare for releasing tag version of the jar file.

Note you would need to setup certificate required to access the Docker daemon. For example, include the following environment variables:

DOCKER_HOST // to <host ip address>
DOCKER_TLS_VERIFY = true
DOCKER_CERT_PATH = C:\Users\<username>\.docker\machine\certs

Consult Docker documentation for more details on secure access to the Docker daemon.

Below is an excerpt of what you would see in a console when running mvn install

[INFO] ------------------------------------------------------------------------
[INFO] Building docker-maven 0.0.1-SNAPSHOT
[INFO] ------------------------------------------------------------------------
...
[INFO] --- maven-resources-plugin:2.6:copy-resources (copy-resources) @ docker-maven ---
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] Copying 1 resource
[INFO] 
[INFO] --- maven-compiler-plugin:3.1:compile (default-compile) @ docker-maven ---
...
[INFO] --- replacer:1.5.3:replace (default) @ docker-maven ---
[INFO] Replacement run on 1 file.
[INFO] 
[INFO] --- maven-jar-plugin:2.6:jar (default-jar) @ docker-maven ---
[INFO] Building jar: D:\src\blog_docker_maven\target\docker-maven-0.0.1-SNAPSHOT.jar
...
[INFO] Image will be built as <image name here>
[INFO] 
[INFO] Step 1/7 : FROM frolvlad/alpine-oraclejdk8:slim
[INFO] Pulling from frolvlad/alpine-oraclejdk8
...

Docker image for Oracle JDK

Oracle has deprecated their official Docker image for JDK a few years back. The only official image available is for open JDK (here). There are quite a few docker images available and here is another one I create.  It creates an image with JDK8(u131) under Oracle Linux and can be pulled in from Docker Hub using the following command

docker pull rhslee2000/docker_oraclejdk

Paging with Spring Data

This blog shows by an example on how to use paging support in Spring Data. Let start with following repository without paging

 

public interface ProductRepository extends JpaRepository<Product, Long> {

 @Query("SELECT p FROM product p WHERE p.category= :category")
 List<Product> findByCategory(@Param("category") String category);

}

 

The above query will return all products with matching category. To change it to only return products required to fill in a single page,  the method is updated to:

import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
...
public interface ProductRepository extends JpaRepository<Product, Long> {

 @Query("SELECT p FROM product p WHERE p.category= :category")
 Page<Product> findByCategory(@Param("category") String category, 
     Pageable pageable);

}

Note:

  1. The method now returns an object with class Page instead of List
  2. A new argument with type Pageable is added

The Pageable argument allows you to specify a few things including which page to return, the number of items in each page and properties used to sort the results. For example, to return the 2nd page where size of each page is 10, the method can be called with Pageable below

Pageable pageable = new PageRequest(1, 10);

Class PageRequest is a concrete implementation of Pageable provided by Spring Data. Note page number is 0-indexed.

To define how the results should be ordered, a Sort object can be supplied as the 3rd argument of the PageRequest constructor. For example, to return list of products sorted from highest price and then by name in alphabetical order:

Sort sort = new Sort(new Order(Direction.DESC, "price"), 
                     new Order(Direction.ASC, "name"));

Pageable pageable = new PageRequest(1, 10, sort);

Finally, note that the Page object returned from the method contains not just the list of products but also other values useful for implementing pagination, e.g. total number of results, number of pages. See the javadoc here for more detail.

 

 

Using Google Maps Directions APIs with more than 8 waypoints

Google Maps Directions APIs are great to create routes and directions for use in many practical business problems, e.g. delivery routing. One limitation is it only allows 8 (or 23 in premium plan) waypoints between the origin and destination locations. This blog demonstrates how to work around this limitation with the Maps JavaScript API. I am implementing the solution with TypeScript for use in an AngularJS 1.x application.

The idea here is straight forward: Given a route with more than 8 waypoints, i.e. 10 or more locations, we divide the route into multiple legs where each leg can have at most 8 waypoints. That is for a route with locations

[1,2,3,...,n] where n > 10

we would like to end up with multiple legs like:

leg 1: [1,2,3,...,10]
leg 2: [10,11,...,19]
...
leg 3: [...,n]

then we can use the directions API to get the route for each leg and concatenate the results back together as a single route on Google Maps.

First let define the objects to represent the route, waypoints and legs.

// Class to represent a route
export class Route {
 totalDistanceInKm : number;
 totalDurationInMins : number;
 drops : Drop[];
 legs : Leg[];
...
}

// Class to represent a drop (or waypoint)
export class Drop {
 location : google.maps.LatLng;
 // route details from directions service
 distanceInKm : number; // route distance from previous drop
 durationInMin : number; // route duration from previous drop
...
}

// Class to represent a leg of a route, used as input to directions service
export class Leg {
 origin : google.maps.LatLng;
 destination : google.maps.LatLng;
 waypoints : Array<google.maps.LatLng>;
}

A route can have multiple drops. The first and last drop corresponds to route origin and destination respectively. A drop consists of just a latlng location and a number of variables to store results, e.g. distance and duration, from the directions service response. The Leg class stores the information required as input requests to the directions service.

To create a route from a list of locations, use the following methods (assuming the class member _currentRoute contains the list of drops of the input route.

export class DeliveryRouteService implements IDeliveryRouteService {

...

 private createLegs() : void {
      var route = this._currentRoute;
      var drops = route.drops;
      route.legs = [];
      var i = 0; 
      var j = Math.min(drops.length, 10);

      while (j <= drops.length && j > i + 2) {
           route.legs.push(this.createLeg( drops.slice(i,j))); 
           i = j - 1;
           j = Math.min(drops.length, i + 10);
      }
 }

 private createLeg(drops : Drop[]) : Leg {
      var leg = new Leg();
      leg.origin = drops[0].location;
      leg.destination = drops[drops.length-1].location;
      leg.waypoints = [];
      if (drops.length > 2) {
           for (var k = 1; k < drops.length - 1; k++) {
                leg.waypoints.push(drops[k].location);
           }
      }
      return leg;
 }

With the route and legs created, you could call the directions API to obtain the route one leg at a time and concatenate the results for the whole route in your own codes. The following code snippets demonstrate how this could be done:

public directionsForLeg(idx : number, dropIdx : number) {
      var timeout : ng.ITimeoutService = this._timeoutService;
      var leg = this._currentRoute.legs[idx];
      var waypoints : google.maps.DirectionsWaypoint[] = [];
      
      for (var i = 0; i < leg.waypoints.length; i++) {
           waypoints.push(<google.maps.DirectionsWaypoint> {
           location : leg.waypoints[i],
           stopover : true
          })
      }
      // (1) Create directions API request
      var request = <google.maps.DirectionsRequest> {
           origin : leg.origin,
           destination : leg.destination,
           waypoints : waypoints,
           optimizeWaypoints : false,
           travelMode : google.maps.TravelMode.DRIVING
      }
 
      // (2) invoke directions service using wrapper service
      this._googleMapService.directions(request).then(function(result) {
           var resultLegs = result.routes[0].legs;
           for (var j = 0; j < resultLegs.length; j++) {
             // do something with results here, e.g. get leg distance/duration
             ...
             dropIdx++;
           }

           if (idx < that._currentRoute.legs.length - 1) {
                // (3) do the next leg if there are more, 
                // put some delay to avoid hitting request limit
                timeout(function() {
                 that.directionsForLeg(idx+1, dropIdx);
                }, 1000, true);
           } else {
                // all done, notify route/directions is ready
                ...
           }
      }, function(e) {
           that._logger.error('directions service failed: ' + e);
      });
 }

Note the recursion used to call directions service for the next leg in (3). To get route and directions for the entire route,  just call

 directionsForLeg(0, 1); // start with 1, first drop is origin

Below is the wrapper service for Google Maps JavaScript API

export class GoogleMapsService {
 static id : string = 'GoogleMapsService';
 private _map : google.maps.Map;
 private _renderers : google.maps.DirectionsRenderer[];
 private _directionsService : google.maps.DirectionsService;

...

      directions(request : google.maps.DirectionsRequest) : ng.IPromise<any> {
           var deferred = this.$q.defer();
           // create renderer
           var opts = <google.maps.DirectionsRendererOptions> {
               map : this._map,
               suppressMarkers : true
           };
           var renderer = new google.maps.DirectionsRenderer(opts);
           this._renderers.push(renderer);

           this._directionsService.route(request, function(response, status) {
               if (status == google.maps.DirectionsStatus.OK) {
                    renderer.setDirections(response);
                    deferred.resolve(response);
               } else {
                    deferred.reject(status.toString());
               }
           })
           return deferred.promise;
      }

}

Each call to the directions service will create and render the route for the input leg on the map.

The work around here has one obvious limitation – you can’t optimize the waypoints for the entire route. In practice, this may not be a severe issue if the waypoints are reasonably well ordered. It is still possible to apply optimization to each leg to improve the overall route distance and duration.