In this recipe we’ll add functionality to display and add a route to a Google Map.
For this recipe, we’re only going to use one route on our Google Map. If our route consists of visiting multiple contacts then we’ll add them as ‘waypoints’ along the route. In this way, the route will have a number of legs (for each waypoint/contact) – each leg having its own set of details (e.g. distance/duration). See this Using Google Maps using Waypoints in directions for more details.
Note. The Google Map javaScript library is only loaded into the page (using our GoogleMapsService below) if the user is online (or comes online from being offline). We dynamically add this javascript library to cater for devices going offline and then coming back online.
This codepen demonstrates the code below.
See the Pen RWPavm by Todd Halfpenny (@toddhalfpenny) on CodePen.
The Code
Add the service code
Add the following code to the services.js file.
Notes:
- Our GoogleMapsService is only ever using one route, and adding ‘waypoints’ (or legs) along that route for each contact. The route should be regenerated (i.e. callout to Google) each time a contact is added/removed from it. A code example of adding a route can be found in the controllers section below.
- If you’ve built your app from a MobileCaddy shell or seed app then you may already have the NetworkService – if so, just update your version with the Google related parts from the one below.
- MobileCaddy uses a local storage item (‘networkStatus’) to determine whether the app is online/offline – and therefore whether the maps can be used or not. This item is set by event listeners setup in app.js (these are included as part of the MobileCaddy shell/seed apps and can be found on the Github repos). The NetworkService and ConnectivityService then make use this local storage item.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 |
/* =========================================================================== G O O G L E M A P S =========================================================================== */ .factory('GoogleMapsService', ['$rootScope', '$ionicLoading', '$timeout', '$window', '$document', 'ConnectivityService', function($rootScope, $ionicLoading, $timeout, $window, $document, ConnectivityService){ var apiKey = false, map = null, mapDiv = null, directionsService, directionsDisplay, routeResponse; function initService(mapEl, key) { mapDiv = mapEl; if (typeof key !== "undefined") { apiKey = key; } if (typeof google == "undefined" || typeof google.maps == "undefined") { disableMap(); if (ConnectivityService.isOnline()) { $timeout(function() { loadGoogleMaps(); },0); } } else { if (ConnectivityService.isOnline()) { initMap(); enableMap(); } else { disableMap(); } } } function initMap() { if (mapDiv) { var mapOptions = { zoom: 10, mapTypeId: google.maps.MapTypeId.ROADMAP }; map = new google.maps.Map(mapDiv, mapOptions); directionsService = new google.maps.DirectionsService(); directionsDisplay = new google.maps.DirectionsRenderer(); directionsDisplay.setMap(map); // Wait until the map is loaded google.maps.event.addListenerOnce(map, 'idle', function(){ enableMap(); }); } } function enableMap() { // For demonstration purposes we’ll use a $rootScope variable to enable/disable the map. // However, an alternative option would be to broadcast an event and handle it in the controller. $rootScope.enableMap = true; } function disableMap() { $rootScope.enableMap = false; } function loadGoogleMaps() { // This function will be called once the SDK has been loaded $window.mapInit = function() { initMap(); }; // Create a script element to insert into the page var script = $document[0].createElement("script"); script.type = "text/javascript"; script.id = "googleMaps"; // Note the callback function in the URL is the one we created above if (apiKey) { script.src = 'https://maps.google.com/maps/api/js?key=' + apiKey + '&sensor=true&callback=mapInit'; } else { script.src = 'https://maps.google.com/maps/api/js?sensor=true&callback=mapInit'; } $document[0].body.appendChild(script); } function checkLoaded() { if (typeof google == "undefined" || typeof google.maps == "undefined") { $timeout(function() { loadGoogleMaps(); },2000); } else { enableMap(); } } function addRoute(origin, destination, waypts, optimizeWaypts) { routeResponse = null; if (typeof google !== "undefined") { var routeRequest = { origin : origin, destination : destination, waypoints: waypts, optimizeWaypoints: optimizeWaypts, travelMode : google.maps.TravelMode.DRIVING }; directionsService.route(routeRequest, function(response, status) { if (status == google.maps.DirectionsStatus.OK) { directionsDisplay.setDirections(response); google.maps.event.trigger(map, 'resize'); // Save the response so we access it from controller routeResponse = response; // Broadcast event so controller can process the route response $rootScope.$broadcast('googleRouteCallbackComplete'); } }); } } function removeRoute() { if (typeof google !== "undefined" && typeof directionsDisplay !== "undefined") { directionsDisplay.setMap(null); directionsDisplay = null; directionsDisplay = new google.maps.DirectionsRenderer(); directionsDisplay.setMap(map); } } return { initService: function(mapEl, key){ initService(mapEl, key); }, checkLoaded: function(){ checkLoaded(); }, disableMap: function(){ disableMap(); }, removeRoute: function(){ removeRoute(); }, getRouteResponse: function(){ return routeResponse; }, addRoute: function(origin, destination, waypts, optimizeWaypts){ addRoute(origin, destination, waypts, optimizeWaypts); } }; }]) /* =========================================================================== C O N N E C T I V I T Y =========================================================================== */ .factory('ConnectivityService', [function(){ return { isOnline: function(){ var status = localStorage.getItem('networkStatus'); if (status === null || status == "online") { return true; } else { return false; } } }; }]) /* =========================================================================== N E T W O R K =========================================================================== */ .factory('NetworkService', ['GoogleMapsService', function(GoogleMapsService){ /* * handles network events (online/offline) */ return { networkEvent: function(status){ var pastStatus = localStorage.getItem('networkStatus'); if (status == "online" && pastStatus != status) { // The app has regained connectivity... GoogleMapsService.checkLoaded(); } if (status == "offline" && pastStatus != status) { // The app has lost connectivity... GoogleMapsService.disableMap(); } localStorage.setItem('networkStatus', status); return true; } }; }]) |
Add the HTML and CSS to display map
The following HTML gives an example of how a map might be included in a template. The CSS applied to the elements is displayed below it (and should go in your ‘scss/app.scss’ file).
HTML:
1 2 3 4 5 |
<div class="map-container" ng-show="$root.enableMap"> <div id="map" data-tap-disabled="true"></div> </div> |
CSS:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
#map { width: 100%; height: 100%; background-color: #fff !important; } .scroll { height: 100%; } .map-container { background-color: #fff; padding-left: 20px; padding-right: 20px; padding-bottom: 10px; width: 100%; height: 100%; } |
Add the controller code
The following code snippets give examples of how, and when, the controller might call our GoogleMapsService defined in step 1 above. This code should go in your controllers.js file. (Remember to inject the GoogleMapsService into the controller.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
// When you first go into your view (with a map) you should call the following method. // (an example of the map HTML is in the above step) GoogleMapsService.initService(document.getElementById("map")); // The following code can be used to add a route to the map. This particular style of calling the // Google service will use ‘waypoints’ for each contact along the route. // Points of interest: // 1. The origin and destination parameters are zip/post codes // 2. $scope.journeyLegs is an array of contacts on the route (each contact has a zip/post code) // 3. Once the callout to Google (to get the route) has completed, an event will be broadcast from // the GoogleMapsService – this event (‘googleRouteCallbackComplete’) can be handled by the // controller code and the route information interrogated in more detail e.g. to get the distance // between the contacts legs. // Set up some example test data for a leg to be stopped over on the route $scope.journeyLegs = []; var journeyLeg = {“zipPostalCode”: “66045”, “contactId”: “1”}; $scope.journeyLegs.push(journeyLeg); // Call the method to add the route $scope.addRoute(“94087”, “27215”); // Call this method to add the route $scope.addRoute = function(origin, destination) { if (origin !== "" && destination !== "") { // Callout to Google to get route between first and last contact. // The 'legs' between these contact will give us the distance/time var waypts = []; for (i = 0, len = $scope.journeyLegs.length; i < len; i++) { waypts.push({ location: $scope.journeyLegs[i].zipPostalCode, stopover: true }); } GoogleMapsService.addRoute(origin, destination, waypts, true); } }; |
The following code can added to a controller in order to interrogate a route returned from the Google route callout. In the example below, we’re handling the event fired from our GoogleMapsService (when the callback has completed) and using the route data to update distances.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
// Handle callback from Google Maps after route has been generated var deregisterUpdateDistance = $rootScope.$on('googleRouteCallbackComplete', function(event, args) { $scope.updateDistance(); }); // Deregister the handler when scope is destroyed $scope.$on('$destroy', function() { deregisterUpdateDistance(); }); $scope.updateDistance = function() { $timeout(function() { // Get the route saved after callback from Google directionsService (to get route) var routeResponse = GoogleMapsService.getRouteResponse(); if (routeResponse) { // We’ve only defined one route with waypoints (or legs) along it var route = routeResponse.routes[0]; // The following is an example of getting the distance and duration for the last leg of the route var distance = route.legs[route.legs.length-1].distance; var duration = route.legs[route.legs.length-1].duration; // Add some code to use the above distance/duration data: // distance.value and duration.value give you numeric values // distance.text and duration.text give you a text version. E.g. 120 mi } }); }; |
1 2 3 4 5 |
// Use the following call if you need to remove the route from the map (it won’t destroy the map) GoogleMapsService.removeRoute(); |