Geocoding in CakePHP

by nate
A couple of weeks ago, I was building an event management system which required that people be able to search events near them. I found several existing solutions, but none that I could integrate as easily as a proper Cake extension.

Introduction


The Geocoded Behavior for CakePHP is the simplest and most powerful way to integrate geocoding into your CakePHP application. It integrates with both Google Maps and Yahoo! Local geocoding services (and is extensible to allow for the inclusion of other services), it automatically caches geocode data, and best of all, it can be implemented in just 3 lines of code.

Downloading & Setup


The Geocoded Behavior is featured in the initial release of the Mashup API Project, which is a new repository for CakePHP components, behaviors and helpers which integrate web service APIs. You can download the latest release of the Mashup API Project here: https://cakeforge.org/frs/?group_id=161
Once you have downloaded and unzipped the release, copy geocoded.php from the /models/behaviors folder into your application's /models/behaviors folder. Then, import /config/sql/geocodes.php into your application's database. Finally, you need an Application ID (Yahoo! Local) or an API key (Google Maps) in order to integrate the service of your choice. You can get your keys here:
  1. [li]Google Maps API Key - http://www.google.com/apis/maps/signup.html [li]Yahoo! Local App ID - http://search.yahooapis.com/webservices/register_application

A Simple Example


Let's start by verifying that our service works, and we're able to connect to it using our key. We'll create a simple model called Location, using the following SQL:

SQL:


CREATE TABLE `locations` (
  `id` int(11) unsigned NOT NULL auto_increment,
  `name` varchar(255) default NULL,
  `addr1` varchar(255) default NULL,
  `addr2` varchar(255) default NULL,
  `city` varchar(255) default NULL,
  `state` varchar(255) default NULL,
  `zip` varchar(5) NOT NULL default '',
  `lat` float default NULL,
  `lon` float default NULL,
  `created` datetime default NULL,
  `modified` datetime default NULL,
  PRIMARY KEY  (`id`)
);

Then we'll create our model, designating that we want to enable it for geocoding:

Model Class:

<?php 

class Location extends AppModel {

    var 
$name 'Location';

    var 
$actsAs = array('Geocoded' => array(
        
'key' => 'ABQIAAAAn0kmVahg_WhO0jCT8Z8MkBT2yXp_ZAY8_u....'
    
));
}

?>

Replace the value of 'key' with your Google Map API key or Yahoo! App ID (the default API key for the localhost domain is ABQIAAAAn0kmVahg_WhO0jCT8Z8MkBT2yXp_ZAY8_ufC3CFXhHIE1NvwkxS-Zl837z60cpTjKeSeelhEJVmNOQ).

Then, create a controller that uses the Location model, and add the following to an action:

Controller Class:

<?php 
    pr
($this->Location->geocode('1600 Pennsylvania Ave. Washington DC USA'));
?>

You should then see an array similar to the following:


Array
(
    [lat] => 38.898758
    [lon] => -77.037691
)

If DEBUG is set to 2 or higher, you should also see the following queries:

SQL:


    SELECT `Geocode`.`address`, `Geocode`.`lon`, `Geocode`.`lat` FROM `geocodes` AS `Geocode` WHERE `Geocode`.`address` = '1600 pennsylvania ave. washington dc usa' LIMIT 1
    INSERT INTO `geocodes` (`address`,`lat`,`lon`) VALUES ('1600 pennsylvania ave. washington dc usa', 38.898758,-77.037691)

Before querying the web service, the Geocoded behavior checks the cache to see if a lookup on this address has already been performed. Then, after successfully retrieving the results from the web service, it saves those results to the cache table. From now on, any lookups for that address will be read from the cache.

Getting Fancy


Besides strings, the geocode() method will also accept arrays, from which it will attempt to extract address information. This makes it extremely easy to add geo-data to models which include address data. The list of extractable fields is as follows:

'street', 'address', 'addr', 'address1', 'addr1', 'address2', 'address2', 'apt', 'city', 'state', 'zip', 'zipcode', 'zip_code'

Using beforeSave(), we can automatically save geocoded coordinates to our Location model every time a record is created or updated:

Model Class:

