In [Part 1][1] of this tutorial series we looked at how to set up a basic Google Maps integration within an Ionic application. In [Part 2][2], we took that a little further and looked at how we might load in map markers dynamically using the $http service.

In this tutorial we’re going to complete the transition from basic Google Maps implementation to an advanced and production ready implementation. We’re going to tackle the two major remaining issues with integrating Google Maps into an Ionic application which are:

Dealing with a user who has no Internet connection or loses their Internet connection Only loading markers that need to be loaded, not loading every single marker that exists at once

To do this we’re going to make some modifications to what we have already done, so if you want to follow along make sure you have completed [Part 1][1] and [Part 2][2] already.

Dealing with Online and Offline States

We’re using the Google Maps JavaScript SDK which needs to be loaded from Googles servers. So what happens when a user tries to access our application when they are not connected to the Internet?

If we haven’t added any mechanism to deal with that scenario, then the app will break. The app will try to access the google object at some point which will not exist because the SDK could not be loaded and it will throw an error.

Alternatively, what if the user did have an Internet connection initially but loses it later? An Internet connection is needed to pull in new data to the map so again, it won’t work.

To deal with this, we’re going to implement functionality within our application that will:

Only load the Google Maps SDK if the user is online

If the user is not online, wait until they are online and then load the Google Maps SDK

If the user was online but goes offline, disable the map with a friendly error message

If the user was offline but comes back online, reenable the map and remove the error message.

and it’s going to look something like this:

[ ][3]

There are ways we can detect if the user is online both by using a Cordova plugin, and also by just using simple JavaScript. As well as detecting if they are online, we can also detect when they go online or offline. I’ve talked about how to do this in depth [in this tutorial][4] but in short the code looks like this:

$rootScope . $on ( '$cordovaNetwork:online' , function ( event , networkState ) { doSomething ( ) ; } ) ; $rootScope . $on ( '$cordovaNetwork:offline' , function ( event , networkState ) { doSomething ( ) ; } ) ;

when using the [$cordovaNetwork plugin][5], or like this:

window . addEventListener ( "online" , function ( e ) { doSomething ( ) ; } , false ) ; window . addEventListener ( "offline" , function ( e ) { doSomething ( ) ; } , false ) ;

when just using plain old JavaScript. So we’re going to use this concept to dynamically load the Google Maps SDK based on when an Internet connection is available, and disable and reenable the map based on the users current connection.

REMOVE the following line from index.html

< script src = " http://maps.google.com/maps/api/js?&sensor=true " > </ script >

Since we want to load the Google Maps SDK based on when a connection is available, we have to remove it from index.html which will try to load it right away.

NOTE: This code is based on Rohde Fischer’s [solution to this problem in Sencha Touch][6].

First we are going to create another factory to give us a consistent way to tell if the user is online or offline (since which method we use depends on whether it is running on a device or not). Before we do that though, let’s install the $cordovaNetwork plugin.

Run the following command to install the $cordovaNetwork plugin

cordova plugin add cordova-plugin-network-information

Create a ConnectivityMonitor factory that looks like this:

. factory ( 'ConnectivityMonitor' , function ( $rootScope , $cordovaNetwork ) { return { isOnline : function ( ) { if ( ionic . Platform . isWebView ( ) ) { return $cordovaNetwork . isOnline ( ) ; } else { return navigator . onLine ; } } , ifOffline : function ( ) { if ( ionic . Platform . isWebView ( ) ) { return ! $cordovaNetwork . isOnline ( ) ; } else { return ! navigator . onLine ; } } } } )

Now we can simply call the isOnline or isOffline functions from this factory and it will return the network status no matter what platform we are running on.

Modify your GoogleMaps factory to reflect the following

