9781449380373
_geolocation.html

Chapter 10. Geolocation

The example in this chapter shows how to write an application that uses the user’s geographical location and a location web service. To do that, we are going to use the CoreLocation framework, the gowalla.com web API (offering location information based on coordinates), and a TableView instance.

As before, we start by creating a MacRuby application using Xcode. Start Xcode and choose the MacRuby template. In the template settings, set the Product Name to “AroundMe” and disable the Document-Based and Core Data options.

User Interface

In this example, the UI will be very simple. We are going to work with just an NSTableView instance and a bunch of columns (NSTableColumn).

Edit your .xib file and resize the main window to be 640 pixels by 440 pixels (use the Size Inspector in the Utilities panel). Now drag and drop a Table View from the Object Library inside the Window’s view. Using the grid guides, scale the table to use most of the window’s real estate.

At this point, make sure the AroundMe scheme is selected and click Run. The app should look like Figure 10.1, “Blank Table View”.

Figure 10.1. Blank Table View

Blank Table View

We are going to use the Gowalla API to find the locations (called spots in Gowalla’s jargon) around our users. You can see the type of information the API provides us by going to the following page: http://gowalla.com/api/explorer#/spots?lat=30.2697&lng=-97.7494&radius=50.

We can’t easily display all the spot’s information. Instead, we are going to filter the data and display only the following:

  • The spot’s category

  • The spot’s name

  • The spot’s description

  • The spot’s distance

  • The number of items in the spot

  • The number of users who visited this spot

  • The number of checkins in the spot

Let’s add some columns to our table. The easiest way to do that is to copy/paste an existing table column. The end result should look like Figure 10.2, “Seven table columns added”.

Figure 10.2. Seven table columns added

Seven table columns added

Now, let’s name each column’s header by setting the header cell title via the Attributes Inspector (Figure 10.3, “Setting the column’s header title”).

Figure 10.3. Setting the column’s header title

Setting the column’s header title

Our first column will display an image, so we need to change the type of cell it uses. Drag and drop an Image Cell inside the first column so the Text Field cell gets replaced by its Image counterpart (Figure 10.4, “Image Cell being used in the first column”).

Figure 10.4. Image Cell being used in the first column

Image Cell being used in the first column

Finally, one by one, edit the table columns and use the Attributes Inspector to make sure the columns are not editable.

Note

You can select all the columns at once and set the flag the way you want it.

That’s enough for the UI for now. Let’s move on to writing some code.

Table View

Before we get to call the web API and fetch the information, we need to wire the table view. We are going to call our table spot_table and create an outlet for it in our AppDelegate.rb file:

class AppDelegate
  attr_accessor :window
  attr_accessor :spot_table

  ...
end

Going back to the UI editor, right-click the App Delegate object, choose the spot_table outlet, and connect it to the table view.

Warning

Connect the outlet to the table view, not the scroll view. The table view is inside the scroll view.

We can now initialize the table view delegation as follows:

def applicationDidFinishLaunching(a_notification)
  spot_table.dataSource = self
  spot_table.doubleAction = "preview:"
end

Setting the TableView instance’s dataSource to self means that we are expected to implement the table’s methods in our AppDelegate class (which is what self refers to in this context). We are also setting the doubleAction selector, representing the action to dispatch when a row is double-clicked. Of course, that means that we will need to implement this action in a minute.

For the table to work properly, we need to implement a few methods. But before we get there, we need to define an object that will hold the references to spots. Our table will use this object to fetch and sort its content. Let’s call this object @spots, make an attribute accessor for it, and initialize it as an empty array when the application finishes loading. We are going to use this object as the table data source. For more information about how to use a table data source, please read the Apple developer page, About Table Views in Mac OS X Applications.

This is how our AppDelegate class should look:

class AppDelegate
  attr_accessor :window
  attr_accessor :spots, :spot_table

  def applicationDidFinishLaunching(a_notification)
    @spots = []
    spot_table.dataSource = self
    spot_table.doubleAction = "preview:"
  end

  def windowWillClose(sender); exit; end

end

Note

I skipped the windowWillClose delegate explanation here because I covered it many times previously. Remember that you need to set the window’s delegate to the AppDelegate class.

Now that we defined our data source object, we can define some table delegate methods based on this reference object. One of these methods indicates to the table view the number of records in the source:

