Sencha Touch 2 – The maps connectivity issue

IPhone

Image from FreeDigitalPhotos.net

Using the Map component in Sencha Touch 2 is really convenient. Right until you test it on an phone that’s offline. Creating a nice looking and userfriendly 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 <script>-tag, as you’d do normally your app won’t start when you’re offline. The second thing you notice is that the script system in app.json from ST doesn’t load it when you become online either. Even worse a lot of the ways you’re likely to try to dynamically load the scripts will likely fail.

Setting the scene

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

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
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's 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:

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
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 <script>-tag in head, and adding it by creating different kinds of request. The problem I ran into was that when I downloaded the code and ran it, the page would just turn white. For some reason something in Google’s JavaScript interfered with my code. The solution turned out to be quite simple when I just looked the right place.

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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
  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 in 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 is to check if the phone is online and to listen for the events telling that the state has changed:

1
2
3
4
5
6
  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:

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
  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 has 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 is 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.ControlPosition.RIGHT_BOTTOM,
        }
      },

      listeners: {
        maprender: function(extMap, 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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
  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 listernes to react to connectivity changes. Loads the maps API if they aren’t loaded and the phone was connected after load, but before listening for connectivity. Lastly updates the container with a status message or the map.

This entry was posted in Uncategorized by admin. Bookmark the permalink.

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>