Sencha Touch 2 – The maps connectivity issue


WARNING

This is a legacy post. The best solution today is most likely to base a maps component on OpenStreetMap

Even though this post is quite old by now, it still seems to be of use to people. Awesome, and thanks to Josh Morony for the credit. It seems he had some minor issues that he fixed, so if this post doesn’t do the trick for you, please go visit his blog post about Dynamically Loading Google Maps Based on Internet Connection Availability

Using the Map component in Sencha Touch 2 is really convenient. Right until you test it on a phone, that’s offline. Creating a nice looking and user-friendly way of handling this isn’t as easy as I would have hoped for it to be.

The first problem you run into is that if you add Google Maps using the

Setting the scene

First off I’ll need to create the basic layout, so we have something for our magic:

Ext.define('Project.view.DetailView', {
    extend: 'Ext.Panel',
    xtype: 'detailView',
    requires: [ 'Project.model.ModelData', 'Ext.Img' ],
    
    config: {
        store: 'ModelDataStore',
        modelData: null,
        layout: 'vbox',

        items: [ {
            xtype: 'panel',
            html: '<h1>Info</h1>',
            flex: 2
        }, {
            // This is going to contain our map,
            // it is used to mask it when offline
            // and have an image till the map is loaded.
            xtype: 'container',
            id: 'mapContainer',
            flex: 5,
            layout: 'fit',
    
            // Add an image, so the user knows that
            // a map will appear
            items: [ {
                xtype: 'image',
                src: 'maps_offline.png',
            } ],
    
            // Disable the component and give a status message
            masked: {
                xtype: 'loadmask',
                message: "Google Maps isn't loaded.",
                indicator: false,
            },
        }],
    },
});

Now we have a basic layout having some heading (just for show) and an area for the map. Then it’s for the “fun” part:

Doing the magic

The real magic is in the controller for the view. Let’s start by creating the frame with all the functions and references we need to get what we need:

var mapInit; // please ignore this for now
  
Ext.define('Project.controller.DetailViewController', {
  extend: 'Ext.app.Controller',
  
  config: {
    refs: {
      detailView: 'detailView',
      mapContainer: 'detailView #mapContainer',
    },
  
    control: {
      detailView: {
        show: 'showDetails',
      },
    },
  },
  
  /**
     Show the model data. Most importantly set up the map
     in a nice way even if the phone is offline.
    */
  showDetails: function() {
  },
  
  /**
     Asks phonegap if there's a connection to the
     internet.
      
     @returns {Boolean} true if connected.
    */
  hasConnection: function() {
  },
  
  /**
     Listen for connectionchanges from PhoneGap
    */
  addConnectivityListeners: function() {
  },
  
  /**
     Download the Google Maps JavaScript and execute it.
      
     @param me
              the current class or another place where
              the functions showMap and enableMap resides
    */
  loadGoogleMaps: function(me) {
  },
  
  /**
     Remove the dummy picture and show the real map with
     a marker on the model place.
    */
  showMap: function() {
  },
  
  /**
     Unmask the map container, so the map can be used.
    */
  enableMap: function() {
  },
  
  /**
     Masks the map container with a user friendly message
     when offline or maps isn't loaded.
    */
  disableMap: function() {
  },
});

Hopefully this will give some idea of what will happen throughout this code. Now for the individual parts. The most important part, and the part that caused me most trouble is how to load the maps API dynamically.

Conjuring the API

I found a lot of examples using the classic

For doing this, I need an API key from Google, for more info read at: https://developers.google.com/maps/documentation/javascript/tutorial#api_key and then I could do as follows:

loadGoogleMaps: function(me) {
    var me = this;
    console.log('Load Google APIs');
    
    this.getMapContainer().setMasked({
        xtype: 'loadmask',
        message: 'Downloading Google Maps.',
        indicator: true,
    });
    
    mapInit = function() {
        console.log("Map loaded");
        me.showMap();
        me.enableMap();
    };
    
    var apiKey = 'YOUR KEY HERE';
    var script = document.createElement("script");
    script.type = "text/javascript";
    script.src = 'http://maps.google.com/maps/api/js?" +
            'key=' + apiKey + '&sensor=true&' +
            callback=mapInit';
    document.head.appendChild(script);
},

