Trains on the Range

I’m at SpringOne2GX this week, but this morning before my first presentation I noticed the following in my Twitter feed:

DailyGrailsTip_ranges

First of all, I think that’s the first time anything of mine has ever appeared in @DailyGrailsTip, so yay for me. On the other hand, the link is to a script that isn’t terribly self explanatory. At the risk of spoiling part of my Advanced Groovy Tips and Tricks talk, I thought I’d discuss it here.

The basic idea is that any Groovy class can be made into a range if it:

  1. implements java.util.Comparable
  2. has a next method
  3. has a previous method

My example uses a TrainStation class, which forms a doubly-linked list when I place them along a track. So part of the implementation is trivial:
[sourcecode language=”groovy”]
class TrainStation implements Comparable<TrainStation> {
// Links to next and previous stations
TrainStation next
TrainStation previous

TrainStation next() { next }
TrainStation previous() { previous }

@Override // implement Comparable
int compareTo(TrainStation ts) {
// Hmm. What to do here?
}
}
[/sourcecode]
The TrainStation has a reference to the next one and the previous one, and the implementations of next and previous just return them. (I considered making a wrapper class called Track, but kept it simple here.)

The question is, how am I going to implement Comparable? Since one important characteristic of train stations is their physical location, I decided to use latitude and longitude, and made the comparison based on latitude.

The result is:
[sourcecode language=”groovy”]
import java.text.NumberFormat

class TrainStation implements Comparable<TrainStation> {

// Links to next and previous stations
TrainStation next
TrainStation previous

// location attributes
String city
String state

// set by Geocoder service
BigDecimal latitude
BigDecimal longitude

TrainStation next() { next }
TrainStation previous() { previous }

@Override
int compareTo(TrainStation ts) {
this.latitude <=> ts.latitude
}

String toString() {
NumberFormat nf = NumberFormat.instance
"$city, $state \t (${nf.format(latitude)}, ${nf.format(longitude)})"
}
}
[/sourcecode]
Each train station has a city and state, which I set, and a latitude and longitude, which are computed from a geocoder. My geocoder is based on Google’s RESTful web service, which I’ve used many times before:
[sourcecode language=”groovy”]
class Geocoder {
public static final String BASE = ‘http://maps.google.com/maps/api/geocode/xml?&#8217;

void fillInLatLng(TrainStation station) {
String encoded =
[station.city, station.state].collect {
URLEncoder.encode(it,’UTF-8′)
}.join(‘,’)
String qs = [sensor:false, address: encoded].collect { it }.join(‘&’)
def response = new XmlSlurper().parse("$BASE$qs")
station.latitude = response.result[0].geometry.location.lat.toBigDecimal()
station.longitude = response.result[0].geometry.location.lng.toBigDecimal()
}
}
[/sourcecode]
The fillInLatLng method takes a train station and URL encodes its city and state, so that they can be sent as part of a URL. It then builds a query string by converting the map entries in the form “key:value” into list entries in the form “key=value”.

(Note: I used to write that as [a:1, b:2].collect { k,v -> "$k=$v" }. Then Paul King pointed out that the default toString method on Map.Entry is in fact “key=value”. Therefore I can just write [a:1, b:2].collect { it } and it does the same thing. Live and learn.)

After building the query string, I use the parse method from XmlSlurper to access the web service, download the resulting XML, parse it into a DOM tree, and hand me the root. The rest is just walking the tree, keeping in mind that sometimes Google returns more than one request, so I made sure to use the first one only by writing result[0].

To use the new class, I need to create some train station and set their next and previous properties. Then I can use them in a range. Here’s a script to do all that:
[sourcecode language=”groovy”]
// Amtrak, NE corridor (mostly)
TrainStation was = new TrainStation(city:’Washington’, state:’DC’)
TrainStation bal = new TrainStation(city:’Baltimore’, state:’MD’)
TrainStation wil = new TrainStation(city:’Wilmington’, state:’DE’)
TrainStation phl = new TrainStation(city:’Philadelphia’, state:’PA’)
TrainStation nwk = new TrainStation(city:’Newark’, state:’NJ’)
TrainStation nyc = new TrainStation(city:’New York’, state:’NY’)
TrainStation nhv = new TrainStation(city:’New Haven’, state:’CT’)
TrainStation pvd = new TrainStation(city:’Providence’, state:’RI’)
TrainStation bos = new TrainStation(city:’Boston’, state:’MA’)

// Arrange the stations on the track
was.next = bal; bal.previous = was
bal.next = wil; wil.previous = bal
wil.next = phl; phl.previous = wil
phl.next = nwk; nwk.previous = phl
nwk.next = nyc; nyc.previous = nwk
nyc.next = nhv; nhv.previous = nyc
nhv.next = pvd; pvd.previous = nhv
pvd.next = bos; bos.previous = pvd

def ne_corridor = [was, bal, wil, phl, nwk, nyc, nhv, pvd, bos]

// Fill in all the latitudes and longitudes
Geocoder geo = new Geocoder()
ne_corridor.each { station ->
geo.fillInLatLng(station)
}

// range heading north
println ‘\nNorthbound from WAS to BOS’
(was..bos).each { println it }

// range heading south
println ‘\nSouthbound from BOS to WAS’
(bos..was).each { println it }

// subrange from nyc to bos
println ‘\nNYC to BOS’
(nyc..bos).each { println it.city }
[/sourcecode]
The resulting output is:

Northbound from WAS to BOS
Washington, DC (38.907, -77.036)
Baltimore, MD (39.29, -76.612)
Wilmington, DE (39.746, -75.547)
Philadelphia, PA (39.952, -75.164)
Newark, NJ (40.736, -74.172)
New York, NY (40.714, -74.006)
New Haven, CT (41.308, -72.928)
Providence, RI (41.824, -71.413)
Boston, MA (42.358, -71.06)

Southbound from BOS to WAS
Boston, MA (42.358, -71.06)
Providence, RI (41.824, -71.413)
New Haven, CT (41.308, -72.928)
New York, NY (40.714, -74.006)
Newark, NJ (40.736, -74.172)
Philadelphia, PA (39.952, -75.164)
Wilmington, DE (39.746, -75.547)
Baltimore, MD (39.29, -76.612)
Washington, DC (38.907, -77.036)

NYC to BOS
New York
New Haven
Providence
Boston

There you have it. I’ve taken an arbitrary class, TrainStation, and enabled it to be used in a Groovy range. This whole demo is part of the code I use in my “Advanced Groovy” talk, stored in my GitHub repository at https://github.com/kousen/AdvancedGroovy .

Quick book update: The ebook version of Making Java Groovy should be released today. The print book should be available from Manning on 9/16 or 9/17. The remaining mobile formats (epub and mobi) will be released on 9/27. I’m still hoping to have some physical copies here at the conference to give away, but I haven’t seen them yet.

Btw, it’s wonderful seeing so many of the Groovy and Grails team members at the conference. I seriously doubt, however, that any of them took the train to get here. 🙂

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.