. factory ( 'GoogleMaps' , function ( $cordovaGeolocation , $ionicLoading , $rootScope , $cordovaNetwork , Markers , ConnectivityMonitor ) { var apiKey = false ; var map = null ; function initMap ( ) { var options = { timeout : 10000 , enableHighAccuracy : true } ; $cordovaGeolocation . getCurrentPosition ( options ) . then ( function ( position ) { var latLng = new google . maps . LatLng ( position . coords . latitude , position . coords . longitude ) ; var mapOptions = { center : latLng , zoom : 15 , mapTypeId : google . maps . MapTypeId . ROADMAP } ; map = new google . maps . Map ( document . getElementById ( "map" ) , mapOptions ) ; google . maps . event . addListenerOnce ( map , 'idle' , function ( ) { loadMarkers ( ) ; enableMap ( ) ; } ) ; } , function ( error ) { console . log ( "Could not get location" ) ; } ) ; } function enableMap ( ) { $ionicLoading . hide ( ) ; } function disableMap ( ) { $ionicLoading . show ( { template : 'You must be connected to the Internet to view this map.' } ) ; } function loadGoogleMaps ( ) { $ionicLoading . show ( { template : 'Loading Google Maps' } ) ; window . mapInit = function ( ) { initMap ( ) ; } ; var script = document . createElement ( "script" ) ; script . type = "text/javascript" ; script . id = "googleMaps" ; if ( apiKey ) { script . src = 'http://maps.google.com/maps/api/js?key=' + apiKey + '&sensor=true&callback=mapInit' ; } else { script . src = 'http://maps.google.com/maps/api/js?sensor=true&callback=mapInit' ; } document . body . appendChild ( script ) ; } function checkLoaded ( ) { if ( typeof google == "undefined" || typeof google . maps == "undefined" ) { loadGoogleMaps ( ) ; } else { enableMap ( ) ; } } function loadMarkers ( ) { Markers . getMarkers ( ) . then ( function ( markers ) { console . log ( "Markers: " , markers ) ; var records = markers . data . result ; for ( var i = 0 ; i < records . length ; i ++ ) { var record = records [ i ] ; var markerPos = new google . maps . LatLng ( record . lat , record . lng ) ; var marker = new google . maps . Marker ( { map : map , animation : google . maps . Animation . DROP , position : markerPos } ) ; var infoWindowContent = "<h4>" + record . name + "</h4>" ; addInfoWindow ( marker , infoWindowContent , record ) ; } } ) ; } function addInfoWindow ( marker , message , record ) { var infoWindow = new google . maps . InfoWindow ( { content : message } ) ; google . maps . event . addListener ( marker , 'click' , function ( ) { infoWindow . open ( map , marker ) ; } ) ; } function addConnectivityListeners ( ) { if ( ionic . Platform . isWebView ( ) ) { $rootScope . $on ( '$cordovaNetwork:online' , function ( event , networkState ) { checkLoaded ( ) ; } ) ; $rootScope . $on ( '$cordovaNetwork:offline' , function ( event , networkState ) { disableMap ( ) ; } ) ; } else { window . addEventListener ( "online" , function ( e ) { checkLoaded ( ) ; } , false ) ; window . addEventListener ( "offline" , function ( e ) { disableMap ( ) ; } , false ) ; } } return { init : function ( key ) { if ( typeof key != "undefined" ) { apiKey = key ; } if ( typeof google == "undefined" || typeof google . maps == "undefined" ) { console . warn ( "Google Maps SDK needs to be loaded" ) ; disableMap ( ) ; if ( ConnectivityMonitor . isOnline ( ) ) { loadGoogleMaps ( ) ; } } else { if ( ConnectivityMonitor . isOnline ( ) ) { initMap ( ) ; enableMap ( ) ; } else { disableMap ( ) ; } } addConnectivityListeners ( ) ; } } } ) ;

There’s a bunch of code that has been added to the factory so I’ve added some comments in the parts that might not be obvious. But essentially we have just created some functions to only load the SDK when the user is online, disable the map if they go offline, and reenable it when they come back online.

Also note that we have injected a few extra services into the Factory:

$ionicLoading allows us to display a nice loading mask

$rootScope and $cordovaNetwork allow us to monitor the network status

ConnectivityMonitor is the factory we just created

We’ve also added the ability to supply an API Key by initlaising the factory like this:

GoogleMaps.init("API KEY GOES HERE");

if an API key is not supplied it will initialise without one. If you try loading your application without an Internet connection now you should see something like this:

[ ][7]

and if you come back online, even without refreshing the app, the map should start working as normal.

Only Loading On-Screen Markers

If your map only has a few markers, even 50 or a 100 markers then this isn’t really a problem. You could simply load all the markers at once and be done with it.

But loading each marker and adding it to the map does mean a small performance hit. If you’re loading 1000 markers then this performance hit is no longer insignificant. What if you allow users of your application to submit markers and you end up with a total of 500,000 markers? You certainly don’t want to load all of those in at once.

