angularJS and google maps

I’ve been playing more and more with angularjs nowadays. Previously, I experimented with angularjs directives by  created a directive for CkEditor 4, inline mode.

This time, I am trying to create a directive for google maps. The use-case for this project, is for example creating a page where you give your viewers a tool to give them navigational directions to your business establishment.proj

I posted all the relevant codes to Github, so feel free to grab the experimental codes from there.

Part I Without Using Directive

I started by just making this experiment work – without using directives, no asynchronous loading, just putting all the code in the controller.

First, is creating the html page where we are are supposed to display our map.  The plan is to display the directions on the left side of the page (3 columns of Bootstrap 3 units with a DIV id of directions), and the map on the remaining 9 columns with a div ID of map_canvas.

index1

Controller

For the controller, I have variables (fixed) that stored the business address, location, and blurbs of the destination. Upon startup, it will try to use the HTML5 geolocation, to detect the user’s current address. That will be the initial “start” address. The controller will then call Google’s direction service to plot the route(s) towards the destination.

'use strict';
gApp.controller('CtrlGMap',
    function CtrlGMap($scope) {
        // defaults for your business location and blurb
        var streetAddress = "5111 47 St NE  Calgary, AB T3J 3R2",
            Location = new google.maps.LatLng(51.098945, -113.970889),
            businessWriteup = "<b>Calgary Police Service</b><br/>Calgary's Finest<br/>",
            defaultFromAddress = 'Calgary',
            businessTitle = "Calgary Police Service",
            directionsService = new google.maps.DirectionsService(),
            directionsDisplay = new google.maps.DirectionsRenderer({
                draggable: true
            }),

            mapOptions = {
                center: Location,
                zoom: 11,
                mapTypeId: google.maps.MapTypeId.ROADMAP
            },
            map = new google.maps.Map(document.getElementById("map_canvas"),
            mapOptions);

        // add your fixed business marker
        var contentString = businessWriteup + streetAddress,
          marker = new google.maps.Marker({
              position: Location,
              map: map,
              title: businessTitle,
              animation: google.maps.Animation.DROP
          });
        // show info Window
        var infowindow = new google.maps.InfoWindow({
            content: contentString
        });
        google.maps.event.addListener(marker, 'click', function () {
            infowindow.open(map, marker);
        });

        directionsDisplay.setMap(map);
        directionsDisplay.setPanel(document.getElementById('directions'));


        $scope.fromAddress = defaultFromAddress;
        $scope.selectedOption = 'Driving';
        $scope.options = ['Driving', 'Walking', 'Bicycling', 'Transit'];
        $scope.totalKm = 0;

        $scope.setDirections = function () {
            var selectedMode = $scope.selectedOption.toUpperCase() || 'DRIVING',
                from = $scope.fromAddress || defaultFromAddress,
                request = {
                    origin: from,
                    destination: streetAddress,
                    travelMode: selectedMode,
                    provideRouteAlternatives: true,
                    unitSystem: google.maps.UnitSystem.METRIC,
                    optimizeWaypoints: true
                };
            if (selectedMode === 'TRANSIT') {
                request.transitOptions = {
                    departureTime: new Date()
                };
            }

            directionsService.route(request, function (response, status) {
                if (status === google.maps.DirectionsStatus.OK) {
                    directionsDisplay.setDirections(response);
                } else {
                    toastr.error(status);
                }
            });
        }

        // Try HTML5 geolocation
        if ("geolocation" in navigator) {
            navigator.geolocation.getCurrentPosition(function (position) {
                var pos = new google.maps.LatLng(position.coords.latitude,
                                                 position.coords.longitude);
                //map.setCenter(Location);
                $scope.$apply(function () {
                    $scope.fromAddress = pos;
                });
                $scope.setDirections();
            });
        }







        google.maps.event.addListener(directionsDisplay, 'directions_changed', function () {

            computeTotalDistance(directionsDisplay.directions);
            try {
                if (directionsDisplay.directions.routes[0].legs[0]) {

                    $scope.$apply(function () {
                        $scope.fromAddress = directionsDisplay.directions.routes[0].legs[0].start_address;
                    });
                }
            } catch (e) { }
        });

        // fire it up initially
        $scope.setDirections();
        // watch if the mode has changed
        $scope.$watch('selectedOption', function (newValue, oldValue) { $scope.setDirections(); });

        function computeTotalDistance(result) {
            var total = 0, i,
                myroute = result.routes[0];
            for (i = 0; i < myroute.legs.length; i++) {
                total += myroute.legs[i].distance.value;
            }
            total = total / 1000;
            $scope.$apply(function () {
                $scope.totalKm = total;
            });
        }

    }
);

