Over the past few weeks, I started working on a jQuery Mobile based application that would tie together some of my work with campus maps and campus events. My use case was getting a user information about departments, buildings, campus and local events. I've gotten to an alpha version that is deployed to my iPhone and iPad.
I wanted to spend some time learning the following technologies:
Below are some of my notes on where I got stuck and how I got unstuck. But first, here are some early screen shots. The first is a simple home screen design pattern; a list of options for the app. The second is a filtered list; type in a few characters and the list shortens. Then we have a map with marker; note the location button would grab the users current geolocation and drop a second marker on the map.
jQuery Mobile / Cordova
I selected jQuery Mobile as the framework I wanted to work with due its ability to work with Phonegap, now Cordova. I had done some early experimenting with Phonegap tutorials I found on-line and in a book I had purchased. [reference links] The second reason I selected jQuery Mobile was for its cross-platform, HTML 5. One of the nicest things about this framework is that it has a large collection of UI widgets and a theming system that gives a polished look without a lot of graphic design time (not my strength).
One of the hopes for the Cordova framework is being able to target multiple mobile platforms. I'm primarily interested in iOS and Android. I've only gotten as far as iOS; we'll work on Android over time. Another reason to focus on Cordova is its ability to direclty interface with mobile device features; unlike other mobile approaches. So, if I want to vibrate the device or use the compass or accelerometer, Cordova has methods for my app to get to those features.
Drupal 7 Views geoJSON
Today, our existing campus map uses a Drupal back end with a Drupal 6 OpenLayers module acting as an interface to place markers on the map.This give us a method to mark various types of map items. (You can read about the campus map in other articles on this site.) Because I wanted to review the Drupal 7 version of OpenLayers, I created a new site and imported the map data from the production site into this new development site. Further, I was looking for a method to pull the map data points (latitude, longitude, buidling or department, etc.) into a map loaded on a mobile device.
As I learn more about the technologies of web mapping, it seemed appropriate to utilize a JSON (Javascript Object Notation) feed. Leaflet had good documentation and examples of how to work with geoJSON data. In my case, we're only using points, but lines, polygons, etc., can be generated. A geoJSON feed was available from a views module available for Drupal 7, so that's what I set up. And that's when the cross-domain problems started.
JSON and JSONP
You see, while I could send a URL request (http://host/campus_departments.json) and get back a response, getting the mobile app to issue the request violates the cross-domain policy. Since my data is pretty static, my fisrt order solution was to simply save the file and save it locally to the mobile app as 'data/campus_departments.json'. I could then iterate the data with jQuery and generate lists of departments and building names in jQuery Mobile.
So from an app design perspective, we could have handled new buildings or changes to departments via some type of upgrade process. But that wouldn't be very elegant. It would be much easier to make a change once for a record in the Drupal database and then have it dynamically available to the app. In order to make that happen, I had to figure out JSONP. JSONP is JSON "with padding". That's not very explanatory but what it meant for me is that by reformatting my ajax query and matching the JSONP callback function in the Drupal geoJSON UI, I started getting successful interchanges. Below, you can see the difference between a geoJSON and geoJSONP formatted response.
{"type":"FeatureCollection", "features":[ ]}
mycallback({"type":"FeatureCollection", "features":[ ]})
The code below makes a call to the Drupal server and processes the results using the Mustache library. The Mustache library is used to template or pull out the gis coordinates and title out of the geoJSON for each li element in the template.
$("#chooseBuilding").live( 'pageinit', function(event) {
$.ajax({
url: "http://hostname/departments_buildings?",
type: "GET",
dataType: "jsonp",
jsonpCallback: "mycallback",
crossDomain: "true",
data: "status=1&type=map_item&field_building_value=All&field_dept_value=Yes&callback=mycallback",
complete: function(data) {
//called when complete
//alert('complete');
},
success: function(data) {
//called when successful
//alert('success');
// got JSON, now template it with mustache
//extract the content from JSON, append it as <li>'s to the <ul>
$('#templateTest').mustache(data).appendTo('#content ul');
// run refresh on the listview to get the theme added correctly
$('#content ul').listview('refresh');
},
error: function(data) {
//called when there is an error
//alert('error');
}
});
}); // end of pageinit
Where I struggled with this was in using examples without the specific jsonpCallback entry as noted above. The error I was getting was "Uncaught SyntaxError: Unexpected token :". I finally came across a post that explained that without the "wrapping" of the callback function, the parser would choke on the unexpected colon character. Oh well, there was three hours of research.
Back to jQuery Mobile and Mustache
Below, we'll look at the HTML used in the page that builds the list of buildings or departments (currently these are separate pages, but may be consolidated).
<body>
<div data-role="page" data-title="Camps Buildings" id="chooseBuilding" addBackBtn="true">
<div data-role="header" data-add-back-btn="true">
<a href="index.html" rel="external" data-icon="arrow-l" data-direction="reverse" class="ui-btn-left" data-theme="b" >Back</a>
<h1>Campus Buildings</h1>
<a href="index.html" rel=
"external" data-icon="home" data-iconpos="notext" data-direction="reverse" class="ui-btn-right" data-theme="b" >Home</a>
</div>
<div data-role="content" id="content">
<ul data-role="listview" data-filter="true" id="buildingList" data-theme="b">
<script id="templateTest" type="x-tmpl-mustache">
{{#features}}
{{#properties}}
<li id="{{name}}">
<a href="map.html?{{#geometry}}{{#geometries}}{{coordinates}}{{/geometries}}{{/geometry}}&title={{name}}" rel="external">{{name}}</a></li>
{{/properties}}
{{/features}}
</script>
</ul>
</div> <!-- /content -->
</div> <!-- /page -->
</body>
What we have here is the basic jQuery mobile page layout. We open with a page div, followed by a header div that contains the h1 tag. In those divs are special jQuery mobile parameters that you can read about in the jQuery docs. In the content div, you can see the ul element that begins our unordered list.
The script templateTest is an embedded javascript tag that implements the templating via the Mustache library. The double curly braces indicate the nesting of the geoJSON object that we are iterating through. There are lots of good Mustach tutorials out there. See the reference section below.
So what's happens is that Mustache is given the geoJSON data. Mustache extracts the {{coordinates}} and {{name}} for each building or department and inserts them into the href tag. As I refine this code, I'll probably insert a map-coordinates parameter into the href tag and then place the coordinates there, then use $.mobile.changePage() to get the data into the navigation. In the meantime, this works. We end up with a href link that looks something like map.html?-122.061946392,36.9968760663&title=Academic%20Senate. There's some code on the map.html page that parses the URL, creates a marker and popup and then pulls in the map as you can see in the screen shots above.
Leaflet
Leaflet is a lightweight javascript mapping library. It utilizes Cloudmade and OpenStreetMap data. I've been looking for alternatives to Google maps due to some issues with data accuracy. They have some good documentation and examples to work through, so I went for it.
Our design pattern takes the list of buildings or departments and then provides a link that will drop a marker on the map with a popup window. Thus we have to parse the URL into pieces on the map.html page so we can create the map instance, the marker, and the popup. There were a few gotchas that held me back in my early testing.
One such problem was getting the Leaflet map to draw correctly in the viewport. The static examples worked fine. My implementation didn't. Part of the problem was integrating with jQuery Mobile. There were lots of Google examples, but not many Leaflet examples to analyze. One problem was triggering a resizing event so that the map tiles would draw in the div container properly. I finally found the Leaflet plugin for Leaflet and started using map.invalidateSize() which got me going.
Another gotcha was that my Drupal OpenLayers WKT data. WKT data is ordered in longitude, latitude. Leaflet wants inputs as latitude, longitude. I finaly came across a post by David Hall that showed a similar example. My aha moment was that he declared lat and lon as float types. I'd been trying to work with them as strings! aha! duh
Next Steps
Now that the design pattern is working, the next steps include refining and polshing. That will include adding some thematic colors and icons, navigation improvements and integration of Phonegap/Cordova plugins to some of the native aspects of the mobile device (compass, accelerometer). Features I hope to implement soon include geolocation of your current position in relation to the building/department you want to navigate to (a Lisa Gardner idea) and gesture integration (swipe, pinch, zoom), orientation changes for iPhone and iPad. More later.