To deal with this, we are going to figure out the area of the map that the user is currently looking at and only load in markers that are contained within that area. The process will look something like this:

Send the current bounds of the map to the server that contains the markers Only return markers that fall within those bounds Add those markers to the map

This process will be triggered again whenever the user drags the map, which changes the bounds of the map. The end result is that markers will drop in just as the user is looking at that portion of the map.

NOTE: This code is based on the [Ext.ux.MapLoader plugin by SwarmOnline][8] for Sencha Touch.

Modify your GoogleMaps factory to reflect the following:

. factory ( 'GoogleMaps' , function ( $cordovaGeolocation , $ionicLoading , $rootScope , $cordovaNetwork , Markers , ConnectivityMonitor ) { var markerCache = [ ] ; var apiKey = false ; var map = null ; function initMap ( ) { var options = { timeout : 10000 , enableHighAccuracy : true } ; $cordovaGeolocation . getCurrentPosition ( options ) . then ( function ( position ) { var latLng = new google . maps . LatLng ( position . coords . latitude , position . coords . longitude ) ; var mapOptions = { center : latLng , zoom : 15 , mapTypeId : google . maps . MapTypeId . ROADMAP } ; map = new google . maps . Map ( document . getElementById ( "map" ) , mapOptions ) ; google . maps . event . addListenerOnce ( map , 'idle' , function ( ) { loadMarkers ( ) ; google . maps . event . addListener ( map , 'dragend' , function ( ) { console . log ( "moved!" ) ; loadMarkers ( ) ; } ) ; google . maps . event . addListener ( map , 'zoom_changed' , function ( ) { console . log ( "zoomed!" ) ; loadMarkers ( ) ; } ) ; enableMap ( ) ; } ) ; } , function ( error ) { console . log ( "Could not get location" ) ; } ) ; } function enableMap ( ) { $ionicLoading . hide ( ) ; } function disableMap ( ) { $ionicLoading . show ( { template : 'You must be connected to the Internet to view this map.' } ) ; } function loadGoogleMaps ( ) { $ionicLoading . show ( { template : 'Loading Google Maps' } ) ; window . mapInit = function ( ) { initMap ( ) ; } ; var script = document . createElement ( "script" ) ; script . type = "text/javascript" ; script . id = "googleMaps" ; if ( apiKey ) { script . src = 'http://maps.google.com/maps/api/js?key=' + apiKey + '&sensor=true&callback=mapInit' ; } else { script . src = 'http://maps.google.com/maps/api/js?sensor=true&callback=mapInit' ; } document . body . appendChild ( script ) ; } function checkLoaded ( ) { if ( typeof google == "undefined" || typeof google . maps == "undefined" ) { loadGoogleMaps ( ) ; } else { enableMap ( ) ; } } function loadMarkers ( ) { var center = map . getCenter ( ) ; var bounds = map . getBounds ( ) ; var zoom = map . getZoom ( ) ; var centerNorm = { lat : center . lat ( ) , lng : center . lng ( ) } ; var boundsNorm = { northeast : { lat : bounds . getNorthEast ( ) . lat ( ) , lng : bounds . getNorthEast ( ) . lng ( ) } , southwest : { lat : bounds . getSouthWest ( ) . lat ( ) , lng : bounds . getSouthWest ( ) . lng ( ) } } ; var boundingRadius = getBoundingRadius ( centerNorm , boundsNorm ) ; var params = { "centre" : centerNorm , "bounds" : boundsNorm , "zoom" : zoom , "boundingRadius" : boundingRadius } ; var markers = Markers . getMarkers ( params ) . then ( function ( markers ) { console . log ( "Markers: " , markers ) ; var records = markers . data . result ; for ( var i = 0 ; i < records . length ; i ++ ) { var record = records [ i ] ; if ( ! markerExists ( record . lat , record . lng ) ) { var markerPos = new google . maps . LatLng ( record . lng , record . lat ) ; var marker = new google . maps . Marker ( { map : map , animation : google . maps . Animation . DROP , position : markerPos } ) ; var markerData = { lat : record . lat , lng : record . lng , marker : marker } ; markerCache . push ( markerData ) ; var infoWindowContent = "<h4>" + record . name + "</h4>" ; addInfoWindow ( marker , infoWindowContent , record ) ; } } } ) ; } function markerExists ( lat , lng ) { var exists = false ; var cache = markerCache ; for ( var i = 0 ; i < cache . length ; i ++ ) { if ( cache [ i ] . lat === lat && cache [ i ] . lng === lng ) { exists = true ; } } return exists ; } function getBoundingRadius ( center , bounds ) { return getDistanceBetweenPoints ( center , bounds . northeast , 'miles' ) ; } function getDistanceBetweenPoints ( pos1 , pos2 , units ) { var earthRadius = { miles : 3958.8 , km : 6371 } ; var R = earthRadius [ units || 'miles' ] ; var lat1 = pos1 . lat ; var lon1 = pos1 . lng ; var lat2 = pos2 . lat ; var lon2 = pos2 . lng ; var dLat = toRad ( ( lat2 - lat1 ) ) ; var dLon = toRad ( ( lon2 - lon1 ) ) ; var a = Math . sin ( dLat / 2 ) * Math . sin ( dLat / 2 ) + Math . cos ( toRad ( lat1 ) ) * Math . cos ( toRad ( lat2 ) ) * Math . sin ( dLon / 2 ) * Math . sin ( dLon / 2 ) ; var c = 2 * Math . atan2 ( Math . sqrt ( a ) , Math . sqrt ( 1 - a ) ) ; var d = R * c ; return d ; } function toRad ( x ) { return x * Math . PI / 180 ; } function addInfoWindow ( marker , message , record ) { var infoWindow = new google . maps . InfoWindow ( { content : message } ) ; google . maps . event . addListener ( marker , 'click' , function ( ) { infoWindow . open ( map , marker ) ; } ) ; } function addConnectivityListeners ( ) { if ( ionic . Platform . isWebView ( ) ) { $rootScope . $on ( '$cordovaNetwork:online' , function ( event , networkState ) { checkLoaded ( ) ; } ) ; $rootScope . $on ( '$cordovaNetwork:offline' , function ( event , networkState ) { disableMap ( ) ; } ) ; } else { window . addEventListener ( "online" , function ( e ) { checkLoaded ( ) ; } , false ) ; window . addEventListener ( "offline" , function ( e ) { disableMap ( ) ; } , false ) ; } } return { init : function ( key ) { if ( typeof key != "undefined" ) { apiKey = key ; } if ( typeof google == "undefined" || typeof google . maps == "undefined" ) { console . warn ( "Google Maps SDK needs to be loaded" ) ; disableMap ( ) ; if ( ConnectivityMonitor . isOnline ( ) ) { loadGoogleMaps ( ) ; } } else { if ( ConnectivityMonitor . isOnline ( ) ) { initMap ( ) ; enableMap ( ) ; } else { disableMap ( ) ; } } addConnectivityListeners ( ) ; } } } ) ;

