TwitterGalaxy – Part 2 – Twitter API Integration into Google Earth using a KML Tour

In my last post TwitterGalaxy – DIY Google Liquid Galaxy from Recycled Monitors with integrated Twitter Feed we had put together massive great array of monitors and I had promised that there was a reason – other then porn and gaming.

The idea is to show the locations and latest Tweets of the Times’ foreign corespondents on Google Earth. The code to do this is not clean but it’s a prototype *cough* so what are you going to do? The reason for the mess in the code is that Google Earth uses a variant on XML, KML to control the content seen in Google Earth. This means you end up with a sea of string concatenation to create the file from you back-end server.

First lets deal with the easy bit and look at getting the Tweet data. Handily, the list of Times correspondents are kept in a Twitter list. Using this and the Twitter search API we can dodge the need to use o’Auth or even have an account. To make life even easier the API call to get a Twitter list returns the latest Tweet for each User in the list, result!. The code goes something like this:

$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, "https://api.twitter.com/1/lists/members.json?slug=LISTNAME&owner_screen_name=USERNAME");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);

$output = curl_exec($ch);
curl_close($ch);
$correspondents = json_decode($output);
$correspondents = $correspondents->users;

The problem is that none of the tweets or the accounts have Geo tags in, this is for security reasons, so people don’t know exactly where they are. So we need to add in a look up table (code’s looking clunky already):

$locations = array(
    'user1'  => array("lat" => '51.50015240', "lng" => '-0.12623620',        'location' => 'London'),
    'user2'  => array("lat" => '40.71435280', "lng" => '-74.00597309999999', 'location' => 'New York'),
    'user3'  => array("lat" => '40.71435280', "lng" => '-74.00597309999999', 'location' => 'New York'),
    'user4'  => array("lat" => '38.89511180', "lng" => '-77.03636580',       'location' => 'Washington')
);

$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, "https://api.twitter.com/1/lists/members.json?slug=LISTNAME&owner_screen_name=USERNAME");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);

$output = curl_exec($ch);
curl_close($ch);
$correspondents = json_decode($output);
$correspondents = $correspondents->users;

Next comes the mess, the Google KML code. First off we have to actually create a file as Google Earth does not support a direct like to anything that does not have a .KML extension. So we can’t just point it to the web server, I guess you could do something with rewrite rules, but I’m not up for that at the moment. Let’s just use the inbuilt PHP functions to write out a .KML file.

I’m going to build up the file step by step, rather then show you all the code and try and explain it. First lest create a KML file with all the locations of the Journalists marked out with a Placemarker:

$locations = array(
    'user1'  => array("lat" => '51.50015240', "lng" => '-0.12623620',        'location' => 'London'),
    'user2'  => array("lat" => '40.71435280', "lng" => '-74.00597309999999', 'location' => 'New York'),
    'user3'  => array("lat" => '40.71435280', "lng" => '-74.00597309999999', 'location' => 'New York'),
    'user4'  => array("lat" => '38.89511180', "lng" => '-77.03636580',       'location' => 'Washington')
);

$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, "https://api.twitter.com/1/lists/members.json?slug=LISTNAME&owner_screen_name=USERNAME");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);

$output = curl_exec($ch);
curl_close($ch);
$correspondents = json_decode($output);
$correspondents = $correspondents->users;

$kml = fopen(getcwd()."/tweets.kml", "w+");

$content = '';
$content .= '<?xml version="1.0" encoding="UTF-8"?>';
$content .= '<kml xmlns="http://www.opengis.net/kml/2.2" xmlns:gx="http://www.google.com/kml/ext/2.2">';
$content .= '  <Document>';

