GeoCool!

Web 2.0 and the programmable web that I and others have been talking about for a while has mostly been vapourware so far. There are a few generic components that are useful, but it is somewhat limited what you can do with them. And yes, you may consider this a somewhat biased view, but I think Yahoo!'s new geocoding platform is a huge step in the right direction.

There is of course the fancy new maps.yahoo.com/beta site which is fun, but as far as I am concerned the killer app here is the geocoding platform that drives this. And it is completely accessible for anyone to use. It's also a sane API that anybody can figure out in minutes. Here are a few tips for using this API from PHP 5.

Step 0 - The raw geocoding API

Whenever I do anything with web services, I always add a request caching layer. So here are the base building blocks implemented in 2 functions. One for doing request caching and the second to do the actual REST query to the geocoding service.

<?php
function request_cache($url, $dest_file, $timeout=43200) {
  if(!file_exists($dest_file) || filemtime($dest_file) < (time()-$timeout)) {
    $stream = fopen($url,'r');
    $tmpf = tempnam('/tmp','YWS');
    file_put_contents($tmpf, $stream);
    fclose($stream);
    rename($tmpf, $dest_file);
  }
}

function yahoo_geo($location) {
  $q = 'http://api.local.yahoo.com/MapsService/V1/geocode';
  $q .= '?appid=rlerdorf&location='.rawurlencode($location);
  $tmp = '/tmp/yws_geo_'.md5($q);
  request_cache($q, $tmp, 43200);
  libxml_use_internal_errors(true);
  $xml = simplexml_load_file($tmp); 
  $ret['precision'] = (string)$xml->Result['precision'];
  foreach($xml->Result->children() as $key=>$val) {
    if(strlen($val)) $ret[(string)$key] =  (string)$val;
  } 
  return $ret;
}
?>

The above code is the contents of geo.inc which you will see included in the following examples.

Easy enough? No real tricks here. We simply send a regular GET request to http://api.local.yahoo.com/MapsService/V1/geocode with the location parameter set to an address. You can try it yourself directly from your browser by clicking here:

http://api.local.yahoo.com/MapsService/V1/geocode?appid=rlerdorf&location=701%20First%20Ave,%2094089

You can read more about the geocoding service here:

http://developer.yahoo.net/maps/rest/V1/geocode.html

Step 1 - Writing your first application

We can just toss a form around this and dump the results to make sure things are working.

<html>
<head>
<title>GeoCoding API Example</title>
</head>
<body>
<form action="/php/ymap/geo1.php" method="GET">
<input type="text" size="80" name="location" />
</form>
<?php
include './geo.inc';
if(!empty($_REQUEST['location'])) {
  $a = yahoo_geo($_REQUEST['location']);
  echo "<pre>"; print_r($a); echo "</pre>";
}
?>
</body></html>

You can see this one in action here:

http://lerdorf.com/php/ymap/geo1.php

Note how it is able to fill in missing details for a partial address. eg.

http://lerdorf.com/php/ymap/geo1.php?location=701+First+Avenue+94089

results in:

    [precision] => address
    [Latitude] => 37.416384
    [Longitude] => -122.024853
    [Address] => 701 FIRST AVE
    [City] => SUNNYVALE
    [State] => CA
    [Zip] => 94089-1019
    [Country] => US

This means that you can use it for a bunch of different things. Address to lat/long, of course, but also address to city, or city to zip code conversions. Or 5-digit zip to 5+4. This is of course rather US-centric right now, but that will improve over time.

Step 2 - Adding a map

The geocoding is cool, but an actual map is cooler. Easy enough:

<html><head>
<script type="text/javascript" src="http://api.maps.yahoo.com/v2.0/fl/javascript/apiloader.js"></script>
<style type="text/css">
#mapContainer { 
height: 600px; 
width: 800px; 
} 
</style> 
<title>GeoCoding API Example</title>
</head><body>
<form action="/php/ymap/geo2.php" method="GET">
<input type="text" size="80" name="location" />
</form>
<?php
include './geo.inc';
if(!empty($_REQUEST['location'])) {
  $a = yahoo_geo($_REQUEST['location']);
  echo "[ {$a['Latitude']}, {$a['Longitude']} ] {$a['precision']}-level coordinate accuracy<br />\n";
  if(!empty($a['Address'])) echo $a['Address'].', ';
  if(!empty($a['City'])) echo $a['City'].', ';
  if(!empty($a['State'])) echo $a['State'].' ';
  if(!empty($a['Zip'])) echo $a['Zip'].' ';
  if(!empty($a['Country'])) echo $a['Country'].' ';
}
?>
<div id="mapContainer"></div>
<script type="text/javascript">
var latlon = new LatLon(<?php echo $a['Latitude']?>, <?php echo $a['Longitude']?>);
var map = new Map("mapContainer", "rlerdorf", latlon, 3);
map.addTool( new PanTool(), true );
</script>

