Newsmap – using Placemaker to add geo location to a news feed

I am right now very excited about the new Placemaker beta – a location extraction web service released at Where2.0. Using Placemaker you can find all the geographical locations in a feed or a text or a web url and you get them back as an array of places.

As a demo I took the Yahoo News feed and ran it through Placemaker. The resulting places are plotted on a map and the map moves from location to location when you hover over the news items.

The result is online at http://isithackday.com/hacks/placemaker/map.php

Yahoo News Map by  you.

Getting the data from the data feed and running it through placemaker is very straight forward. I explained the basic principle in this blog post on the Yahoo Developer Network blog. The only thing to think about is to define the input and output types correctly:


<?php 
$key = 'PASTE YOUR API KEY HERE';
$apiendpoint = 'http://wherein.yahooapis.com/v1/document';
$url = 'http://rss.news.yahoo.com/rss/topstories';
$inputType = 'text/rss';
$outputType = 'rss';
$post = 'appid='.$key.'&documentURL='.$url.
                '&documentType='.$inputType.'&outputType='.$outputType;
$ch = curl_init($apiendpoint);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, $post);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);  
$results = curl_exec($ch);
?>

If you look at the source of this example you will find that Placemaker injected contentlocation elements in the feed itself:


<cl:contentlocation 
xmlns:georss="http://www.georss.org/georss" xmlns:cl="http://wherein.yahooapis.com/v1/cle"
xmlns:xml="http://www.w3.org/XML/1998/namespace" xml:lang="en">
  <cl:place>
    <cl:woeId>2514815</cl:woeId>
    <cl:name><![CDATA[Washington, DC, US]]></cl:name>
    <cl:latitude>38.8913</cl:latitude>
    <cl:longitude>-77.0337</cl:longitude>
  </cl:place>
  <cl:place>
    <cl:woeId>23424793</cl:woeId>
    <cl:name><![CDATA[Cuba]]></cl:name>
    <cl:latitude>21.511</cl:latitude>
    <cl:longitude>-77.8068</cl:longitude>
  </cl:place>
  <cl:place>
    <cl:woeId>23424977</cl:woeId>
    <cl:name><![CDATA[United States]]></cl:name>
    <cl:latitude>48.8907</cl:latitude>
    <cl:longitude>-116.982</cl:longitude>
  </cl:place>
  <cl:place>
    <cl:woeId>55843872</cl:woeId>
    <cl:name><![CDATA[Guantanamo Bay, Caimanera, 
    Guantanamo, CU]]></cl:name>
    <cl:latitude>19.9445</cl:latitude>
    <cl:longitude>-75.1541</cl:longitude>
  </cl:place>
</cl:contentlocation>

You’ll also notice that the elements are namespaced and the names of the locations in CDATA blocks, both things I hate with a passion. Not because they don’t make sense, but because simplexml can be drag to make understand them.

What I wanted to do with this data was twofold: create a JSON array of geo locations to plot on a map and a display of the news content. This is the PHP that does that:


$places = simplexml_load_string($results, 'SimpleXMLElement', 
                                LIBXML_NOCDATA);  
