An Integrative Example: Showing Flickr Pictures in Google Earth

In this section of this chapter, I’ll walk you through an example that mashes up Flickr, Google Earth, and Google Maps. Specifically, I’ll show you how to query Flickr for public geotagged photos and convert the response to KML that can then be channeled to either Google Earth or Google Maps.

You can see the program that combines this functionality here:

And you can see the PHP code here:

The flickrgeo.php script described in this chapter is the same code as that in Chapter 10.

What you will see is a basic HTML form aimed at a user who already understands the parameters for the method ( ch13/flickrgeo.php). There are two differences between the parameters used for Flickrgeo and the Flickr API:

When the Flickrgeo form loads, o_format is set by default to html so that you can use the form to enter some values and see some search results rendered as HTML. For example, the following:


displays my public geotagged photos in the Berkeley area. You can change o_format to json and rest to get JavaScript and XML versions of this data, respectively; Flickrgeo just passes back the data that comes from Flickr.

If you set o_format to kml, you get the results as a KML feed. For example, here’s a KML feed of my public geotagged photos in the Berkeley area:


which you can see on Google Maps (see Figure 13-7):[218]


Figure 13.7. Flickr photos displayed in Google Maps via a KML feed

Flickr photos displayed in Google Maps via a KML feed

If you set o_format to nl, you get a KML NetworkLink that enables you to use Google Earth to interact with Flickrgeo. That is, if you change your viewpoint on Google Earth, Google Earth will send to Flickrgeo the parameters of your new viewpoint to get pictures for that new viewpoint.

Hence, Flickrgeo does four major tasks to pull off the mashup:

  1. Querying Flickr for geotagged photos

  2. Converting Flickr results from a single into the corresponding KML

  3. Generating a KML NetworkLink to feed your actions in Google Earth to Flickrgeo

  4. Knitting together the various possibilities for o_format into one script

For the first topic, I refer you to Chapter 10 for a detailed discussion and remind you that the essential point is that you use the bbox parameter to specify a bounding box, add geo to the extras parameter to get the latitude and longitude, and set a minimum upload time to something like 820483200 (for January 1, 1996 Pacific Time) to coax some photos from the API when you are not searching on tags or text.

For the final topic, you can see the logic of what is done by studying the source code. Next I’ll discuss the topics of KML NetworkLink and generating KML from results.

KML NetworkLink

So far I have shown you how to write a KML document and render it with Google Earth and Google Maps. In many situations, including the mashup I create here, it’s extremely helpful to get information flowing from Google Earth back to the program that generated the KML in the first place. Foremost among that data would be the current viewpoint of the user. If you as the generator of the KML know the region that the user is focused on, you can generate data that would be visible in that viewpoint.

That’s the purpose of the <NetworkLink> element in KML. Here I’ll show you how to use it by example. Load the following into Google Earth:

You will see a pin appear in the middle of your current viewpoint announcing the current time. If you click the pin, you’ll see a list of parameters corresponding to the current viewpoint. If you change the viewpoint and wait a couple of seconds, the pin is refreshed to be located in the center of the new viewpoint. How does this happen?

Let’s look first at this:

which is the following:

<?xml version="1.0" encoding="UTF-8"?>
 <kml xmlns="">
     <name>Hello World</name>

A KML NetworkLink defines how often refreshing occurs or the conditions under which it happens. I’ll discuss what a refresh actually does in the next paragraph. There are two modes of refreshing that can be specified in a NetworkLink. The first, based on time, uses the <refreshMode> and <refreshInterval> elements. With those tags, you can, for instance, set a refresh to happen every ten seconds. For the KML I present here, I use refreshing based on changes in the viewpoint, which are parameterized by two tags: <viewRefreshMode> and <viewRefreshTime>. If you consult the KML documentation, you’ll see that one choice for <viewRefreshMode> is onStop—which means a refresh event happens once the viewpoint stops changing for the amount of time specified by the <viewRefreshTime> element—in this case two seconds.

So, what happens during a refresh event? Google Earth does an HTTP GET request on the URL specified by the href element with parameters specified in the <viewFormat> element, which contains something akin to a URI template. That is, Google Earth substitutes the values that correspond to the viewpoint at the time of the refresh event for the value templates such as [bboxWest], [bboxSouth], [bboxEast], and so on. Consult the documentation for a comprehensive list of parameters supported in KML. The template I specify here has the complete current list of parameters. Note that there is no requirement that the parameter names match the naming scheme given by Google. In fact, you should match the parameter names to the ones recognized by the script specified by the href element.