From angularjs coding perspective, the most important lines here to take note are those with the $scope.$apply(function()….. lines.  This is key to coding processes in angularjs wherein the variable (scope) updating may happen in another “thread”. For example, a callback from an ajax call, settimeout, etc…

The rest of the codes will be almost the same, whether you are using angularjs or not, for the purpose of displaying a map.

Part II Using Directives, and Asynchronous Loading of Google Maps

So, I got part I working. It is now time to try using a directive to “encapsulate” and possibly try to make this project re-usable. We now have to change how the html (index2.html) page looks like.

image

At first glance, the code now looks cleaner – and what is that “gmap” attribute doing there?  That looks like an invalid HTML5 attribute! Exactly! but quoting from angularjs docs – the goal of directives is to teach HTML new tricks!

First things first, let’s re-organize our codes

image

The relevant codes and mark-up for this round are as pointed out (in red) above.

Controller

'use strict';
gApp.controller('CtrlGMap2',
    function CtrlGMap2($scope) {
        $scope.gmap = {
            fromAddress: 'Calgary',
            streetAddress: "5111 47 St NE  Calgary, AB T3J 3R2",
            businessWriteup: "<b>Calgary Police Service</b><br/>Calgary's Finest<br/>",
            businessTitle: "Calgary Police Service",
            Lon: -113.970889,
            Lat: 51.098945,
            showError: function (status) {
                toastr.error(status);
            }
        };
    });

The controller now looks very minimal. Basically just a definition of an object that holds initial variables, that are to be passed to our directive.

The directive

"use strict";
angular.module('ep', [])
.directive('gmap', function ($window,$parse) {
    var counter = 0,
    prefix = '__ep_gmap_';
    
    return {
        restrict: 'A',
        replace: false,
        templateUrl: 'App/templates/gmap.html',
        link: function (scope, element, attrs, controller) {
            var getter = $parse(attrs.gmap),
            setter = getter.assign;

            var model = scope.gmap;
            model.options = ['Driving', 'Walking', 'Bicycling', 'Transit'];
            model.selectedOption = 'Driving';
            model.totalKm = 0;
            
            setter(scope, model);

            if ($window.google && $window.google.maps) {
                gInit();
            } else {
                injectGoogle();
            }
           

            function gInit() {
                var Location = new google.maps.LatLng(model.Lat, model.Lon),
                    directionsService = new google.maps.DirectionsService(),
                    directionsDisplay = new google.maps.DirectionsRenderer({
                        draggable: true
                    }),

                    mapOptions = {
                        center: Location,
                        zoom: 11,
                        mapTypeId: google.maps.MapTypeId.ROADMAP
                    },
                    map = new google.maps.Map(document.getElementById("map_canvas"),
                    mapOptions);
                // add your fixed business marker
                var contentString = model.businessWriteup + model.streetAddress,
                  marker = new google.maps.Marker({
                      position: Location,
                      map: map,
                      title: model.businessTitle,
                      animation: google.maps.Animation.DROP
                  });
                // show info Window
                var infowindow = new google.maps.InfoWindow({
                    content: contentString
                });
                google.maps.event.addListener(marker, 'click', function () {
                    infowindow.open(map, marker);
                });

                directionsDisplay.setMap(map);
                directionsDisplay.setPanel(document.getElementById('directions'));
                model.setDirections = function () {
                    var selectedMode = model.selectedOption.toUpperCase() || 'DRIVING',
                        from = model.fromAddress,
                        request = {
                            origin: from,
                            destination: model.streetAddress,
                            travelMode: selectedMode,
                            provideRouteAlternatives: true,
                            unitSystem: google.maps.UnitSystem.METRIC,
                            optimizeWaypoints: true
                        };
                    if (selectedMode === 'TRANSIT') {
                        request.transitOptions = {
                            departureTime: new Date()
                        };
                    }

                    directionsService.route(request, function (response, status) {
                        if (status === google.maps.DirectionsStatus.OK) {
                            directionsDisplay.setDirections(response);
                        } else {
                            if (angular.isFunction(model.showError)) {
                                scope.$apply(function () {
                                    model.showError(status);
                                });
                            }
                        }
                    });
                }

                // Try HTML5 geolocation
                if ("geolocation" in navigator) {
                    navigator.geolocation.getCurrentPosition(function (position) {
                        var pos = new google.maps.LatLng(position.coords.latitude,
                                                         position.coords.longitude);
                        //map.setCenter(Location);
                        scope.$apply(function () {
                            model.fromAddress = pos;
                        });
                        model.setDirections();
                    });
                }

                google.maps.event.addListener(directionsDisplay, 'directions_changed', function () {

                    computeTotalDistance(directionsDisplay.directions);
                    try {
                        if (directionsDisplay.directions.routes[0].legs[0]) {

                            scope.$apply(function () {
                                model.fromAddress = directionsDisplay.directions.routes[0].legs[0].start_address;
                            });
                        }
                    } catch (e) { }
                });

                // fire it up initially
                model.setDirections();
                // watch if the mode has changed
                scope.$watch('gmap.selectedOption', function (newValue, oldValue) { model.setDirections(); });

                function computeTotalDistance(result) {
                    var total = 0, i,
                        myroute = result.routes[0];
                    for (i = 0; i < myroute.legs.length; i++) {
                        total += myroute.legs[i].distance.value;
                    }
                    total = total / 1000;
                    scope.$apply(function () {
                        model.totalKm = total;
                    });
                }

            }
            function injectGoogle() {
                var cbId = prefix + ++counter;

                $window[cbId] = gInit;

                var wf = document.createElement('script');
                wf.src = ('https:' == document.location.protocol ? 'https' : 'http') +
                '://maps.googleapis.com/maps/api/js?v=3.exp&sensor=false&' + 'callback=' + cbId;
                wf.type = 'text/javascript';
                wf.async = 'true';
                var s = document.getElementsByTagName('script')[0];
                s.parentNode.insertBefore(wf, s);
            };
        }
    }


});

A couple of things to point out:

  • The google.maps object is loaded asynchronously – injectGoogle()
  • The “template” markup is in App/templates/gmap.html , and is passed in as a property for the directive called templateUrl
  • scope.$apply(function () { …. is heavily used here as well….

Disclaimer: The opinions expressed herein are my own personal opinions and do not represent my employer’s view in any way. The business entities/organizations used here as an example does not constitute endorsement on my part.

Tagged with: , ,
Posted in angularJS, google map
  • lenin m

    Thanks for great blog. very helpful. but download from github does not have Scripts folder for scripts. thanks!

  • ericpanorel

    @leninm:disqus I didn’t them because they are standard javascript libraries which can be downloaded separately

    • lenin m

      thank you!

  • Anekant

    hey friend can i get tutorial of google map integration like if i entered location, it shows that perticular location in google map. By using angular JS please leave me msg/mail