Yahoo Maps-based Geocoding

I recently had to create online maps for an art gallery, specifically for their exhibits. So I decided to use the Yahoo API to grab the coordinates based upon the data that was being sent to the model and save this to the database. Here's how I did it.

First, the model has to be aware of the Yahoo AppID. I set this as a variable within the model itself, but you can set it in the url.

<?php
class Exhibit extends AppModel {

    var $name = 'Exhibit';
    var $yahooApiID = 'YOURAPPIDHERE';
}
?>

Next we should probably actually get the Latitude and Longitude for the data. I am assuming you have the correct fields defined in your exhibits table. The CREATE TABLE query is shown below

CREATE TABLE `exhibits` (
  `id` int(11) unsigned NOT NULL auto_increment
  `address` varchar(50) default NULL,
  `city` varchar(20) default NULL,
  `state_id` int(3) unsigned default NULL,
  `zipcode` int(5) default NULL,
  `lattitude` varchar(20) default NULL,
  `longitude` varchar(20) default NULL
  PRIMARY KEY  (`id`),
) ENGINE=InnoDB DEFAULT CHARSET=utf8

Now that you have the proper fields, we'll REALLY get the Latitude and Longitude. Below is the entire Model class relating to Exhibits:

<?php
class Exhibit extends AppModel {

    var $name = 'Exhibit';

    function beforeValidate(){
        $curl = false;

        // Start constructing the URL
        $q = 'http://local.yahooapis.com/MapsService/V1/geocode?appid=';
        // Adding apiid
        $q .= $this->yahooApiID;
        // Adding Street
        $q .= '&street='.$this->data['Exhibit']['address'];
        // Adding City
        $q .= '&city='.$this->data['Exhibit']['city'];
        // Adding State
        //ClassRegistry::init('State')->getState($this->data['Exhibit']['state_id'])
        $q .= '&state='.ClassRegistry::init('State')->getState($this->data['Exhibit']['state_id']);
        // Adding Zip
        $q .= '&zip='.$this->data['Exhibit']['zipcode'];
        // If We had a location, we would do the following instead:
        // $q .= '&location='.rawurlencode($this->data['Exhibit']['location']);
        // Setting Output to xml
        $q .= '&output=php';
        $q = str_replace(' ', '%20', $q);

        //Using cURL or file_get_contents
        if ($curl){
            $c = curl_init();
            curl_setopt($c, CURLOPT_URL, $q);
            curl_setopt($c, CURLOPT_RETURNTRANSFER, 1);
            $phpContent = trim(curl_exec($c));
            curl_close($c);
        } else {
            $phpContent = file_get_contents($q);
        }
        $phpContent = unserialize($phpContent);

        $this->data['Exhibit']['lattitude'] = $phpContent['ResultSet']['Result']['Latitude'];
        $this->data['Exhibit']['longitude'] = $phpContent['ResultSet']['Result']['Longitude'];
    }
?>

I have allowed you to use either cURL or fopen. For cURL, you need to have that installed on your server, and for fopen, you need to have allow_url_fopen set to true in php.ini. Just giving options in case you can't do one or the other :)

You'll notice that when I get the State for this location, I initialize the model via ClassRegistry. Just out of habit. I also don't have a relationship to the State in my model, which is why I did this. Read about Containable (and BindModel for extra points!) for more information. The code for that call in the State model is as follows:

<?php
class State extends AppModel{
    var $name = 'State';
    function getState($state = null){
        $state = $this->find('first',
            array(
                'recursive' => -1,
                'fields' => array(
                    'State.title'
                ),
                'conditions' => array('State.id' => $state)
            )
        );
        return $state['State']['title'];
    }
}
?>

So lets review:

  • I set my AppID somehow
  • Made sure my model contained the proper fields
  • Added some code to the beforeValidate
  • Updated two fields (latitude and longitude) in my model
  • Made getting the State (depending upon state_id) a bit easier

You can now use the Latitude and Longitude in the views just as any other model data.

I realize that you might be thinking "This should be a behavior or something, I'll end up replicating this several times!" Well good. You make the behavior and I'll take credit! In all seriousness though, there is already a Geocode Model on the bakery that should be made into a behavior. This was just a quick fix before I converted it into a Behavior for my own usage :)