We are using the Flash-Javascript API here. Try it out!

http://lerdorf.com/php/ymap/geo2.php?location=701+First+Avenue%2C+94089

And yes, of course the map is draggable. The PanTool() part of the above script adds the panning feature.

This is an API that lets you embed a Flash-based map, but control it with Javascript. It's quite cool even if you think Flash sucks. It is described at:

http://developer.yahoo.net/maps/flash/jsGettingStarted.html

and the AJAX-DHTML API is described at:

http://developer.yahoo.net/maps/ajax/index.html

Step 3 - Making the map prettier

The map looks a bit bare. We don't see our address marker, for example. So let's add that.

<html><head>
<script type="text/javascript" src="http://api.maps.yahoo.com/v2.0/fl/javascript/apiloader.js"></script>
<style type="text/css">
#mapContainer { 
height: 600px; 
width: 800px; 
} 
</style> 
<title>GeoCoding API Example</title>
</head><body>
<form action="/php/ymap/geo3.php" method="GET">
<input type="text" size="80" name="location" />
</form>
<?php
include './geo.inc';
if(!empty($_REQUEST['location'])) {
  $a = yahoo_geo($_REQUEST['location']);
  echo "[ {$a['Latitude']}, {$a['Longitude']} ] {$a['precision']}-level coordinate accuracy<br />\n";
  $mtitle = ''; 
  if(!empty($a['Address'])) { 
    echo $a['Address'].', '; 
    $mtitle = $a['Address'];
  }
  if(!empty($a['City'])) {
    echo $a['City'].', ';
    if(!$mtitle) $mtitle = $a['City'];
  }
  if(!empty($a['State'])) echo $a['State'].' ';
  if(!empty($a['Zip'])) echo $a['Zip'].' ';
  if(!empty($a['Country'])) echo $a['Country'].' ';
  $info = str_replace("\n",'',nl2br(print_r($a,true)));
}
?>
<div id="mapContainer"></div>
<script type="text/javascript">
var latlon = new LatLon(<?php echo $a['Latitude']?>, <?php echo $a['Longitude']?>);
var mymap = new Map("mapContainer", "rlerdorf", latlon, 3);
mymap.addTool( new PanTool(), true );
marker1 = new CustomPOIMarker('A','<?php echo $mtitle?>', '<?php echo $info?>', '0x0012f0', '0x88CCFF'); 
mymap.addMarkerByLatLon(marker1, latlon);
</script>
</body></html>

There is a lot of stuff there, but all I really changed was a bit of code related to picking information out of the address so I can fill in the expanded marker, and then the marker code. The last 2 lines of the Javascript there that creates a new CustomPOIMarker and then uses addMarkerByLatLon to the map does the trick. When you mouse over it, it will expand to show the title ($mtitle) and when you click on it, it will show the contents of $info.

Step 4 - But but, why Flash?

Aside from portability and less DHTML browser quirks, it gives us widgets! We can add the Navigation widget very easily. 2 lines of Javascript:

  navWidget = new NavigatorWidget(); 
  mymap.addWidget(navWidget); 

That's all. Have a look at it now:

http://lerdorf.com/php/ymap/geo4.php?location=701+First+Avenue%2C+94089

But if the thought of Flash still makes your skin crawl. No worries. You can get pretty close to the Flash version with straight DHTML. Here is the geo4 demo using the DHTML-AJAX API:

http://lerdorf.com/php/ymap/dgeo4.php?location=701+First+Avenue%2C+94089

Step 5 - Something real and useful

By using nothing more than what I have showed so far you can build this:

http://lerdorf.com/php/ymap/yquakes.php

This uses the simple_rss parser I wrote a while ago. You can see the source for the RSS parser here:

http://lerdorf.com/php/simple_rss.phps