def numberOfRowsInTableView(view)
  spots.size
end

We also need to implement some sort of overloaded method. In Objective-C, a single method name can be defined with different method signatures. MacRuby supports the same approach when a hash is used in the method signature. We are going to implement the following methods:

def tableView(view, objectValueForTableColumn:column, row:index)
end

def tableView(view, sortDescriptorsDidChange:descriptors)
end

These are two alternative signatures defined by the TableView API. The first method retrieves the value for a given column, whereas the second defines sorting in the table. In both, the second argument is a hash object (Ruby 1.9 style), but the keys are different between the two method signatures.

When the first method is dispatched, it is passed the column object and row index to retrieve the data to display. Now we have two issues: we don’t have any data to look at yet, and we don’t have a way to look up a record’s value with a column instance as a key. To make things easier, we are going to edit our columns and set their editors with the matching JSON key we are getting from the web API call. Once we implement the web API fetching, we will wrap each spot in an object that will respond to the key names. We can also directly look up the values in the JSON object, but that won’t allow us to customize and massage the data easily, since the JSON data structure is somewhat limited.

In the UI editor, select each column and set the identifier as follows:

  • Category image

  • Name name

  • Description description

  • Distance radius_meters

  • Items items_count

  • Users users

  • Checkins checkins

The implementation of the first method is as follows:

def tableView(view, objectValueForTableColumn:column, row:index)
  spot = spots[index]
  id = column.identifier
  if id == 'image'
    # NSImageCell instance, we need to feed it an NSImage
    @cell_images ||= {}
    @cell_images[spot.image] ||= NSImage.alloc.initWithData(
       NSURL.URLWithString(spot.image).resourceDataUsingCache(true)
      )
  else
    spot.send(id.to_sym)
  end
end

The first thing we are doing in this method is fetching the spot based on its index in the data source. We then extract the column identifier and check whether it matches the image string. If it does, we allocate a new NSImage instance based on the image URL that was fetched from the web API. To improve performance, we are memoizing all the NSImage instances we create and making sure to use the built-in cache. Finally, for all other columns, we are dispatching the identifier’s method using Ruby’s #send method, which takes, as its argument, the method’s name as a symbol (#to_sym converts a string into a symbol).

We’ll keep the second method very simple for now. Instead of doing anything here, we will just reload the data for the moment:

def tableView(view, sortDescriptorsDidChange:descriptors)
  spot_table.reloadData
end

Core Location

Before we can call Gowalla’s API, we still need to get the user’s location. For that, we are going to use CoreLocation, a framework available in OS X that allows the user to share his geographical location with a given program.