<?php 

class Location extends AppModel {

    var 
$name 'Location';

    var 
$actsAs = array('Geocoded' => array(
        
'key' => 'ABQIAAAAn0kmVahg_WhO0jCT8Z8MkBT2yXp_ZAY8_u....'
    
));

    function 
beforeSave() {
        if (
$coords $this->geocode($this->data)) {
            
$this->set($coords);
        }
        return 
true;
    }
}

?>

This will save any valid coordinate set, based on the data provided. The Location model uses the field combination of 'addr1, 'addr2', 'city', 'state', 'zip' to create the address string. Some other valid combinations are:
  1. [li]street, city, state [li]address, city, state, zip [li]addr, apt, city, zipcode [li]city, zip_code [li]zipcode [li]And so on.

Any other valid combination of fields from the list will work just fine. You can also customize the field names in the Location model which are used to store the coordinate data, if, for example you wanted to use the field names 'latitude' and 'longitude'.

The full list of configuration options for the Geocoded behavior is as follows:
  • [li]lookup - The name of the lookup service to use. Currently available options are 'google' and 'yahoo'. Defaults to 'google'. [li]key - The Google Maps API key or Yahoo! Local App ID for your application [li]cacheTable - The name of the table to use when caching geocode data. Defaults to 'geocodes'. Alternatively, you can create a Geocode model which will be used for all saves and lookups, in which case this setting will be ignored. [li]fields - An array containing the field names to use for latitude and longitude data. These should match the field names of your geocoded model. Defaults to array('lat', 'lon').

Searching


After creating a few locations, we can search for ones in our area. The Geocoded Behavior includes a method called findAllByDistance(), which allows you to search for records within a given distance of a certain point. We can first get the coordinates of our search location, then do the search. The findAllByDistance() method can be called in one of two ways:


findAllByDistance($coords, $distance);
- or -
findAllByDistance($x, $y, $distance);

In the first example, $coords is an array containing longitude and latitude values (in that order). In the second example, $x and $y are longitude and latitude values, respectively. In both examples, $distance is the search radius in miles.

Putting this into practice, we can do something like the following:

Controller Class:

<?php 
    $youAreHere 
$this->Location->geocode("132 Tremont St. Boston, MA");
    
$locations $this->Location->findAllByDistance($youAreHere5);
?>

This will find all the Location records within 5 miles of me. Alternatively, you could create a form based on the address fields in the Location model, and run your searches dynamically:

Controller Class:

<?php 
    
if (!empty($this->data)) {
        
$youAreHere $this->Location->geocode($this->data);
        
$locations $this->Location->findAllByDistance($youAreHere5);
    }
?>

Adding location searching in CakePHP is now as simple as that.

Future versions of the Geocoded Behavior will generate a virtual 'distance' field within your query, allowing you to do sorting and more advanced filtering and comparison. Other future plans include setting a default measurement unit, with automatic unit converstions, as well as setting default array keys from which to generate addresses, as well as methods for facilitating the integration of other geocoding APIs.

Stay tuned for more fun web APIs, and check out the official CakePHP Mashup API Project at https://cakeforge.org/projects/mashup/, where more code examples and API integrations will be appearing shortly.

Report

More on Tutorials

Advertising

