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.

Discover more from Stuff I've learned recently...

Subscribe now to keep reading and get access to the full archive.

Continue reading