The CLLocationManager class, available from CoreLocation (see http://developer.apple.com/library/mac/#documentation/CoreLocation/Reference/CoreLocation_Framework/_index.html) framework, does all the work, but it’s a bit clumsy. So let’s write a thin wrapper to keep our delegate class simpler. Add a new Ruby file called location_manager.rb and paste the following code in the file:

framework 'CoreLocation'
# CLLocationManager wrapper
class LocationManager

  def initialize(&block)
    @loc          = CLLocationManager.alloc.init
    @loc.delegate = self
    @callback     = block
  end

  def start
    @loc.startUpdatingLocation
  end

  def stop
    @loc.stopUpdatingLocation
  end

  # Dispatch the CLLocationManager callback to the Ruby callback
  def locationManager(manager, didUpdateToLocation: new_location,
                               fromLocation: old_location)
    @callback.call(new_location, self)
  end

end

This wrapper doesn’t do much, but it will make our code cleaner. We basically define a new class called LocationManager, which expects a block at initiation. The block is kept in memory and is called when new location information is retrieved. I also added two simple instance methods, #start and #stop, which delegate to the underlying CLLocationManager instance. There is documentation for CoreLocation at the Apple developer site.

This nice wrapper allows us to print a location like this:

location_manager = LocationManager.new do |new_location, manager|
  puts "location: #{new_location.description}"
  manager.stop
end
location_manager.start

As a matter of fact, you can put the wrapper and the previous code in a file and get your location that way. You will need to add the following line at the bottom of the file to keep the run loop running:

NSRunLoop.currentRunLoop.runUntilDate(NSDate.distantFuture)

Execute the file using MacRuby, and you should be presented with a dialog box like the one shown in Figure 10.5, “The location-sharing security dialog box”.

Figure 10.5. The location-sharing security dialog box

The location-sharing security dialog box

And something like this should be printed out on your terminal:

location: <+32.67000000, 117.24000000> +/- 181.00m
              (speed -1.00 mps / course -1.00) @ 2011-05-15 22:46:24 -0700

Back to our app. Let’s use our wrapper in our applicationDidFinishLaunching callback method:

def applicationDidFinishLaunching(notification)
  @spots = []
  location_manager = LocationManager.new do |new_location, manager|
    puts "location: #{new_location.description}"
    gowalla = gowalla_req(new_location)
    @spots  = gowalla['spots'].map{|spot| GowallaSpot.new(spot)}
    @spots.sort!{|a,b| a.radius_meters <=> b.radius_meters}
    spot_table.reloadData
    manager.stop
  end

  spot_table.dataSource = self
  spot_table.doubleAction = "preview:"
  location_manager.start
end

Within the block, we are making a Gowalla call, iterating through the spots, and storing them in an array of GowallaSpot instances. The spots are then sorted based on their distances. The table view reloads its data and the location manager is stopped.

The location manager obviously cannot be started from within the callback block, so we need to start it once everything is set up. That’s what we do before closing the method definition.

Web API

Right now, this code won’t work. We still need to implement the GowallaSpot class and the #gowalla_req method. I personally like to write high-level code before I implement the underlying methods; I feel it helps me design my APIs better.

The GowallaSpot class is very straightforward. Create a new file with the following code:

class GowallaSpot

  attr_reader :name
  attr_reader :url
  attr_reader :description
  attr_reader :highlights_url
  attr_reader :lat, :lgn
  attr_reader :image
  attr_reader :items_count
  attr_reader :radius_meters
  attr_reader :users, :checkins

  def initialize(spot_hash)
    @name = spot_hash['name']
    @url = NSURL.URLWithString(
                  "http://gowalla.com#{spot_hash['url']}")
    @description = spot_hash['description']
    @highlights_url = 'http://gowalla.com'  \
      + spot_hash['highlights_url']
    @lat = spot_hash['lat']
    @lgn = spot_hash['lgn']
    @image = spot_hash['_image_url_50']
    @items_count = spot_hash['items_count']
    @radius_meters = spot_hash['radius_meters']
    @users = spot_hash['users_count']
    @checkins = spot_hash['checkins_count']
  end

end

The #gowalla_req method is defined inside the AppDelegate class like this:

def gowalla_req(location)
  http = Net::HTTP.start('api.gowalla.com')
  req = Net::HTTP::Get.new("/spots?lat=#{location.coordinate.latitude}&" + \
                           "lng=#{location.coordinate.longitude}&radius=500")
  req.add_field("Accept", "application/json")
  req.add_field("X-Gowalla-API-Key", GOWALLA_API_KEY)
  response = http.request(req)
  JSON.parse(response.body)
end

This method uses Ruby’s net/http library to make a call to the Gowalla web API and the built-in JSON library. To use these libraries, we need to require them at the top of our file using require "net/http" and require "json". You need to get your own API key from Gowalla and store it in the GOWALLA_API_KEY constant.

If you run the app, everything should load properly and you will see the various spots around you. It’s nice, but we are missing one last thing: the preview action. Remember that earlier we set a selector to trigger when a user double-clicks a row. Let’s implement this preview action:

def preview(sender)
  if spot_table.selectedRow >= 0
    url = spots[spot_table.selectedRow].url
    NSWorkspace.sharedWorkspace.openURL(url)
  end
end

NSWorkspace.sharedWorkspace.openURL (see http://developer.apple.com/library/mac/#documentation/Cocoa/Reference/ApplicationKit/Classes/NSWorkspace_Class/Reference/Reference.html) opens a given URL in the user’s default browser. We made sure that when this method is called, the table has a valid selected row and we retrieve the URL by accessing the spots data source using the row index.

If you run the application now, you should be able to view all the Gowalla spots around you and visit their web pages by double-clicking a row (Figure 10.6, “The end result”).

Figure 10.6. The end result

The end result

Site last updated on: November 9, 2011 at 10:00:57 AM PST
Cover for MacRuby: The Definitive Guide