Comments

  • linkingarts posted on 03/21/11 05:44:33 AM
    line 152 in geocoded.php should read:

    return $model->find('all', array('conditions' => "(3958 * 3.1415926 * SQRT(({$y2} - {$y}) * ({$y2} - {$y}) + COS({$y2} / 57.29578) * COS({$y} / 57.29578) * ({$x2} - {$x}) * ({$x2} - {$x})) / 180) <= {$distance}");
  • tasin posted on 01/21/11 04:54:28 AM
    Thanks a lot for this.

    I had to make few tweaks to use it correctly.

    One last thing I am having problem with. I am getting this error: Undefined index: id [CORE\cake\libs\model\model.php, line 1329]
    Not sure how to fix this.

    Any help would be much appreciated.
  • hernanc posted on 03/09/10 06:43:50 PM
  • gmansilla posted on 02/21/10 10:31:25 PM
    I want to know exactly where my visitors come from
  • redabbey posted on 12/06/09 07:45:49 AM
    I am working on a project and i am wanting to use this in a search engine, there will be a keyword field, location/zip code and a radius field.
    I need the search function to work something like this:

    $this->set('jobs', $this->Job->findAllByDistance($this->data['keywords'], $this->data['location'], $this->data['radius']));

    So that it will find all the entries with the defiened keywords, and within the defined distance from the users chosen location.

    I am guessing i have to change something in line 152 in geocoded.php:

    return $model->findAll("(3958 * 3.1415926 * SQRT(({$y2} - {$y}) * ({$y2} - {$y}) + COS({$y2} / 57.29578) * COS({$y} / 57.29578) * ({$x2} - {$x}) * ({$x2} - {$x})) / 180) <= {$distance}");

    Thanks
  • mariano posted on 08/24/09 02:44:48 PM
    Those of you interested in a Geocode plugin may want to check out this work in progress:

    http://groups.google.com/group/cake-php/t/7fc055b246a37f11
  • matousek posted on 08/19/09 03:26:11 PM
    I was trying to call the distance function from my controller (Cake 1.2), e.g. $this->Location->distance($lat1, $lon1, $lat2, $lon2).

    However I kept getting the error: Object of class Location could not be converted to double.

    The distance function is missing the first argument, which is the name of the model. The method signature should be:

    function distance(&$model, $lat1, $lon1, $lat2 = null, $lon2 = null, $unit = 'M')





  • salentinux posted on 04/29/09 07:12:58 AM
    Someone more experienced in math could post how to replace the formula to calculate distances of addresses in kilometers?
    tnks all.
  • deftonez4me posted on 03/04/09 05:11:50 PM
    I am using the findAllByDistance() function in one of my projects, but i am having trouble fuguring out how to sort the results by distance, so i can display them closest to furthest. I have looked around forums for the answer, but having been unable to find a solution. Can someone please help with sorting these function results?
    • deftonez4me posted on 03/05/09 12:53:22 PM
      I am using the findAllByDistance() function in one of my projects, but i am having trouble fuguring out how to sort the results by distance, so i can display them closest to furthest. I have looked around forums for the answer, but having been unable to find a solution. Can someone please help with sorting these function results?
      Searching for this answer i came across multiple people who had the same problem i had, so here is the ridiculously easy answer...

      Modify the findAll function in the findAllByDistance() function to order by the distance being calculated...

      return $model->findAll("(3958 * 3.1415926 * SQRT(({$y2} - {$y}) * ({$y2} - {$y}) + COS({$y2} / 57.29578) * COS({$y} / 57.29578) * ({$x2} - {$x}) * ({$x2} - {$x})) / 180)<= {$distance}", null, "(3958 * 3.1415926 * SQRT(({$y2} - {$y}) * ({$y2} - {$y}) + COS({$y2} / 57.29578) * COS({$y} / 57.29578) * ({$x2} - {$x}) * ({$x2} - {$x})) / 180)", 10);



  • cguyer posted on 01/06/09 07:37:48 AM
    We be cool to see some more development on this or at least bring it up to the latest cake revisions.
  • tomchance posted on 12/27/08 11:07:12 AM
    I don't understand this at all - I'm getting the wrong coordinates for data I put in!

    I've integrated this code into an existing web site, and I'm getting the coordinates with a UK post code like so:

    $this->set( 'lonlat', $this->Location->geocode(array('zip' => $user['User']['postcode'])));

    Put a post code into Google Maps and you get the right location, but put it through this and it comes out slightly south and ever so slightly to the East. For example:

    Postcode: SE22 9QE
    Geocoded loc: -0.0673365, 51.4493
    Correct loc: -0.068, 51.45996

    I get this for a range of other UK post codes that I've tried as well. Any thoughts?
  • ylat posted on 07/31/08 04:26:11 PM
    I have tried to post my previous question as "Geocoding and Pagination?" which caused an error saying "This field cannot be left emtpy" or something like it. The question mark seems to have caused this error you may have a look at it and delete this post.
  • ylat posted on 07/31/08 04:23:51 PM
    I am fairly new to CakePHP and wonder if there is a way to make pagination work with geocoding. Atm I do not feel comfortable enough to hack behaviours or to write my own ones. I would be glad in case someone has an idea.

    I do not forget to thank for this great work.
  • amfriedman posted on 06/12/08 12:14:45 AM
    I was having problems getting an array with two address fields (I labeled them 'addr1' and 'addr2') to geocode accurately.

    Then I noticed (I can't believe I spotted this in passing) a duplicate key in the array on line 77 in geocoded.php:


    $vars = array('street', 'address', 'addr', 'address1', 'addr1', 'address2', 'address2', 'apt', 'city', 'state', 'zip', 'zipcode', 'zip_code');

    See it? Replace 'address2' with 'addr2'.

  • yuri41 posted on 01/06/08 09:52:25 AM
    If you plan to use this in Cake 1.2 Beta you have to replace on line 44 the "loadModel('Geocode')" with "App::import('Geocode')"

    have Fun
    Christoph Hochstrasser
  • yuri41 posted on 01/06/08 09:20:00 AM
    I'm using the most recent CakePHP 1.2 Beta and I'm getting this error:
    Warning (512): loadModel is deprecated see App::import('Model', 'ModelName'); [CORE/cake/basics.php, line 1035]
  • sdc53 posted on 06/22/07 04:50:23 AM
    The bugfix:
    I added the following to geocoded.php:
    function geocode(&$model, $address) {
    //go all the way to the end
    //added check for failed code lookup
    if ($code) {
    return array_reverse($code);
    } else {
    return false;
    }
    }

    it was possible for code to be null if the lookup in the cache passed, but the http lookup failed.

    I'm having a problem with lookups to yahoo failing inside:
    HttpSocket::decodeChunkedBody - Could not parse malformed chunk. Activate quirks mode to do this. [CORE/cake/libs/http_socket.php, line 399] Context:
    $this = httpsocket object
    $body = "1e9

    44.94141-123.034286
    SALEMORUS


    0

    "
    $decodedBody = null
    $chunkLength = null
    $match = array()

    Code:
    while ($chunkLength !== 0) {
    if (!preg_match("/^([0-9a-f]+)(?:;(.+)=(.+))?\r\n/iU",
    $body, $match)) {
    if (!$this->quirksMode) {
    trigger_error(__('HttpSocket::decodeChunkedBody - Could not parse malformed chunk. Activate quirks mode to do this.', true), E_USER_WARNING);
    return false;
    }
    break;
    }
    // and so on..
    }

    so it's triggering this error because something doesn't pass the preg_match in the results from yahoo. google works fine with the same code. I tried overriding quirksMode to no avail. what to do?
  • phpnewbe posted on 04/24/07 05:21:55 PM
    Thanks for the tutorial. I was wondering how can I use the data to actually embed a map in my webpage with a pointer at the address.
    • nate posted on 06/05/07 02:29:42 PM
      Thanks for the tutorial. I was wondering how can I use the data to actually embed a map in my webpage with a pointer at the address.
      That's beyond the scope of this particular tutorial, although a mapping helper will probably be provided in subsequent releases of The Mashup API Project. In the meantime, you can find information on implementing with Google Maps here: http://www.google.com/apis/maps/documentation/
  • karmer posted on 04/12/07 03:22:19 AM
    Sorry Nate; I missed the fact that I was meant to be using the 1.2 release of CakePHP. I was using the current stable release - 1.1.14.4797.

  • nate posted on 04/11/07 08:51:08 PM
    It looks like you're either not attaching the behavior to the model correctly, or you're not running Cake 1.2. If it's still not working for you, try pasting some code into CakeBin and posting a link.
  • karmer posted on 04/11/07 07:36:00 PM
    Hi Nate,

    Thanks for the tutorial. I got as far as the simple example and got an error (see below)! It doesnt appear to be creating a full SQL string from what i can tell. Any ideas?

    Query: geocode
    Warning: SQL Error: 1064: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'geocode' at line 1 in /home/mashupk/public_html/cake/libs/model/datasources/dbo_source.php on line 476
login to post a comment.