foreach($correspondents as $k => $c) {
  $content .= '  <Placemark id="placemark'.$k.'">';
  $content .= '    <name>'.ucwords($c->name).'</name>';
  $content .= '    <open>0</open>';
  $content .= '    <description>';
  $content .= '      &lt;img src="'.$c->profile_image_url.'" /&gt;';
  $content .= '      &lt;br /&gt;&lt;br /&gt;';
  $content .= '      &lt;b&gt;Tweet: &lt;/b&gt;'.htmlspecialchars(strip_tags($c->status->text));
  $content .= '      &lt;br /&gt;&lt;br /&gt;';
  $content .= '      &lt;b&gt;Sent: &lt;/b&gt;'.$c->status->created_at;
  $content .= '      &lt;br /&gt;&lt;br /&gt;';
  $content .= '      &lt;b&gt;Location: &lt;/b&gt;'.$locations[$c->screen_name]['location'];
  $content .= '    </description>';
  $content .= '    <address>'.$locations[$c->screen_name]['location'].'</address>';
  $content .= '  </Placemark>';
}
$content .= '  </Document>';
$content .= '</kml>';

fwrite($kml, $content);
fclose($kml);

With the code in place, hit your web server to create the KML file. Now lets load it into Google Earth. Open up Google Earth goto File>Open. Point to the KML file you have created. Now you should have drop pins for all of the locations you listed in you XML file. This is great for a test but we want this to be dynamic so you can update on the KML file on the fly.

To do this in Google Earth you have to create a Network link, which means another KML file. Create a file containing the below code:

<?xml version="1.0" encoding="UTF-8"?>
<kml xmlns="http://www.opengis.net/kml/2.2">
<NetworkLink>
  <name>Loads Times</name>
  <Link>
    <href>http://www.YOURSERVER.com/tweets.kml</href>
    <refreshMode>onInterval</refreshMode>
    <refreshInterval>240</refreshInterval>
    <viewRefreshMode>onStop</viewRefreshMode>
    <viewRefreshTime>0</viewRefreshTime>
  </Link>
</NetworkLink>
</kml>

Change the code to point to your web server and KML file. Now go back in to Google Earth, remove the placmerker file you created earlier by right clicking on it in the side bar and selecting delete. Then go to File>Open select the network file. Now in the right hand bar you should have a network drive listed, if all has gone to plan you will have folder icon with a green network light, if not you will get a red light.

Assuming that the light is green if you click the arrow to the left of the logo you should see your main KML file. Now when you update your code every four minutes Google Earth should update. If you want this to happen quicker you can right click on the network icon and select refresh or edit the network file and reduce the waiting time.

Back to the PHP code again. You can override the default Google Earth displays from your KML file. So lets change the icons from a drop pin to something more interesting, and style up the placemarker overlay box:

$locations = array(
    'user1'  => array("lat" => '51.50015240', "lng" => '-0.12623620',        'location' => 'London'),
    'user2'  => array("lat" => '40.71435280', "lng" => '-74.00597309999999', 'location' => 'New York'),
    'user3'  => array("lat" => '40.71435280', "lng" => '-74.00597309999999', 'location' => 'New York'),
    'user4'  => array("lat" => '38.89511180', "lng" => '-77.03636580',       'location' => 'Washington')
);

$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, "https://api.twitter.com/1/lists/members.json?slug=LISTNAME&owner_screen_name=USERNAME");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);

$output = curl_exec($ch);
curl_close($ch);
$correspondents = json_decode($output);
$correspondents = $correspondents->users;

$kml = fopen(getcwd()."/tweets.kml", "w+");

$content = '';
$content .= '<?xml version="1.0" encoding="UTF-8"?>';
$content .= '<kml xmlns="http://www.opengis.net/kml/2.2" xmlns:gx="http://www.google.com/kml/ext/2.2">';
$content .= '  <Document>';
$content .= '    <Style id="barStyle">';
$content .= '      <IconStyle id="barIcon">';
$content .= '        <Icon>';
$content .= '            <href>http://www.YOURSERVER.com/YOURICON.png</href>';
$content .= '        </Icon>';
$content .= '      </IconStyle>';

$content .= '    <BalloonStyle>';
$content .= '      <bgColor>ffffff</bgColor>';
$content .= '      <text><![CDATA[';
$content .= '      <b><font size="+3" face="Times New Roman">$[name]</font></b>';
$content .= '      <br/><br/>';
$content .= '      <font size="+2" face="Times New Roman">$[description]</font>';
$content .= '      ]]></text>';
$content .= '    </BalloonStyle>';
$content .= '  </Style>';