Let’s now turn to the script here:

to see how the HTTP GET request is processed. Here’s the PHP code:

// get the time
$timesnap = date("H:i:s");

// for clarity, place each coordinate into a clearly marked ?bottom-?left 
// or ?top-?right variable

$bboxWest  = isset($_GET['bboxWest']) ? $_GET['bboxWest'] : "-180.0";
$bboxSouth  = isset($_GET['bboxSouth']) ? $_GET['bboxSouth'] : "-90.0";
$bboxEast  = isset($_GET['bboxEast']) ? $_GET['bboxEast'] : "180.0";
$bboxNorth  = isset($_GET['bboxNorth']) ? $_GET['bboxNorth'] : "90.0";

// calculate the approx center of the view -- note that this is inaccurate 
// if the user is not looking straight down
$userlon = (($bboxEast - $bboxWest)/2) + $bboxWest;
$userlat = (($bboxNorth - $bboxSouth)/2) + $bboxSouth;

$response = '<?xml version="1.0" encoding="UTF-8"?>';
$response .= '<kml xmlns="">';
$response .= '<Placemark>';
$response .= "<name>Hello at: $timesnap</name>";

# calculate all the parameters

$arg_text = "";
foreach ($_GET as $key => $val) {
  $arg_text .= "<b>{$key}</b>:{$val}<br>";

$description_text = $arg_text;
$description = "<![CDATA[{$description_text}]]>";
$response .= "<description>{$description}</description>";

$response .= '<Point>';
$response .= "<coordinates>$userlon,$userlat,0</coordinates>";
$response .= '</Point>';
$response .= '</Placemark>';
$response .= '</kml>';
# set $myKMLCode together as a string
 $downloadfile="myKml.kml"; # give a name to appear at the client
 header("Content-disposition: attachment; filename=$downloadfile");
 header("Content-Type: application/; charset=utf8");
 header("Content-Transfer-Encoding: binary");
 header("Content-Length: ".strlen($response));
 header("Pragma: ?no-?cache");
 header("Expires: 0");
echo $response;
  reads all the parameters that are passed to it and displays them to the user. This is accomplished by generating the KML for a <Placemark> element with a <description> element with all the parameters.

Let’s return to how I use the KML NetworkLink in flickrgeo.php. In that script, I needed to generate <NetworkLink> elements that looked like this:

<?xml version="1.0" encoding="UTF-8"?>
<kml xmlns="">
    <name>Pictures from Flickr</name>
      <![CDATA[<a href='
%2Cowner_name%2Cicon_server%2Ctags&o_format=html'>Search Something Different</a>]]>

This is a <NetworkLink> element for generating refreshable KML on photos that match a full-­text search for stop sign. Notice how the viewpoint is passed from Google Earth to by the viewFormat element:


Generating the KML for the Photos

The following excerpt of KML shows the structure of the KML that flickrgeo.php generates to display the photos in Google Earth or Google Maps (I have put some KML elements in bold that I have yet to introduce.):

<?xml version="1.0" encoding="UTF-8"?>
<kml xmlns="">
    <Style id="118550863">
      <name>Flickr Photos</name>
      <description>Total Number of Photos available: 72&amp;nbsp;&lt;
a href='
a href='
        <name>shrink wrap car</name>
          <![CDATA[<a href=''>
<img src=''></a>]]>

Note the following features of the KML:

  • There is a <Placemark> element for each photo, whose name element holds the title for the photo. In the <description> element is HTML for the medium-­sized version of the photo. The latitude and longitude, drawn from the geo information provided by Flickr, goes into two places: the coordinates element for the <Point> element and a <LookAt> view.

  • Each <Placemark> element is tied to a <Style> element to generate custom icons for each photo. The icon is the square version of the Flickr photo. The association is made through the <styleUrl> element.

  • There is a <Folder> element that groups all the <Placemark> elements. The <description> element for the <Folder> element contains links to the KML itself and to a Google Map showing this KML. These links provide you with a way of getting hold of what you are seeing in Google Earth.

The flickrgeo.php Code

Here’s an edited listing of the flickrgeo.php code:

# flickrgeo.php
# copyright Raymond Yee, 2007

# xmlentities substitutes characters in $string that can be expressed
# as the predefined XML entities.

function xmlentities ($string)
{ return str_replace ( 
           array ( '&', '"', "'", '<', '>' ), 
           array ( '&amp;' , '&quot;', '&apos;' , '&lt;' , '&gt;' ), 
           $string ); 

# converts an associative array representing form parameters 
# and values into the request part of a URL.

function form_url_params($arg_list, $rid_empty_value=TRUE) {
    $list = array();

    foreach ($arg_list as $arg => $val) {
     if (!($rid_empty_value) || (strlen($val) > 0)) {
        $list[] = $arg . "=" . urlencode($val);
    return join("&",$list);

# a simple wrapper around for public photos.
# It deals a request for either the Flickr REST or JSON formats

class flickrwrapper {
  protected $api_key;

  public function __construct($api_key) {
    $this->api_key = $api_key;

  # generic method for retrieving content for a given URL.
  protected function getResource($url){
    $chandle = curl_init();
    curl_setopt($chandle, CURLOPT_URL, $url);
    curl_setopt($chandle, CURLOPT_RETURNTRANSFER, 1);
    $result = curl_exec($chandle);

    return $result;

  # returns an HTTP response body and headers
  public function search($arg_list) {
    # attach API key
    $arg_list['api_key'] = $this->api_key;

    # attach parameters specific to the format request, which is either JSON or REST.
    $format = $arg_list["format"];
    if ($format == "rest") {
      $url = "" . 
      $rsp = $this->getResource($url);
      $response["body"] = $rsp;
      $response["headers"] = array("Content-Type"=>"application/xml");
      return $response;
    } elseif ($format == "json") {
      $arg_list["nojsoncallback"] = 1;
      $url = "" . 
      $rsp = $this->getResource($url);
      $response["headers"] = array("Content-Type"=>"text/javascript");
      $response["body"] = $rsp;
      return $response;
  } // search
} //flickrwrapper

class flickr_html {

# generates a simple form based on the parameters 
# and values of the input associative array $arg_array
# uses $path as the target of the form's action attribute

  public function generate_form($arg_array, $path) {
    $form_html = "";
    foreach ($arg_array as $arg => $default) {
      $form_html .= <<<EOT
{$arg}:<input type="text" size="20" name="{$arg}" value="{$default}"><br>

    $form_html = <<<EOT
<form action="{$path}" method="get">
<input type="submit" value="Go!">

    return $form_html;
  } //generate_form

  # generates a simple HTML representation of the results of
  public function html_from_pics($rsp) {

    $xml = simplexml_load_string($rsp);
    $s = "";
    $s .= "Total number of photos: " . $xml->photos['total'] . "<br>";

    # http://farm{farm-id}{server-id}/{id}_{secret}.jpg
    foreach ($xml->photos->photo as $photo) {
      $farmid = $photo['farm'];
      $serverid = $photo['server'];
      $id = $photo['id'];
      $secret = $photo['secret'];
      $owner = $photo['owner'];
      $thumb_url = 
      $page_url = "{$owner}/{$id}";
      $image_html= "<a href='{$page_url}'><img src='{$thumb_url}'></a>";
      $s .= $image_html;
    return $s;
} // flickr_html

# a class to handle conversion of Flickr results to KML

class flickr_kml {

# helper function to create a new text node with $string that is wrapped by an
# element named by $childName -- and then attach the whole thing to $parentNode.
# allow for a namespace to be specified for $childName

  protected function attachNewTextNode($parentNode, 
                                       $childName,$childNS="",$string="") {
    $childNode = $parentNode->appendChild(new DOMElement($childName,$childNS));
    $childNode->appendChild(new DOMText($string));
    return $childNode;

  # create the subelements for Style
    <Style id="118550863">

  protected function populate_style($style,$photo) {
    $id = $photo['id'];
    $farmid = $photo['farm'];
    $serverid = $photo['server'];
    $secret = $photo['secret'];
    $square_url = 

    $id_attr = $style->setAttributeNode(new DOMAttr('id', $id));
    $iconstyle = $style->appendChild(new DOMElement("IconStyle"));
    $icon = $iconstyle->appendChild (new DOMElement("Icon"));
    $href = $this->attachNewTextNode($icon,"href","",$square_url);

    return $style;

  # converts the response from the Flickr photo search ($rsp),
  # the arguments from the original search ($arg_array),
  # the $path of the script to KML

  public function kml_from_pics($arg_array, $path, $rsp) {

    $xml = simplexml_load_string($rsp);
    $dom = new DOMDocument('1.0', 'UTF-8');
    $kml = $dom->appendChild(new DOMElement('kml'));
    $attr = $kml->setAttributeNode(new DOMAttr('xmlns', 
    $document = $kml->appendChild(new DOMElement('Document'));

    # See
    # Remember http://farm{farm-id}{server-id}/{id}_{secret}.jpg 
    # syntax for URLs

    # parameters for LookAt -- ?hard-?coded in this instance
    $range = 2000;
    $altitude = 0;
    $heading =0;
    $tilt = 0;

    # make the <Style> elements first
    foreach ($xml->photos->photo as $photo) {
      $style = $document->appendChild(new DOMElement('Style'));

    # now make the <Placemark> elements -- but tuck them under one Folder
    # in the Folder, add URLs for the KML document and how to send 
    # the KML document to Google Maps

    $folder = $document->appendChild(new DOMElement('Folder'));
    $folder_name_node = $this->attachNewTextNode($folder,"name","","Flickr Photos");
    $kml_url =  $path . "?" . form_url_params($arg_array,TRUE);
    $description_string = "Total Number of Photos available: {$xml->
photos['total']}" . "&nbsp;<a href='{$kml_url}'>KML</a>";
    $description_string .= "&nbsp;<a href='" . "" . 
urlencode($kml_url) .  "'>GMap</a>";
    $folder_description_node = $this->

    # loop through the photos to convert to a Placemark KML element
    foreach ($xml->photos->photo as $photo) {
      $farmid = $photo['farm'];
      $serverid = $photo['server'];
      $id = $photo['id'];
      $secret = $photo['secret'];
      $owner = $photo['owner'];
      $thumb_url = 
      $med_url = 
      $page_url = "{$owner}/{$id}";
      $image_html= "<a href='{$page_url}'><img src='{$med_url}'></a>";
      $title = $photo['title'];
      $latitude = $photo['latitude'];
      $longitude = $photo['longitude'];

      $placemark = $folder->appendChild(new DOMElement('Placemark'));

      # place the photo title into the <name> KML element
      $name = $this->attachNewTextNode($placemark,"name","",$title);

      # drop the title and thumbnail into description and wrap in CDATA 
      # to work around encoding issues
      $description_string = "{$image_html}";
      $description = $placemark->appendChild(new DOMElement('description'));

      $lookat = $placemark->appendChild(new DOMElement('LookAt'));
      $longitude_node = $this->attachNewTextNode($lookat,"longitude","",$longitude);
      $latitude_node = $this->attachNewTextNode($lookat,"latitude","",$latitude);
      $altitudeNode = $this->attachNewTextNode($lookat,"altitude","",$altitude);
      $altitudeMode = 
      $rangeNode = $this->attachNewTextNode($lookat,"range","",$range);
      $tiltNode = $this->attachNewTextNode($lookat,"tilt","",$tilt);
      $headingNode = $this->attachNewTextNode($lookat,"heading","",$heading);

      $styleurl = $this->attachNewTextNode($placemark, "styleUrl","","#".$id);

      $point = $placemark->appendChild(new DOMElement('Point'));
      $coordinates_string = "{$longitude},{$latitude},{$altitude}";
      $coordinates = 
    return $dom->saveXML();

  # generate a network link based on the user search parameters ($arg_list) 
  # and the $path to this script
  public function generate_network_link ($arg_list, $path) {

    # look through the $arg_list but get rid of lat/long and blanks
    unset ($arg_list['lat0']);
    unset ($arg_list['lat1']);
    unset ($arg_list['lon0']);
    unset ($arg_list['lon1']);

    $arg_list['o_format'] = 'kml';  //set to KML
    $url = $path . "?" . form_url_params($arg_list,TRUE);
    $url = xmlentities($url);

    # generate a description string to guide user 
    # to reparameterizing the network link
    $arg_list['o_format'] = 'html';
    $url2 = $path . "?" . form_url_params($arg_list,TRUE);
    $description = "<a href='{$url2}'>Search Something Different</a>";

    $nl = <<<EOT
<?xml version="1.0" encoding="UTF-8"?>
<kml xmlns="">
    <name>Pictures from Flickr</name>
   return $nl;
} // flickr_kml

# this class translates what comes in on the URL and from form values 
# to parameters to submit to Flickr

class flickr_view {

  # this function filters $_GET passed in as $get by parameters 
  # that are in $defaults
  # only parameters named in $defaults are allowed -- and if that value isn't set in
  # $get, then this function passes back the default value

  public function form_input_to_user_inputs($get,$defaults) {
    $params = array();
    foreach ($defaults as $arg => $default_value) {
      $params[$arg] = isset($get[$arg]) ? $get[$arg] : $default_value;
    return $params;

  # translate the user inputs to the appropriate ones for Flickr.
  # for example -- fold the latitudes and longitude coordinates into bbox
  # get rid of o_format  for Flickr

  public function user_inputs_to_flickr_params($user_inputs) {
    $search_params = $user_inputs;

    $o_format = $user_inputs["o_format"];
    unset ($search_params["o_format"]);

    if (($o_format == "json") || ($o_format == "rest")) {
      $search_params["format"] = $o_format;
    } else {
      $search_params["format"] = "rest";

    #recast the lat and long parameters in bbox

    $bbox = "{$search_params['lon0']},{$search_params['lat0']},
    $search_params['bbox'] = $bbox;

    return $search_params;
  } // user_inputs_to_flickr_params
} //flickr_view

// API key here
$api_key = "[API-KEY]";

# a set of defaults -- center the search around Berkeley by default 
# and any geotagged photo in that bounding box.
# BTW, this script needs at least geo in extras.
# min_upload_date corresponds to Jan 1, 1996 (Pacific time)
$default_args = array(
  "user_id" => '',
  "tags" => '',
  "tag_mode" => '',
  "text" => '',
  "min_upload_date" => '820483200',
  "max_upload_date" => '',
  "min_taken_date" => '',
  "max_taken_date" => '',
  "license" => '',
  "sort" => '',
  "privacy_filter" => '',
  "lat0" => 37.81778516606761,
  "lon0" => -122.34374999999999,
  "lat1" => 37.92619056937629,
  "lon1" => -122.17208862304686,
  "accuracy" => '',
  "safe_search" => '',
  "content_type" => '',
  "machine_tags" => '',
  "machine_tag_mode" => '',
  "group_id" => '',
  "place_id" => '',
  "extras" => "geo",
  "per_page" => 10,
  "page" => 1,
  "o_format" => 'html'

# calculate the path to this script as a URL.
$path = "http://" . $_SERVER['SERVER_NAME'] . $_SERVER['PHP_SELF'];

# instantiate the Flickr wrapper and the view object
$fw = new flickrwrapper($api_key);
$fv = new flickr_view();

# get the parameters that have been submitted to it.
$user_inputs = $fv->form_input_to_user_inputs($_GET,$default_args);
$search_params = $fv->user_inputs_to_flickr_params($user_inputs);

# see what the requested format is
$o_format = $user_inputs["o_format"];

# if the user is looking for a network link, 
# calculate the Networklink KML and return it
# with the appropriate ?Content-?Type for KML

if ($o_format == 'nl') {
  $fk = new flickr_kml();
  $downloadfile="flickr.kml"; # give a name to appear at the client
  header("Content-disposition: attachment; filename=$downloadfile");
  print $fk->generate_network_link($user_inputs,$path);

# If the user is looking instead for JSON, REST, HTML, or KML, we query Flickr

$response = $fw->search($search_params);

# If the request is for JSON or REST, 
# just pass back the results of the Flickr search
if (($o_format == "json") || ($o_format == "rest")) {
  foreach ($response["headers"] as $header => $val) {
  print $response["body"];

  # if the request is for HTML or KML, do the appropriate transformations.

} elseif ($o_format == "html") {

  # now translate to HTML
  $fh = new flickr_html();
  print $fh->generate_form($user_inputs, $_SERVER['PHP_SELF']);
  print $fh->html_from_pics($response["body"]);

} elseif ($o_format == "kml") {

  $fk = new flickr_kml();
  $downloadfile="flickr.kml"; # give a name to appear at the client
  header("Content-disposition: attachment; filename=$downloadfile");
  print $fk->kml_from_pics($user_inputs, $path, $response["body"]);