This article is the fourth in a series about my adventures developing a Fitness Tracking application with HTML5, Play Scala, CoffeeScript and Jade. Previous articles can be found at:
- Integrating Scalate and Jade with Play 1.2.3
- Trying to make CoffeeScript work with Scalate and Play
- Integrating HTML5 Boilerplate with Scalate and Play
Developing Features
After getting my desired infrastructure setup, I started coding like a madman. The first feature I needed was a stopwatch to track the duration of a workout, so I started writing one with CoffeeScript. After spending 20 minutes playing with dates and setTimeout, I searched and found a stopwatch jQuery plug-in. I added this to my app, deployed it to Heroku, brought up the app on my iPhone 3G, clicked Start and started riding my bike to work.
When I arrived, I unlocked my phone and discovered that the time had stopped. At first, I thought this was a major setback. My disappointed disappeared when I found a Super Neat JavaScript Stopwatch and Kåre Byberg's version that worked just fine. This stopwatch used setTimeout, so by keeping the start time, the app on the phone would catch up as soon as you unlocked it. I ported Kåre's script to CoffeeScript and rejoiced in my working stopwatch.
# Created by Kåre Byberg © 21.01.2005. Please acknowledge if used
# on other domains than http://www.timpelen.com.
# Ported to CoffeeScript by Matt Raible. Also added hours support.
flagClock = 0
flagStop = 0
stopTime = 0
refresh = null
clock = null
start = (button, display) ->
clock = display
startDate = new Date()
startTime = startDate.getTime()
if flagClock == 0
$(button).html("Stop")
flagClock = 1
counter startTime, display
else
$(button).html("Start")
flagClock = 0
flagStop = 1
counter = (startTime) ->
currentTime = new Date()
timeDiff = currentTime.getTime() - startTime
timeDiff = timeDiff + stopTime if flagStop == 1
if flagClock == 1
$(clock).val formatTime timeDiff, ""
callback = -> counter startTime
refresh = setTimeout callback, 10
else
window.clearTimeout refresh
stopTime = timeDiff
formatTime = (rawTime, roundType) ->
if roundType == "round"
ds = Math.round(rawTime / 100) + ""
else
ds = Math.floor(rawTime / 100) + ""
sec = Math.floor(rawTime / 1000)
min = Math.floor(rawTime / 60000)
hour = Math.floor(rawTime / 3600000)
ds = ds.charAt(ds.length - 1)
start() if hour >= 24
sec = sec - 60 * min + ""
sec = prependZeroCheck sec
min = min - 60 * hour + ""
min = prependZeroCheck min
hour = prependZeroCheck hour
hour + ":" + min + ":" + sec + "." + ds
prependZeroCheck = (time) ->
time = time + "" # convert from int to string
unless time.charAt(time.length - 2) == ""
time = time.charAt(time.length - 2) + time.charAt(time.length - 1)
else
time = 0 + time.charAt(time.length - 1)
reset = ->
flagStop = 0
stopTime = 0
window.clearTimeout refresh
if flagClock == 1
resetDate = new Date()
resetTime = resetDate.getTime()
counter resetTime
else
$(clock).val "00:00:00.0"
@StopWatch = {
start: start
reset: reset
}
The Scalate/Jade template to render this stopwatch looks as follows:
script(type="text/javascript" src={uri("/public/javascripts/stopwatch.coffee")})
#display
input(id="clock" class="xlarge" type="text" value="00:00:00.0" readonly="readonly")
#controls
button(id="start" type="button" class="btn primary") Start
button(id="reset" type="button" class="btn :disabled") Reset
:plain
<script type="text/coffeescript">
$(document).ready ->
$('#start').click ->
StopWatch.start this, $('#clock')
$('#reset').click ->
StopWatch.reset()
</script>
Next, I wanted to create a map that would show your location. For this, I used
Merge Design's HTML 5 Geolocation Demo as a guide. The HTML5 Geo API is pretty
simple, containing only three methods:
// Gets the users current position
navigator.geolocation.getCurrentPosition(successCallback,
errorCallback,
options);
// Request repeated updates of position
watchId = navigator.geolocation.watchPosition(successCallback, errorCallback);
// Cancel the updates
navigator.geolocation.clearWatch(watchId);
After rewriting the geolocation example in CoffeeScript, I ended up with the following code in my map.coffee script. You'll notice it uses
Google Maps JavaScript API to show an actual map
with a marker.
# Geolocation with HTML 5 and Google Maps API based on example from maxheapsize:
# http://maxheapsize.com/2009/04/11/getting-the-browsers-geolocation-with-html-5/
# This script is by Merge Database and Design, http://merged.ca/ -- if you use some,
# all, or any of this code, please offer a return link.
map = null
mapCenter = null
geocoder = null
latlng = null
timeoutId = null
initialize = ->
if Modernizr.geolocation
navigator.geolocation.getCurrentPosition showMap
showMap = (position) ->
latitude = position.coords.latitude
longitude = position.coords.longitude
mapOptions = {
zoom: 15,
mapTypeId: google.maps.MapTypeId.ROADMAP
}
map = new google.maps.Map(document.getElementById("map"), mapOptions)
latlng = new google.maps.LatLng(latitude, longitude)
map.setCenter(latlng)
geocoder = new google.maps.Geocoder()
geocoder.geocode({'latLng': latlng}, addAddressToMap)
addAddressToMap = (results, status) ->
if (status == google.maps.GeocoderStatus.OK)
if (results[1])
marker = new google.maps.Marker({
position: latlng,
map: map
})
$('#location').html('Your location: ' + results[0].formatted_address)
else
alert "Sorry, we were unable to geocode that address."
start = ->
timeoutId = setTimeout initialize, 500
reset = ->
if (timeoutId)
clearTimeout timeoutId
@Map = {
start: start
reset: reset
}
The template to show the map is a mere 20 lines of Jade:
script(type="text/javascript" src="//www.google.com/jsapi")
script(type="text/javascript" src="//maps.googleapis.com/maps/api/js?sensor=false")
:css
.demo-map {
border: 1px solid silver;
height: 200px;
margin: 10px auto;
width: 280px;
}
#map(class="demo-map")
p(id="location")
span(class="label success") New
| Fetching your location with HTML 5 geolocation...
script(type="text/javascript" src={uri("/public/javascripts/map.coffee")})
:javascript
Map.start();
The last two features I wanted were 1) distance traveled and 2) drawing the route taken on the map. For this I learned from
A Simple Trip Meter using the Geolocation API.
As I was beginning to port the JS to CoffeeScript, I thought, "there's got to be a better way." I searched and found Js2coffee to do most of the conversion for me. If you know JavaScript and you're learning CoffeeScript, this is an invaluable tool.
I tried out the trip meter that evening
on a bike ride and noticed it said I'd traveled 3 miles when I'd really gone 6. I quickly figured out it was only calculating
start point to end point and not taking into account all the turns in between. To view what was happening, I integrated my
odometer.coffee with my map using
Google Maps Polylines. Upon finishing the integration, I discovered two things, 1) HTML5 geolocation was highly inaccurate and
2) geolocation doesn't run in the background.
I was able to solve the first problem by passing in {enableHighAccuracy: true} to navigator.geolocation.watchPosition(). Below are two screenshots showing before high accuracy and after. Both screenshots are from the same two-block walk.
The second
issue is a slight show-stopper. PhoneGap might be able to solve the problem, but I'm currently using a
workaround → turning off auto-lock and keeping Safari in the foreground.
Making it look good
After I got all my desired features developed, I moved onto making the app look good. I started by using SASS for my CSS and installed Play's SASS module. I then switched to LESS when I discovered and added Twitter's Bootstrap to my project. At first I used Play's LESS module (version 0.3), but ran into compilation issues. I then tried Play's GreenScript module, but gave up on it when I found it was incompatible with the CoffeeScript module. Switching back to the LESS module and using the "0.3.compatibility" version solved all remaining issues.
You might remember that I integrated HTML5 Boilerplate and wondering why I have both Bootstrap and Boilerplate in my project. At this point, I don't think Boilerplate is needed, but I've kept it just in case it's doing something for HTML5 cross-browser compatibility. I've renamed its style.css to style.less and added the following so it has access to Bootstrap's variables.
/* Variables from Bootstrap */
@import "libs/variables.less";
Then I made my app look a lot better with layouts, stylish forms, a fixed topbar and alerts. For example, here's the CoffeeScript I wrote to display geolocation errors:
geolocationError = (error) ->
msg = 'Unable to locate position. '
switch error.code
when error.TIMEOUT then msg += 'Timeout.'
when error.POSITION_UNAVAILABLE then msg += 'Position unavailable.'
when error.PERMISSION_DENIED then msg += 'Please turn on location services.'
when error.UNKNOWN_ERROR then msg += error.code
$('.alert-message').remove()
alert = $('<div class="alert-message error fade in" data-alert="alert">')
alert.html('<a class="close" href="#">×</a>' + msg);
alert.insertBefore($('.span10'))
Then I set about styling up the app so it looked good on a smartphone with CSS3 Media Queries. Below is the LESS code I used to hide elements and squish the widths for smaller devices.
@media all and (max-device-width: 480px) {
/* hide scrollbar on mobile */
html { overflow-y:hidden }
/* hide sidebar on mobile */
.home .span4, .home .page-header, .topbar form {
display: none
}
.home .container {
width: 320px;
}
.about {
.container, .span10 {
width: 280px;
}
.span10 {
padding-top: 0px;
}
}
Tools
In the process of developing a stopwatch, odometer, displaying routes and making everything look good, I used a number of tools. I started out primarily with TextMate and its bundles for LESS, CoffeeScript and Jade. When I started writing more Scala, I installed the Scala TextMate Bundle. When I needed some debugging, I switched to IntelliJ and installed its Scala plugin. CoffeeScript, LESS and HAML plugins (for Jade) were already installed by default. I also used James Ward's Setup Play Framework with Scala in IntelliJ.
Issues
I think it's obvious that my biggest issue so far is the fact that a webapp can't multitask in the background like a native app can. Beyond that, there's accuracy issues with HTML5's geolocation that I haven't seen in native apps.
I also ran into a caching issue when calling getCurrentPosition(). It only worked the first time and I had to refresh my browser to get it to work again. Strangely enough, this only happened on my desktop (in Safari and Firefox) and worked fine on my iPhone. Unfortunately, it looks like PhoneGap has issues similar to this.
My workaround for no webapp multitasking is turning off auto-lock and leaving the browser in the foreground while I exercise. The downside to this is it really drains the battery quickly (~ 3 hours). I constantly have to charge my phone if I'm testing it throughout the day. The testing is a real pain too. I have to deploy to Heroku (which is easy enough), then go on a walk or bike ride. If something's broke, I have to return home, tweak some things, redeploy and go again. Also, there's been a few times where Safari crashes halfway through and I lose all the tracking data. This happens with native apps too, but seemingly not as often.
If you'd like to try the app on your mobile phone and see if you experience these issues, checkout play-more.com.
Summary
Going forward, there's still more HTML5 features I'd like to use. In particular, I'd like to play music while the fitness tracker is running. I'd love it if cloud music services (e.g. Pandora or Spotify) had an API I could use to play music in a webapp. Soundcloud might be an option, but I've also thought of just uploading some MP3s and playing them with the <audio> tag.
I've really enjoyed developing with all these technologies and haven't experienced much frustration so far. The majority has come from integrating Scalate into Play, but I've resolved most problems. Next, I'll talk about how I've improved Play's Scalate support and my experience working with Anorm.