Building a Parse.com Enabled PhoneGap App - Part 4
Join the DZone community and get the full member experience.
Join For FreeWelcome to the fourth part of my blog series on building a Parse.com enabled PhoneGap application. If you haven't yet read the earlier entries in this series, please see the links at the bottom for background and the story so far. In today's entry we're going to add geolocation to the application. This will be supported both in the tip reporting mechanism as well as in the page that fetches tips.
Luckily, Parse.com makes building location-aware applications very easy. In fact the code for these features are so simple you can add them within five minutes. Unfortunately, what isn't so easy is deciding how to use these features. I'm finding that more and more it isn't so much a problem of "How do I do X" but more "How do I X in a way that best works for my users." Consider the simple fact that geolocation can - and does - fail. What do I do? Do I let users enter an address? Do I display a small map so they can touch to select their location? What if they lie? Or what if they are just wrong?
At the end of the day - I don't think I'm a UX (User Experience) expert. Like most folks I recognize good UX versus bad UX, but it is much harder to decide what the best choice is when building your own apps. Hopefully a year or two from now I can say, confidently, that I'm making the right decisions. For now though I'm going to struggle through it (and - of course - share these struggles with you guys).
For now I've decided to make geolocation required. For adding a tip you will be prevented from saving the data if the geolocation check fails. For displaying tips, if we can't get your location, we don't bother getting any of the stored data.
Let's begin by looking at the changes to the tip form page. The first thing I did was add "disabled" as an attribute to the submit button. This means the page loads with the form immediately disabled. Next, I wrote the following snippet that will execute when the Add page loads.
if($("#addTipBtn").length === 1) { currentLocation=null; navigator.geolocation.getCurrentPosition(function(pos) { //store the long/lat currentLocation = {longitude:pos.coords.longitude, latitude:pos.coords.latitude}; $("#addTipBtn").removeAttr("disabled"); }, function(err) { //Since geolocation failed, we can't allow the user to submit doAlert("Sorry, but we couldn't find your location.\nYou may not post a cow tip."); }); }
currentLocation is defined in the root of my JavaScript file so it is a global variable. If everything works well, then we simply enable the form button. Otherwise, the user gets an error.
Once we have the user's location, how do we store it? Parse.com supports what are known as GeoPoints. Any Parse.com data type can support a geopoint, but only one geopoint. A geopoint is simply a longitude/latitude data pair. Using it is quite simple though. Consider this code taken from the form submission handler.
var tip = new TipObject(); var point = new Parse.GeoPoint({latitude: currentLocation.latitude, longitude: currentLocation.longitude}); tip.save( { numcows:numcows, howdangerous:howdangerous, comments:comments, location:point },{ success:function(object) { console.log("Saved object"); doAlert("Tip Saved!", function() { document.location.href='index.html'; }); }, error:function(model, error) { console.log("Error saving"); } });
Compared to our previous example, there are only two changes. First, I make an actual Geopoint with my location data. Then I simply add this value to the tip object. Remember that I said that we can - if we want - change the structure of our data at any time. After the application is released this is probably a bad idea (and I'll talk about how you can avoid that in the next entry), but for now it is pretty helpful.
That's it for the storage aspect. Any records created with this geopoint can now be searched in new and interesting ways. If you remember, our previous code for the "get" page grabbed everything from the database. This was as simple as creating a query and just running find on it. I wish it were more complex.
Here is where the beauty of Parse.com's Query system comes to play. If we want to filter the results, we can simply add more options to the object. For example, assuming I have your location, it takes one line to find data near you:
query.near("location", myLocation);
Seriously - that's it. In my tests this seemed to be within 50 miles or so. Oddly the docs for "near" do not specify exactly what near is. Near like the Sun? Near like Starbucks? I'm just not sure. However, they do provide a few alternatives. These are much more complex though. Instead of typing near, I have to type "withinMiles":
query.withinMiles("location", myLocation, 30);
Yes - I'm being a bit of an ass - but I can't stress enough how darn cool this is. (In case you don't live in a proper country with Imperial units, they also support withinKilometers.) I decided that for my application I wanted to return tips within 30 miles and reports that were no older than 7 days. Adding the date filter was three lines of code that I could have written as one:
var lastWeek = new Date(); lastWeek.setDate(lastWeek.getDate()-7); query.greaterThan("createdAt", lastWeek);
Here's the entire logic for retrieving data on the Get page.
if($("#tipdisplay").length === 1) { //Update status to let the user know we are doing important things. Really important. $("#tipdisplay").html("Please stand by. Checking your location for nearby cow tips!"); navigator.geolocation.getCurrentPosition(function(pos) { var myLocation = new Parse.GeoPoint({latitude: pos.coords.latitude, longitude: pos.coords.longitude}); //Begin our query var query = new Parse.Query(TipObject); //Only within 30 miles query.withinMiles("location", myLocation, 30); //only within last week var lastWeek = new Date(); lastWeek.setDate(lastWeek.getDate()-7); query.greaterThan("createdAt", lastWeek); query.find({ success:function(results) { renderResults(results,myLocation); }, error: function(error) { alert("Error: " + error.code + " " + error.message); } }); }, function(err) { //Since geolocation failed, we can't allow the user to submit doAlert("Sorry, but we couldn't find your location."); },{timeout:20000,enableHighAccuracy:false}); }
You may notice that I've moved the render portion into its own function. For the mapping portion of this application I decided to try the free mapping service Leaflet. Leaflet is not only free, but really easy to use too. Here's the function I use to render out to the map:
function renderResults(results,myLoc) { console.log("renderResults: "+results.length); $("#tipdisplay").html("Displaying tips within 30 miles and from the last 7 days."); var map = L.map('map').setView([myLoc.latitude, myLoc.longitude], 8); var layerOpenStreet = new L.TileLayer('http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {maxZoom:18, minZoom:1, attribution:'Map data © 2012 OpenStreetMap'}).addTo(map); var dangerLevels = ["Totally Safe","Some Risk","Farmer with Shotgun!"]; for(var i=0, len=results.length; i<len; i++) { var result = results[i]; var marker = L.marker([result.attributes.location.latitude, result.attributes.location.longitude]).addTo(map); var markerLabel = "Cows: "+result.attributes.numcows+"<br/>Danger: "+dangerLevels[result.attributes.howdangerous-1]; if(result.attributes.comments && result.attributes.comments.length) markerLabel += "<br>"+result.attributes.comments; marker.bindPopup(markerLabel); } }
Even if you've never seen Leaflet before, you can probably read that easily enough and see what it is doing. For each result I add a marker along with a little info window you can click to get the details. You can see an example of this below:
All in all - I've now got a complete, if basic, application. Don't forget you can see the complete source code for the application at the GitHub Repo and you can download builds of the application on the public PhoneGap Build site.
In the next, and final, part to this series, I'm going to discuss what can, and should, be done both on the PhoneGap side as well as the Parse.com side.
Published at DZone with permission of Raymond Camden, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.
Trending
-
Cypress Tutorial: A Comprehensive Guide With Examples and Best Practices
-
Micro Frontends on Monorepo With Remote State Management
-
Which Is Better for IoT: Azure RTOS or FreeRTOS?
-
RAML vs. OAS: Which Is the Best API Specification for Your Project?
Comments