foreach($correspondents as $k => $c) {
  $content .= '  <Placemark id="placemark'.$k.'">';
  $content .= '    <name>'.ucwords($c->name).'</name>';
  $content .= '    <open>0</open>';
  $content .= '    <description>';
  $content .= '      &lt;img src="'.$c->profile_image_url.'" /&gt;';
  $content .= '      &lt;br /&gt;&lt;br /&gt;';
  $content .= '      &lt;b&gt;Tweet: &lt;/b&gt;'.htmlspecialchars(strip_tags($c->status->text));
  $content .= '      &lt;br /&gt;&lt;br /&gt;';
  $content .= '      &lt;b&gt;Sent: &lt;/b&gt;'.$c->status->created_at;
  $content .= '      &lt;br /&gt;&lt;br /&gt;';
  $content .= '      &lt;b&gt;Location: &lt;/b&gt;'.$locations[$c->screen_name]['location'];
  $content .= '    </description>';
  $content .= '    <address>'.$locations[$c->screen_name]['location'].'</address>';
  $content .= '    <styleUrl>#barStyle</styleUrl>';
  $content .= '  </Placemark>';
}
$content .= '  </Document>';
$content .= '</kml>';

fwrite($kml, $content);
fclose($kml);

When you have saved the code changes, don’t forget to run the code in your browser to update the KML file. Alternatively you can setup a Cron to run and update the KML file.

I’ve made the sizing on the alert boxes much bigger then is needed on a normal screen so you might want to play around with it. This time when you update Google Earth if all has gone according to plan then when you click on a placemarker in the side bar you should see the latest Tweet for that Twitter User.

Now as this is a passive display, we don’t want people to have to go and click on each user to display these tweets. This is where Google Earth tours helps us out. A tour goes thought a list of predefined destinations, then if you click the repeat button in the play/pause controller then it will loop infinitely. This coupled with the network loader means things are now updated automatically.

Here is the code:

$locations = array(
    'user1'  => array("lat" => '51.50015240', "lng" => '-0.12623620',        'location' => 'London'),
    'user2'  => array("lat" => '40.71435280', "lng" => '-74.00597309999999', 'location' => 'New York'),
    'user3'  => array("lat" => '40.71435280', "lng" => '-74.00597309999999', 'location' => 'New York'),
    'user4'  => array("lat" => '38.89511180', "lng" => '-77.03636580',       'location' => 'Washington')
);

$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, "https://api.twitter.com/1/lists/members.json?slug=LISTNAME&owner_screen_name=USERNAME");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);

$output = curl_exec($ch);
curl_close($ch);
$correspondents = json_decode($output);
$correspondents = $correspondents->users;

$kml = fopen(getcwd()."/tweets.kml", "w+");

$content = '';
$content .= '<?xml version="1.0" encoding="UTF-8"?>';
$content .= '<kml xmlns="http://www.opengis.net/kml/2.2" xmlns:gx="http://www.google.com/kml/ext/2.2">';
$content .= '  <Document>';
$content .= '    <Style id="barStyle">';
$content .= '      <IconStyle id="barIcon">';
$content .= '        <Icon>';
$content .= '            <href>http://www.YOURSERVER.com/YOURICON.png</href>';
$content .= '        </Icon>';
$content .= '      </IconStyle>';

$content .= '    <BalloonStyle>';
$content .= '      <bgColor>ffffff</bgColor>';
$content .= '      <text><![CDATA[';
$content .= '      <b><font size="+3" face="Times New Roman">$[name]</font></b>';
$content .= '      <br/><br/>';
$content .= '      <font size="+2" face="Times New Roman">$[description]</font>';
$content .= '      ]]></text>';
$content .= '    </BalloonStyle>';
$content .= '  </Style>';

