The github repo has been updated. Some of the code will not match what is currently in the repo.
An idea that I have had for awhile is a RunKeeper like application that runs completely in the browser. This is not due to any inherit issues with the RunKeeper application, just more of a proof-of-concept. HTML 5 makes this easy. We can save old runs and run without an Internet connection making it almost a native app.
HTML 5 gives us many new methods to make a webpage an application. Before HTML 5 there were many workarounds to make a webpage do what we wanted it to. Now they are built right into the browser. The first thing I am going to cover is application cache. This allows us to tell the browser to store certain resources in case we do not have access to the Internet. We also can specify fallback resources if need be. First we specify the cache manifest which tells the browser what to cache.
One bit of advice is to make sure that your web server is serving the cache manifest as mime-type text/cache-manifest. If not you will run into problems. Please see the resource I linked to for application cache or search the Internet on how to configure your web server. Once it serving correctly we tell it what to cache.
LocalStorage is the next HTML 5 feature we will use. It gives us a simple key -> value interface. We set each run’s key as its start based on the first point’s timestamp. We then JSON stringify our runPath object.
Application Cache and localStorage makes this webpage work like a native app. You can use it without a network connection and it will store information about past uses.
MVC stands for Model View Controller. It is a pattern to help define what object has what responsibilities. As I stated earlier I did not want to use a current MVC library like backbone so I wrote a poor imitation of one. This application is simple, so I built a simple MVC. I created one namespace RunBrowser with three objects to cover each Model – runPath, View – mapView, Controller – AppController. Ultimately the appController gets a location object from browser. It then sends it to the model – runPath to see if it meets the accuracy and time criteria. If it does it passes it the view – mapView to add a point and redraw the vector line. I will dive into each object later in the post.
The first three store objects and the others are all elements from the page.
The init function first creates an appController object and then it runs the method addEvents(). This method is where we tie the appController to our button click events. All we have to do is reference which element we want and use the method addEventListener for the click event.
You will notice that there is a lot of This. We do this so that the appController will handle the event and not the button. If we were to just use this.showMap(), for example, we would get an error telling us that the button element does not have that method. We have to create a variable that points to the appController object (var This = this;) then we call an anonymous function to handle the click and pass it to the appController. This one function makes the entire application work. Without it we would just have a static page that did nothing.
The first shows or hides certain divs (hide the home page, show the map page), creates a new view and model, and finally starts tracking our location. If there is a positive result we pass it to the GetLocation method with a location object.
GetLocation passes it to the model(runPath) to see if we should store it and then passes it to the view(mapView) to update the map and other data. If we get an error we pass it to the method errorHandler which just shows a modal and allows you to cancel back to the start. If you want to cancel a watchPosition you pass the id to the clearWatch method.
Let’s store someones run in localStorage. It’s is really easy, one method setItem which takes a key and a value. We do this in the saveRun method of appController.
First thing we do is save the run to localStorage with the start timestamp and the value as the object stringified. We clean up our objects by destroying the map and clearing out our model. Finally we set the correct divs show and hide the others.
All the other methods are straight forward. Showing and hiding divs as needed.
This application really only has one view – the map view. We create or destroy it as needed. I use Open Layers to do all the map drawing. It is an open source map and vector drawing library. It can use maps from Open Street Maps, Google, Bing, or even your own if you have created them. I use Open Street Maps in this application, but I did leave references to Google and Bing on the index page.
I spent a few hours reading forums and the documentation to get this to work. Hopefully I can save someone that trouble. Here is the code to create a map that is ready to have a line drawn.
Here is the breakdown of what I am doing. First I create a map on the div with id of map with new OpenLayers.Map(“map”). I then have to set a projection to make sure that our points line up correctly on the map with new OpenLayers.Projection(“EPSG:4326”). You can learn more why you need to specify a projection, but just know that if you do not use a projection your coordinates will not match up correctly. I create a blank line by creating it with a blank array using new OpenLayers.Geometry.LineString(). To make this work we have to add layers, controls, and features. The first layer is an Open Street Map layer. If you wanted to use different providers you would replace this line. Next I tell the lineLayer about the line and let it know to draw this on the map. Finally I let the map know about the layers (the actual map and the vector line) and then zoom to level 15.
At this point you would have a map that is zoomed in over the Atlantic Ocean and a vector line with no points. We need to add some points to this. We need to be able to add points on the fly. Here is how to do this.
This is our model object. It has the definitive list of location points and also reports out information about the run. The first thing to do is give it some attributes that we would care about.
We have the distance (in miles), the start timestamp, the last updated time stamp, and an array to hold all our points. Everything else we can calculate, for example duration or minutes per mile.
There are two core functions addPoint and computeDistance that do all the heavy lifting. Here is addPoint.
AddPoint accepts a location object from the browser. We then test to see if it has a good accuracy (usually anything under 50 will be from a GPS) so we don’t have big jumps in position and that it has been six seconds since the last point was saved. If we saved every point given to the function we could easily have hundreds per minute. This assures us we will have 10 or less per minute. If neither of these are true it looks to see if the last update time has been set, if not we know this is the first point. Then we set the last update time and if there is no start time we set that as well. Finally if the array has more than two points we are going to calculate the distance between the last two points and add it to our current distance. This leads us right to computeDistance.
I really don’t know the math behind this. This formula is from a Movable Type script. The only modification I made was to turn it into miles instead of km. The distance comes out really close to what I can measure.
We can compute the elapsed time, current mph(taken from the gps), overall mph, and minutes per mile based on the data we have stored. The only problem we run into with this object is when we stringify it. We lose all of our functions as JSON parses it back as an object. To fix this we create a new runPath object and set all the attributes. At this point we can call all the functions and get our information back.
As I have previously stated I only have an iPhone 4S so that is the only phone I have tested on. I am sure though that this will work in Android (if you have Android and have tested it let me know). Using Bootstrap I have designed the page to be responsive. The main thing is that the main heading will drop from an h1 to an h2 and the byline will change as well. Bootstrap makes this super simple, all you have to do is add the class visible-phone and it will show on devices with width at or under 480px. To hide something on a phone just add the class hidden-phone. You can test this between your phone and computer or just resize the width to under 480px.
There are a few things other things I added in the head that target Apple devices.
The meta viewport helps keep the page from zooming in or out to far. The next meta tag will let the page run in fullscreen mode when added to the home screen. The last three link will be used as icons when the page is added to the home screen. Each one targets a different iOS device and/or retina display. The icon I used is from Open Clipart and is release under CC0 1.0.
If you want to add or edit anything you can clone the github repository. If you want to completely rewrite parts of this application you should be able to reuse the mapView object and the runPath object. Rewire up an AppController and you should be on your way.
If you have thoughts or questions let me know.