// if there are elements found
if($places->channel->item){
  // start a JSON array
  $output .= '[';
  // start the HTML output
  $html = '<ul id="news">';
  // set the counter - this will be needed to link news 
  // items and map markers
  $count = 0;
  // loop over RSS items
  foreach($places->channel->item as $p){
    // set inner counter (as there are more locations per news item)
    $innercount = 0;
    // start the HTML list item and give it an ID with the counter
    // value
    $html .=  '<li id="news'.$count.'"';
    // all child elements with the defined namespace
    $locs = $p->children('http://wherein.yahooapis.com/v1/cle');
    // check that there is a location sub-element in this item
    if($locs->contentlocation){
      // if there is one, add a class to the LI
      $html .= ' class="haslocation"';
      // start an array for displaying of the locations under the
      // news items
      $dlocs = array();
      // loop over all the places found for this item
      foreach($locs->contentlocation->place as $pl){
        // append a new JS object with the location data
        // and a unique ID to the locations array 
        $locations[] = '{"name":"'.$pl->name.'","title":"'.
                        preg_replace('/\n+/','',addslashes($p->title)).
                       '","lat":"'.$pl->latitude.
                       '","lon":"'.$pl->longitude.'","id":"m'.
                       $count.'x'.$innercount.'"}';
        // add the location name to the display locations array
        $dlocs[] = $pl->name;                
        // increase the inner count to ensure that every marker has 
        // a unique ID
        $innercount++;
      }
    }
    // append the HTML for the news item
    $html.='><h2><a href="'.$p->link.'">'.$p->title.'</a></h2><p>'.
            $p->description.'</p>';
    // if locations were found, add them 
    if(sizeof($dlocs)>0){
      $html.='<p class="locations">Locations: '.join(',',$dlocs).'</p>';
    }
    // end the list item
    $html.='</li>';
    // increase the counter
    $count++;
  }
  // join the json object data with a comma and close the JSON array
  $output .= join(',',$locations);
  $output .= ']';
// if there are no items simply return nothing
} else {
  $output = '';
}
// and this ends the HTML
$html.= '</ul>';
?>

The result of this can be seen here http://isithackday.com/hacks/placemaker/map-2.php.

The JavaScript to show the map is pretty straight forward and more or less the demo example of the maps API:


// will be called with the array assembled in PHP
function placeonmap(o){
  // if there are locations
  if(o.length > 0){
    // create a new geopoints array to hold all locations
    // this is needed to determine the original zoom 
    // level of the map
    var geopoints = [];
    // add map with controls
    var map = new YMap(document.getElementById('map')); 
    map.addZoomLong();
    map.addPanControl();
    // loop over locations
    for(var i=0;i<o.length;i++){
      // define a new geopoint and store it in the array
      var point = new YGeoPoint(o[i].lat,o[i].lon);
      geopoints.push(point);
      // create a new marker and give it the unique
      // id defined in the PHP. Pop up the title of 
      // the news item and the name of the location when the
      // user hovers over the marker
      var newMarker = new YMarker(point,o[i].id);
      newMarker.addAutoExpand(o[i].title + '('+o[i].name+')');
      map.addOverlay(newMarker);
    }
    // define best zoom level and show map
    var zac = map.getBestZoomAndCenter(geopoints);        
    map.drawZoomAndCenter(zac.YGeoPoint,zac.zoomLevel);
  }
  // add a mouseover handler to the list of results
  YAHOO.util.Event.on('news','mouseover',function(e){
    // remove the "news" text of the ID of the current target
    // as we named the list items news0 to news19
    var id = YAHOO.util.Event.getTarget(e).id.replace('news','');
    // if there is still something left we have one of the news
    // items
    if(id!==''){
      // get the first marker with the ID we defined in the loop.
      var marker = map.getMarkerObject('m'+id+'x0');
      // if there is one, pan the map there and show the message
      // attached to it.
      if(marker){
        map.panToLatLon(marker.YGeoPoint);
        marker.openAutoExpand();
      }
    }
  });
}
// call placeonmap with the JSON array
placeonmap(<?php echo $output;?>);

That’s pretty much it. I am sure it can be refined, but it is amazing how easy it is to get geo information into any text with Placemaker.

Tags: , , , , ,

One Response to “Newsmap – using Placemaker to add geo location to a news feed”

  1. Ara Pehlivanian Says:

    VERY cool, you’re definitely inspiring me! :-)

Leave a Reply

Subscribe to RSS Brighter Planet's 350 Challenge
Wait till I come! is the blog of Christian Heilmann , a developer evangelist living and working in London, England. Download vcard.

Feed me, Seymour: Entries (RSS) and Comments (RSS).