The code then just loops through the entries for the earthquakes and adds a marker for each quake. A very simple little application:

<html><head>
<script type="text/javascript" src="http://api.maps.yahoo.com/v2.0/fl/javascript/apiloader.js"></script>
<style type="text/css">
#mapContainer { 
height: 600px; 
width: 800px; 
} 
</style> 
<?php
include '/usr/local/php5/lib/php/simple_rss.php';

$url = 'http://earthquake.usgs.gov/recenteqsww/catalogs/eqs7day-M2.5.xml';
$feed = rss_request($url, $timeout=3600);
echo <<<EOB
<title>{$feed['title'][0]}</title>
</head><body>
<h1>{$feed['title'][0]}</h1>
<p><font size="+2">{$feed['description'][0]}<br />
{$feed['pubDate'][0]}</font></p>
EOB;
?>
<div id="mapContainer"></div>
<script type="text/javascript">
var latlon = new LatLon(37.416384, -122.024853);
var mymap = new Map("mapContainer", "rlerdorf", latlon, 13);
mymap.addTool( new PanTool(), true );
navWidget = new NavigatorWidget(); 
mymap.addWidget(navWidget); 
<?php 
  $i = 0;
  while(!empty($feed[$i])) {
    $info  = $feed[$i]['description'][0]."<br />";
    $info .= '<a href="'.$feed[$i]['link'][0].'">'.$feed[$i]['link'][0]."</a>";
?>  
mymap.addMarkerByLatLon(
   new CustomPOIMarker('<?php echo $i?>',
    '<?php echo $feed[$i]['title'][0]?>', '<?php echo $info?>', '0x0012f0', '0xFFFFFF'),
   new LatLon(<?php echo $feed[$i]['lat'][0].','.$feed[$i]['long'][0]?>));
<?php
    $i++;
  }
?>
</script>

You can also let the API figure out your markers for you which makes this even simpler. If the RSS feed is using georss correctly you can use the GeoRSSOverlay mechanism. Here it is using the earthquake RSS feed directly:

http://lerdorf.com/php/ymap/rssquakes.php

And here is the code. I am still loading the RSS feed myself from PHP because I want to get the pubDate and title from it, but everything else is handled automatically.

<html><head>
<script type="text/javascript" src="http://api.maps.yahoo.com/v2.0/fl/javascript/apiloader.js"></script>
<style type="text/css">
#mapContainer { 
height: 600px; 
width: 800px; 
} 
</style> 
<?php
include '/usr/local/php5/lib/php/simple_rss.php';

$url = 'http://earthquake.usgs.gov/recenteqsww/catalogs/eqs7day-M2.5.xml';
$feed = rss_request($url, $timeout=3600);
echo <<<EOB
<title>{$feed['title'][0]}</title>
</head><body>
<h1>{$feed['title'][0]}</h1>
<p><font size="+2">{$feed['description'][0]}<br />
{$feed['pubDate'][0]}</font></p>
EOB;
?>
<div id="mapContainer"></div>
<script type="text/javascript">
var latlon = new LatLon(37.416384, -122.024853);
var mymap = new Map("mapContainer", "rlerdorf", latlon, 13);
mymap.addTool(new PanTool(), true);
navWidget = new NavigatorWidget(); 
mymap.addWidget(navWidget); 
overlay = new GeoRSSOverlay('http://earthquake.usgs.gov/recenteqsww/catalogs/eqs7day-M2.5.xml');           
mymap.addOverlay(overlay);
</script>
</body></html>

There are an amazing number of things you can do with this API. What I have described here is just the surface of it. Overlays and events can do nifty things. You can even get at the low-level tile api directly using this:

http://developer.yahoo.net/maps/rest/V1/mapImage.html

I am very much looking forward to see what people out there end up doing with this. I timed how long it took me to write the Earthquake mapping application above. 21 minutes from the time I started looking for the geotagged earthquake data until I was happy with the final app. And that included normal office interruptions and tracking down a dumb syntax error. Note also that any local.yahoo search through the API now includes lat/long info and Flickr has lat/long information as well. And even if a service doesn't provide geotags, as long as they provide addresses you can use the geocoding api to get the lat/long data and do interesting geospatial things with it. Getting to the point where we have full and trivial control over geocoding and mapping opens up a whole new class of application.

Previous Post Next Post