Again, there’s a lot going on here but the key parts are:

We supply the bounds of the map as parameters when loadining markers (which we will make use of shortly)

We are keeping track of markers we have already added with markerCache to make sure we don’t add the same ones twice

to make sure we don’t add the same ones twice We listen for when the map changes its bounds (when the user drags or zooms) and then load more markers by supplying the new bounds information

We’ve added some functions like getDistanceBetweenPoints which are just using standard formulas for calculating distances between points (it’s the [Haversine Formula][9] for those interested). We do this so we can calculate the distance for which we want to load markers: if the map was zoomed in the distance from the center to the corner might be 1km, but zoomed out it could be 100km

which are just using standard formulas for calculating distances between points (it’s the [Haversine Formula][9] for those interested). We do this so we can calculate the distance for which we want to load markers: if the map was zoomed in the distance from the center to the corner might be 1km, but zoomed out it could be 100km We are recreating our own boundsNorm , centerNorm etc. objects because the object supplied by Google has nonsensical names.

Since we are now supplying a params object to the Markers factory, we need to alter the Markers factory a little bit

Modify your Markers factory to reflect the following:

. factory ( 'Markers' , function ( $http ) { var markers = [ ] ; return { getMarkers : function ( params ) { return $http . get ( "http://example.com/markers.php" , { params : params } ) . then ( function ( response ) { markers = response ; return markers ; } ) ; } } } )

Finally, we need to modify the server side code to take the area of the map we are looking at into account. Again, my example is in PHP but you can implement this with whatever you would like.

Modify your markers.php file or similar to reflect the following