First off I mask the container with a status about downloading the map, so the user knows that something is happening.

Secondly I create a callback function and store it in the mapInit variable that I created at the top of the document. This is used for Google Maps to tell when it’s done loading and enables me to show the now loaded map. In the function, I show the map code (will be implemented in a moment) and enable it (also to be implemented).

Lastly, I create a new script-element with the Google Maps url, and append it to the head, so all the “magic” will happen.

Reading the phones mind

By using PhoneGap, I’ll be able to read different states of the phone and react properly to these. The interesting parts here are to check if the phone is online and to listen for the events telling that the state has changed:

hasConnection: function() {
    var networkState = navigator.network.connection.type;

    return networkState != Connection.NONE &&
        networkState != Connection.UNKNOWN;
},

Assuming that PhoneGap is correctly set up, this will return true if the phone is connected. The check for UNKNOWN turned out to be needed when simulating connectivity on the iPhone simulator. Now for the events:

addConnectivityListeners: function() {
    var me = this;
    
    var onOnline = function() {
        console.log('online');
        if (typeof google == "undefined" || typeof google.maps == "undefined") {
            me.loadGoogleMaps(me);
        } else {
            if (!this.online) {
                me.showMap();
                this.online = true;
            }
            me.enableMap();
        }
    };
    
    var onOffline = function() {
        console.log('offline');
        me.disableMap();
    };
    
    document.addEventListener("online", onOnline,
        false);
    document.addEventListener("offline", onOffline,
        false);
},

This makes the phone react when the connection changes using the two anonymous callbacks I create in the function. Most important is the check in onOnline, where I check if google and google.maps has been defined. If they are, I assume that maps have been loaded correctly (note: I have no guarantee that it actually is) and enable the map. Otherwise, I tell my script to load it.

Pulling the map out of the hat

One of the last things needed to be done is to create all the logic that updates the UI, so the user actually gets a result.

enableMap: function() {
    this.getMapContainer().setMasked(false);
},

disableMap: function() {
    this.getMapContainer()
        .setMasked({
        xtype: 'loadmask',
        message: 'Google Maps only works when ' +
            'you\'re online, please connect to ' +
            'use it.',
        indicator: false,
    });
},

These two are quite simple. enableMap removes the mask that tells the user that he needs to be online, and disableMap adds a message telling the user to go online.

showMap: function() {
    console.log('Showing map');
    var me = this;
    
    var map = Ext.create('Ext.Map', {
        useCurrentLocation: true,
        layout: 'fit',
        
        mapOptions: {
            mapTypeId: google.maps.MapTypeId.ROADMAP,
            streetViewControl: false,
            zoomControl: true,
            zoomControlOptions: {
                style: google.maps.ZoomControlStyle.SMALL,
                position: google.maps.ControlPositionRIGHT_BOTTOM,
            }
        },
        
        listeners: {
            maprender: function(exMap, googleMap, opts) {
                var modelData = me.getDetailView().getModelData();
                
                var point = new google.maps.LatLng(
                    modelData.get("latitude"),
                    modelData.get("longitude")
                );
                var marker = new google.maps.Marker({
                    position: point
                });
                
                marker.setMap(googleMap);
            },
        },
    });
    
    this.getMapContainer().removeAt(0);
    this.getMapContainer().add([ map ]);
    this.getMapContainer().setActiveItem(map);
},

This is a bit more complex. To put things short, I create a map element and add a marker when it’s loaded. Then I remove the image from the container, add the map and activate it.

The prestige

Finally, there’s only one thing left to do and that is to actually run the code when the map container is loaded.

showDetails: function() {
    this.addConnectivityListeners();
    this.online = this.hasConnection();
    
    if (typeof google == "undefined" || typeof google.maps == "undefined") {
        console.log("Google maps JavaScript needs to be loaded.");
    
        this.disableMap();
        if (this.online) {
            this.loadGoogleMaps(this);
        }
    } else {
        if (this.online) {
            this.showMap();
            this.enableMap();
        } else {
            this.disableMap();
        }
    }
},

In short terms this adds the listeners to react to connectivity changes. Load the maps API if they aren’t loaded and the phone was connected after load, but before listening for connectivity. Lastly, update the container with a status message or the map.