$content .= '  <gx:Tour>';
$content .= '    <name>Play me</name>';
$content .= '    <gx:Playlist>';
foreach($correspondents as $k => $c) {
  $content .= '      <gx:FlyTo>';
  $content .= '        <gx:duration>24.0</gx:duration>';  $content .= '        <LookAt>';
  $content .= '          <longitude>'.getLng($c->screen_name, $locations).'</longitude>';
  $content .= '          <latitude>'.getLat($c->screen_name, $locations).'</latitude>';
  $content .= '          <altitude>2000</altitude>';
  $content .= '          <heading>-9</heading>';
  $content .= '          <tilt>75</tilt>';
  $content .= '          <range>10000</range>';
  $content .= '          <gx:altitudeMode>relativeToGround</gx:altitudeMode>';
  $content .= '        </LookAt>';
  $content .= '      </gx:FlyTo>';
}
$content .= '    </gx:Playlist>';
$content .= '  </gx:Tour>';

foreach($correspondents as $k => $c) {
  $content .= '  <Placemark id="placemark'.$k.'">';
  $content .= '    <name>'.ucwords($c->name).'</name>';
  $content .= '    <open>0</open>';
  $content .= '    <description>';
  $content .= '      &lt;img src="'.$c->profile_image_url.'" /&gt;';
  $content .= '      &lt;br /&gt;&lt;br /&gt;';
  $content .= '      &lt;b&gt;Tweet: &lt;/b&gt;'.htmlspecialchars(strip_tags($c->status->text));
  $content .= '      &lt;br /&gt;&lt;br /&gt;';
  $content .= '      &lt;b&gt;Sent: &lt;/b&gt;'.$c->status->created_at;
  $content .= '      &lt;br /&gt;&lt;br /&gt;';
  $content .= '      &lt;b&gt;Location: &lt;/b&gt;'.$locations[$c->screen_name]['location'];
  $content .= '    </description>';
  $content .= '    <address>'.$locations[$c->screen_name]['location'].'</address>';
  $content .= '    <styleUrl>#barStyle</styleUrl>';
  $content .= '  </Placemark>';
}
$content .= '  </Document>';
$content .= '</kml>';

fwrite($kml, $content);
fclose($kml);

Now we have a a block of code to do the tour. When you play the tour, it will fly to each location and then on to the next. Next we want it to fly to a user, pause and show there tweet before moving on to the next:

$locations = array(
    'user1'  => array("lat" => '51.50015240', "lng" => '-0.12623620',        'location' => 'London'),
    'user2'  => array("lat" => '40.71435280', "lng" => '-74.00597309999999', 'location' => 'New York'),
    'user3'  => array("lat" => '40.71435280', "lng" => '-74.00597309999999', 'location' => 'New York'),
    'user4'  => array("lat" => '38.89511180', "lng" => '-77.03636580',       'location' => 'Washington')
);

$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, "https://api.twitter.com/1/lists/members.json?slug=LISTNAME&owner_screen_name=USERNAME");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);

$output = curl_exec($ch);
curl_close($ch);
$correspondents = json_decode($output);
$correspondents = $correspondents->users;

$kml = fopen(getcwd()."/tweets.kml", "w+");

$content = '';
$content .= '<?xml version="1.0" encoding="UTF-8"?>';
$content .= '<kml xmlns="http://www.opengis.net/kml/2.2" xmlns:gx="http://www.google.com/kml/ext/2.2">';
$content .= '  <Document>';
$content .= '    <Style id="barStyle">';
$content .= '      <IconStyle id="barIcon">';
$content .= '        <Icon>';
$content .= '            <href>http://www.YOURSERVER.com/YOURICON.png</href>';
$content .= '        </Icon>';
$content .= '      </IconStyle>';

$content .= '    <BalloonStyle>';
$content .= '      <bgColor>ffffff</bgColor>';
$content .= '      <text><![CDATA[';
$content .= '      <b><font size="+3" face="Times New Roman">$[name]</font></b>';
$content .= '      <br/><br/>';
$content .= '      <font size="+2" face="Times New Roman">$[description]</font>';
$content .= '      ]]></text>';
$content .= '    </BalloonStyle>';
$content .= '  </Style>';

