making geo mashups with ruby and ms virtual earth
I've put together some helper classes for making geo mashups with ruby and MS Virtual Earth.
The first is called 'PlaceFinder', which takes a list of places (stored as a yaml file), then lets that list be searched by name.
In the examples below, I've used my list of NSW suburbs. Unfortunately, the co-ordinates in this file are a kilometer or so out of alignment with the geodetic system used by either the MS or Google map servers, which is noticible when you zoom in to street level, but for a high level 'city wide' view, it's close enough.
Here's an irb session showing how placefinder works:
irb(main):001:0>
irb(main):002:0* require 'placefinder'
=> true
irb(main):003:0> place_finder=PlaceFinder.new
=> #<PlaceFinder:0x2c5d128 @places={}>
irb(main):004:0> place_finder.load_places("nsw_places.yml")
=> true
irb(main):005:0> place_finder.find_by_name("Leura")
=> #<Place:0x310a5e0 @latitude=-33.7089, @longitude=150.335, @postcode=2780, @name="Leura">
irb(main):006:0>
The second module is 'MapMaker', which has a single function 'make_map' that takes a list of points and a zoom factor, and returns html that loads a map from the Microsoft Virtual Earth website, with 'pins' at the specified points
each pin has the following attributes
name
longitude
latitude
caption (which can contain any HTML tags, but should not have any line break characters)
There's some predefined Zoom Factors whose name indicates approximately how large an area will be visible on screen:
MapMaker::ZOOM_STATE=6
MapMaker::ZOOM_GREATER_CITY=9
MapMaker::ZOOM_CITY=10
MapMaker::ZOOM_SUBURB=11
So to put this to use, we need a list of places we want to make pins for. For no particular reason other than that it makes for a fairly simple example, I've decided to plot the home grounds of the 12 rugby clubs that make up the sydney 'grade' competition. (For ancient political reasons, rugby in Sydney is administered in 2 tiers - the top tier is called 'Club Rugby', and the other tier is 'Subbies').
So I went looking for a list of all the sydney club rugby teams which has enough structure to parse with regular expressions.
Screen scraping is a bit of a black art, I'm not going to explain in detail what I'm doing to pull the info out of the web page, but I'm taking advantage of that fact that the data for each team is enclosed in a <tr>..</tr> tag set, and within each team, the data all has nice regexable prefixes like "Address: ", so a line like this
address=team_row.match(/Address: ([^<]*)/)[1]
means "extract the text that comes between the text 'Address: ' and the first '<' character."
The full example follows, but first, have a look at the output, a map showing the home grounds of the sydney club rugby teams. And all the code needed to make this work (including a list of locations of NSW suburbs) is in the file MapMaker_Demo.zip
require 'map_maker'
require 'placefinder'
place_finder=PlaceFinder.new
place_finder.load_places("nsw_places.yml")
require 'open-uri'
html=open('http://www.tahkids.com.au/aboutthe_TNC.html').read
pin_points=[]
#strip off all the stuff up until the table we're interested in
html.gsub!(/.*CLICK HERE/m,"")
#find all the <tr> elements that contain the text 'Nickname:'
team_rows=html.scan(/<tr>.*?Nickname:.*?<\/tr>/m)
team_rows.each do |team_row|
#the team name is the first text in bold
team_name=team_row.match(/<b>(\w[^<]*)/)[1]
home_ground=team_row.match(/Home Ground: ([^<]*)/)[1]
address=team_row.match(/Address: ([^<]*)/)[1]
suburb=address.sub(/.*,\s*/,"") #addresses are all of the form Street, Suburb so strip up to the comma
#some entries have a dud href for the website field so correct them
website=team_row.sub(/href="www/,'href="http://www').match(/http[^"]*/)[0]
caption="<a href=#{website}>#{team_name}</a><br>Home Ground: #{home_ground}"
#now we've extracted all the interesting data, make up a new pin point to stick on the map
pin_point=place_finder.find_by_name(suburb)
pin_point.name=team_name
pin_point.caption=caption
pin_points<<pin_point
end
#now make a map with the list of pin points
map_html=MapMaker.make_map(pin_points,MapMaker::ZOOM_CITY)
#save the html
File.open("sydney_club_rugby_map.html","w") {|f| f<<map_html}