$content .= '  <gx:Tour>';
$content .= '    <name>Play me</name>';
$content .= '    <gx:Playlist>';
foreach($correspondents as $k => $c) {
  $content .= '      <gx:FlyTo>';
  $content .= '        <gx:duration>24.0</gx:duration>';  $content .= '        <LookAt>';
  $content .= '          <longitude>'.$locations[$c->screen_name]['lng'].'</longitude>';
  $content .= '          <latitude>'.$locations[$c->screen_name]['lat'].'</latitude>';
  $content .= '          <altitude>2000</altitude>';
  $content .= '          <heading>-9</heading>';
  $content .= '          <tilt>75</tilt>';
  $content .= '          <range>10000</range>';
  $content .= '          <gx:altitudeMode>relativeToGround</gx:altitudeMode>';
  $content .= '        </LookAt>';
  $content .= '      </gx:FlyTo>';

  $content .= '      <gx:AnimatedUpdate>';
  $content .= '        <Update>';
  $content .= '          <targetHref/>';
  $content .= '          <Change>';
  $content .= '            <Placemark targetId="placemark'.$k.'">';
  $content .= '              <gx:balloonVisibility>1</gx:balloonVisibility>';
  $content .= '            </Placemark>';
  $content .= '          </Change>';
  $content .= '        </Update>';
  $content .= '      </gx:AnimatedUpdate>';

  $content .= '      <gx:Wait>';
  $content .= '        <gx:duration>9.0</gx:duration>';
  $content .= '      </gx:Wait>';

  $content .= '      <gx:AnimatedUpdate>';
  $content .= '        <Update>';
  $content .= '          <targetHref/>';
  $content .= '          <Change>';
  $content .= '            <Placemark targetId="placemark'.$k.'">';
  $content .= '              <gx:balloonVisibility>0</gx:balloonVisibility>';
  $content .= '            </Placemark>';
  $content .= '          </Change>';
  $content .= '        </Update>';
  $content .= '      </gx:AnimatedUpdate>';
}
$content .= '    </gx:Playlist>';
$content .= '  </gx:Tour>';

foreach($correspondents as $k => $c) {
  $content .= '  <Placemark id="placemark'.$k.'">';
  $content .= '    <name>'.ucwords($c->name).'</name>';
  $content .= '    <open>0</open>';
  $content .= '    <description>';
  $content .= '      &lt;img src="'.$c->profile_image_url.'" /&gt;';
  $content .= '      &lt;br /&gt;&lt;br /&gt;';
  $content .= '      &lt;b&gt;Tweet: &lt;/b&gt;'.htmlspecialchars(strip_tags($c->status->text));
  $content .= '      &lt;br /&gt;&lt;br /&gt;';
  $content .= '      &lt;b&gt;Sent: &lt;/b&gt;'.$c->status->created_at;
  $content .= '      &lt;br /&gt;&lt;br /&gt;';
  $content .= '      &lt;b&gt;Location: &lt;/b&gt;'.$locations[$c->screen_name]['location'];
  $content .= '    </description>';
  $content .= '    <address>'.$locations[$c->screen_name]['location'].'</address>';
  $content .= '    <styleUrl>#barStyle</styleUrl>';
  $content .= '  </Placemark>';
}
$content .= '  </Document>';
$content .= '</kml>';

fwrite($kml, $content);
fclose($kml);

t

All that is left is some flair to the tour. The final code I made included a stating point and flying out to space and back between users to give an overview, but it’s just a sea of code which I’m not going to include. Google Earth has some great additional features you can also include, images and music loading during a tour (Flight of the Valkyries springs to mind). You can also see mappings of the sea, star constellations and the moon. For more on how to construct tours checkout Google’s Touring in KML.

Hope you have enjoyed the madness of this project. Let me know in the comments below, what you would do with a massive monitor array?

About This Post

Leave a Reply