Charl van Niekerk » Blog

Main

Latest

Archives

Powered by Blogger

George Geek Breakfast June 2009

The next George Geek Breakfast is as follows:

Hope to see you there!

Tags: geekbreakfast and georgegeekbreakfast.

hCalendar

Nokogiri on Ubuntu Hardy

I had some trouble installing the Nokogiri gem at first but succeeded after installing the following Ubuntu packages in 8.04 (Hardy):

Then I could successfully:

sudo gem install nokogiri

Please comment if you know of more "dependencies"!

Nokogiri on Heroku

Since Nokogiri is not yet available by default on Heroku, you will have to add it to your Gem manifest.

So, in the root of my application, I created a file called .gem that contains just the text nokogiri. That's it. Then git add .gem and git commit -m "Added gem manifest." or whatever and git push heroku master.

I have a live example up that reads and displays some of my contact information.

Thanks very much to Scott from Frogmetrics for the tip last evening!

OpenSolaris 2008.11

OpenSolaris

So I decided to give the OpenSolaris Live CD a spin and was quite surprised. I found myself in a very nicely skinned GNOME desktop environment. I was especially surprised to find BASH after opening up Terminal.

And it was fast!

Muti on Rails

Here is a simple example of how to syndicate your Muti links into Ruby on Rails.

Firstly, I created a new controller simply called muti. Here is my app/controllers/muti_controller.rb:

class MutiController < ApplicationController
  def index
    response = Net::HTTP.get_response("muti.co.za", "/by?name=charlvn&output=xml")
    @links = REXML::Document.new response.body
  end
end

Then, I created my view in app/views/muti/index.html.erb:

<% content_for :head do %>
  <title>Charl van Niekerk on Rails: Muti</title>
<% end %>

<h1>Muti</h1>
<ul>
  <% @links.elements.each("links/link") do |link| %>
    <li>
      <a href="<%= escape_once link.elements["url"].text %>">
        <%= escape_once link.elements["title"].text %>
      </a>
    </li>
  <% end %>
</ul>

For easy reference, I used the following layout in app/views/layouts/application.html.erb:

<!DOCTYPE HTML>
<html lang="en">
  <head>
    <%= yield :head %>
    <%= stylesheet_link_tag "main" %>
  </head>
  <body>
    <%= yield %>
  </body>
</html>

My main stylesheet (so far) in public/stylesheets/main.css:

@import url(screen.css) screen, projection;

My screen stylesheet in public/stylesheets/screen.css:

html {
  background: #555;
}
body {
  margin: 20px auto;
  padding: 20px;
  border: 2px solid black;
  width: 500px;
  font: 0.8em / 1.6 verdana, arial, sans-serif;
  background: white;
}
h1 {
  margin: 0;
  padding: 0;
  font-weight: normal;
  font-size: 2em;
  text-align: center;
  letter-spacing: 1px;
}
:link {
  color: #0652ff;
}
:visited {
  color: #0637a6;
}
:link:hover, :visited:hover {
  color: #067bff;
}

I have a live example running on Heroku and the complete source code is on GitHub.

Zoopy Media Item Metadata and PHP

Here is a simple method I just cooked up:

/**
 * Retrieves a Zoopy media item's metadata given the media item URI.
 *
 * @author    Charl van Niekerk <charlvn@charlvn.com>
 * @copyright Copyleft 2009 Charl van Niekerk.
 * @license   http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
 * @param  string $username A valid Zoopy account username.
 * @param  string $password The password corresponding to the username given.
 * @param  string $uri      The URI of a media item page.
 * @return mixed  An array containing the data if successful, false otherwise.
 */
function getZoopyMediaMeta($username, $password, $uri)
{
 // Encode the username and password for injection into an URI.
 $username = rawurlencode($username);
 $password = rawurlencode($password);

 // Define the perl regular expression pattern to use.
 $pattern = '/http\:\/\/(www\.)?zoopy\.com\/[a-z]+\/([a-z0-9]+)\//';

 // Run the regular expression on the URI and retrieve the matches.
 preg_match($pattern, $uri, $matches);

 // If no successful match has been made, return false.
 if (!isset($matches[2])) {
  return false;
 }

 // Convert the base-36 media identifier into base-10.
 $id = base_convert($matches[2], 36, 10);

 // Construct the API URI from its various parts.
 $apiUri = "http://$username:$password@api.zoopy.com/rest/media/$id.json";

 // Fetch the JSON data from the API URI.
 $json = @file_get_contents($apiUri);

 // If no data was returned, return false.
 if (!$json) {
  return false;
 }

 // Decode the JSON data into a PHP array.
 $data = json_decode($json, true);

 // Return the data in the array.
 return $data;
}

Here is an example of use:

// Define your parameters.
$username = 'charlvn';
$password = 'mypassword';
$uri      = 'http://www.zoopy.com/photo/x57/writing-kubuntu-cd-in-ubuntu-studio';

// Perform the method call.
$meta = getZoopyMediaMeta($username, $password, $uri);

// Output the data in plain text format for debugging.
header('Content-Type: text/plain; charset=UTF-8');
print_r($meta);

Here is an example of output:

Array
(
    [result] => success
    [content] => Array
        (
            [media] => Array
                (
                    [uri] => http://api.zoopy.com/rest/media/42955.json
                    [type] => photo
                    [url] => http://www.zoopy.com/photo/x57/writing-kubuntu-cd-in-ubuntu-studio
                    [title] => Writing Kubuntu CD in Ubuntu Studio
                    [content] => <p></p>
                    [user] => Array
                        (
                            [uri] => http://api.zoopy.com/rest/user/2872.json
                            [username] => charlvn
                        )

                    [created] => Sun, 24 May 2009 12:03:37 +0200
                    [tags] => Array
                        (
                        )

                    [albums] => Array
                        (
                        )

                    [views] => 12
                    [license] => Array
                        (
                            [name] => © All rights reserved
                            [url] => 
                        )

                    [dimensions] => Array
                        (
                            [unit] => px
                            [width] => 800
                            [height] => 600
                        )

                    [oembed] => http://www.zoopy.com/media/oembed?url=http%3A%2F%2Fwww.zoopy.com%2Fphoto%2Fx57%2Fwriting-kubuntu-cd-in-ubuntu-studio&format=xml
                )

        )

)

Windows Live Web Activities

Windows Live Web Activities

I don't spend much time with Windows Live because, well, I'm not even a Windows user. I still have a couple of friends using Messenger only though so normally use Pidgin to connect and I still have an old @hotmail.com address which is now about 11 years old but I no longer use either.

They seem to now have started doing integration with the social services out there. This is a very interesting move in my opinion. Seems like they also got on the "social" bandwagon and since they could not buy out Facebook they now just decided to integrate with Facebook. But the fact that they are actually going to some trouble to integrate instead of their usual "assimilation" tactics is just more evidence of how they are struggling to stay ahead.

Asynchronous JSON Transformation

This uses HTML 5, JSON2, JsonT and XMLHttpRequest.

The HTML:

<!DOCTYPE HTML>
<html lang="en">
 <head>
  <title>Asynchronous JSON Transformation</title>
  <script type="application/javascript" src="json2.js"></script>
  <script type="application/javascript" src="jsont.js"></script>
  <script type="application/javascript" src="loader.js"></script>
 </head>
 <body>
  <h1>Asynchronous JSON Transformation</h1>
  <div id="content"></div>
 </body>
</html>

loader.js:

function loader() {
 var data = null;
 var templates = null;
 var getJSON = function(uri, callback) {
  var xhr = new XMLHttpRequest();
  xhr.onreadystatechange = function() {
   if (this.readyState == 4 && this.status == 200) {
    callback(JSON.parse(this.responseText));
   }
  }
  xhr.open("GET", uri);
  xhr.send(null);
 }
 var render = function() {
  if (data != null && templates != null) {
   for (var id in data) {
    var html = "";
    for (var item in data[id]) {
     html += jsonT(data[id][item], templates);
    }
    document.getElementById(id).innerHTML = html;
   }
  }
 }
 var setData = function(value) {
  data = value;
  render();
 }
 var setTemplates = function(value) {
  templates = value;
  render();
 }
 getJSON("data.json", setData);
 getJSON("templates.json", setTemplates);
}
window.addEventListener("load", loader, false);

data.json:

{
 "content": [
  {"paragraph": "This is the first paragraph."},
  {"paragraph": "This is the second paragraph."}
 ]
}

templates.json:

{
 "paragraph": "<p>{$}</p>",
 "link"     : "<a href=\"{link.uri}\">{link.text}</a>"
}

You might end up with something like this:

<div id="content"><p>This is the first paragraph.</p><p>This is the second paragraph.</p></div>

Heroku App Names

If you use the heroku create command without specifying an application name, you might end up with something like radiant-dawn-37 (like I did). You can easily specify a name like heroku create charlvn. However, if you want to change the name of your application, it might be easier to do this using the heroku tool than using the web interface, to make sure that your git repository knows where to push heroku to:

heroku rename charlvn

This will rename your app from whatever it was previously to charlvn and should therefore now be accessible as http://charlvn.heroku.com/.

Gaborone to George

One of the things I like to make fun of is the inefficient routing in Africa. As I am writing this, I am sitting at the Equatorial Cafe in Gaborone on wifi and am doing a traceroute back to my server at my house in George, South Africa. Since Gaborone is in Botswana and Botswana is a neighboring country of South Africa, one would think that we would not need to get routed via London. But apparently my assumption is wrong.

charlvn@charlvn-laptop:~$ tcptraceroute aurora.charlvn.za.net ssh
Selected device eth1, address 10.5.50.122, port 57755 for outgoing packets
Tracing the path to aurora.charlvn.za.net (196.209.28.171) on TCP port 22 (ssh), 30 hops max
 1  10.10.10.1  652.720 ms  283.139 ms  6.840 ms
 2  192.168.1.254  89.593 ms  99.729 ms  102.345 ms
 3  168.167.152.1  628.219 ms * 964.222 ms
 4  168.167.254.94  134.104 ms  15.706 ms  13.996 ms
 5  168.167.253.12  15.849 ms  142.637 ms  286.772 ms
 6  t2a1-p3-0.nl-ams2.eu.bt.net (166.49.210.125)  349.855 ms  327.303 ms  327.509 ms
 7  t2a7-prc2.nl-ams2.eu.bt.net (166.49.200.33)  330.756 ms  356.087 ms  333.934 ms
 8  * * *
 9  p64-2-0-0.r23.londen03.uk.bb.gin.ntt.net (129.250.4.104)  388.437 ms  283.480 ms  310.293 ms
10  * * *
11  dimensiondata-0.r00.londen03.uk.bb.gin.ntt.net (83.231.181.234)  299.302 ms  296.214 ms  285.955 ms
12  core2a-dock-gi1-0-5.ip.isnet.net (168.209.246.4)  286.272 ms  281.695 ms  281.891 ms
13  csw2-dock-vl8.ip.isnet.net (168.209.246.76)  287.944 ms  285.197 ms  278.662 ms
14  core1b-kdp-po5-0.ip.isnet.net (196.34.54.222)  620.487 ms  576.989 ms  587.422 ms
15  core2b-rba-gi1-0-0-54.ip.isnet.net (168.209.0.197)  589.486 ms  640.430 ms *
16  cdsl2-rba-gi4-0-0.ip.isnet.net (196.26.0.218)  593.968 ms  668.884 ms  660.316 ms
17  * cdsl1-rba-vl2.ip.isnet.net (196.26.77.1) 569.262 ms  604.456 ms
18  196.38.72.222  639.032 ms  614.532 ms  582.900 ms
19  cdsl1-rba-vl82.ip.isnet.net (196.38.72.225)  566.432 ms  578.924 ms  591.994 ms
20  cdsl1-rba-vl2459-ipc.ip.isnet.net (196.38.72.214)  584.750 ms  595.347 ms  630.755 ms
21  196-209-0-1-esdw-esr-3.dynamic.isadsl.co.za (196.209.0.1)  656.607 ms  663.928 ms  629.803 ms
22  * * *
23  196-209-28-171-esdw-esr-3.dynamic.isadsl.co.za (196.209.28.171) [open]  628.088 ms * 775.036 ms

A traceroute in the opposite direction, from my server back to the IP that it sees connecting to it, is very weird:

charlvn@aurora:~$ tcptraceroute 168.167.157.102
Selected device eth0, address 10.0.0.101, port 41360 for outgoing packets
Tracing the path to 168.167.157.102 on TCP port 80 (www), 30 hops max
 1  10.0.0.1  1.259 ms  0.984 ms  0.570 ms
 2  * * *
 3  cdsl1-rba-vl2360.ip.isnet.net (196.38.73.133)  108.672 ms  63.441 ms  66.078 ms
 4  196.38.73.114  66.204 ms  64.741 ms  67.047 ms
 5  cdsl1-rba-vl50.ip.isnet.net (196.38.73.109)  65.239 ms  62.543 ms  62.835 ms
 6  core5a-rba-gi0-0-0.ip.isnet.net (196.26.0.42)  62.732 ms  64.449 ms  62.841 ms
 7  196.26.0.10  63.755 ms  62.430 ms  79.073 ms
 8  rrba-ip-spe-2-wan.telkom-ipnet.co.za (196.25.127.181)  62.662 ms  65.541 ms  65.935 ms
 9  196.43.25.137  75.916 ms  73.317 ms  72.358 ms
10  196.43.39.146  73.718 ms  63.515 ms  75.808 ms
11  botswana-telecom-gw.telkom-ipnet.co.za (196.25.0.90)  81.328 ms  89.779 ms  84.686 ms
12  168.167.253.1  613.723 ms  618.718 ms  631.147 ms
13  * * *
14  * * *
15  * * *
16  * * *
17  * * *
18  * * *
19  * * *
20  * * *
21  * * *
22  * * *
23  * * *
24  * * *
25  * * *
26  * * *
27  * * *
28  * * *
29  * * *
30  * * *
Destination not reached

PHP Interactive Mode

Not too many developers seem to know about or use this, but often I find it very handy to use PHP in interactive command-line mode, especially while testing some new methods I just created.

charlvn@sunbird:~$ php -a
Interactive shell

php > print 'Hello World!';
Hello World!
php >

There is also a command history, similar as in bash, saved in your home directory as ~/.php_history.

Note that in Ubuntu you need to have the php5-cli package installed.

Movie Update

I haven't been to the movies in quite a while so finally decided that it's about time I catch up.

I enjoyed the original National Treasure so rented the sequel National Treasure: Book of Secrets. Definitely was not a let-down.

I also rented The Mummy: Tomb of the Dragon Emperor. I liked the previous two and could also recommend this one.

Then I rented WALL-E. It's amazing to see how Pixar has been progressing over the years and the graphics were certainly impressive but I didn't really find a great storyline in between.

Then the other evening I went to see the latest Star Trek at the theatre. Highly recommended IMHO; probably the best Star Trek film I saw yet.

Ubuntu 9.04 (Jaunty) + OpenBSD 4.5 Dual Boot

After a whole lot of Googling, reading forum and blog posts and then adjusting the given solutions, I managed to figure out what to put into my /boot/grub/menu.lst:

title OpenBSD
root (hd2,3)
makeactive
chainloader +1

So basically, this box has a whole lot of IDE drives in. I have Ubuntu sitting on my primary master (hd0). My primary slave (hd1) is currently being used as a data drive. My secondary master (hd2) is the drive I have dedicated to use OpenBSD on.

MySQL Table Structure Dump in HTML 5

I wanted a customised MySQL database structure dump but not the 90's-style default output of mysql -H so I wrote a quick script to base it on.

Note that I only wanted certain columns of the DESC output too.

<?php
 header('Content-Type: text/html; charset=UTF-8');
 $db = new mysqli('localhost', 'myusername', 'mypassword', 'mydatabase');
 $db->set_charset('utf8');
 $tables = array_map('rtrim', file('tables.txt'));
?>
<!DOCTYPE HTML>
<html lang="en">
 <head>
  <title>Database Structure</title>
  <style type="text/css">
   body {
    margin: 0;
    padding: 0;
    font: 0.8em verdana, arial, sans-serif;
   }
   h1 {
    margin: 0;
    padding: 20px;
    border-bottom: 1px solid #999;
    font-size: 2em;
    font-weight: normal;
    text-align: center;
    text-transform: uppercase;
    letter-spacing: 3px;
    background: #eee;
   }
   h2 {
    margin: 30px auto 0 auto;
    padding: 8px 22px;
    width: 200px;
    font-size: 1em;
    text-align: center;
    text-transform: uppercase;
    letter-spacing: 3px;
    color: white;
    background: #555;
   }
   ul {
    margin: 0 auto 30px auto;
    padding: 20px;
    border: 2px solid black;
    width: 200px;
    list-style: none;
    line-height: 1.6;
   }
   table {
    margin: 0 auto 30px auto;
    border: 2px solid black;
    border-collapse: collapse;
   }
   caption {
    margin: 0 -1px;
    padding: 8px;
    font-weight: bold;
    text-transform: uppercase;
    letter-spacing: 3px;
    color: white;
    background: #555;
   }
   th, td {
    padding: 5px 10px;
    text-align: left;
   }
  </style>
 </head>
 <body>
  <h1>Database Structure</h1>
  <nav>
   <h2>Table Index</h1>
   <ul>
<?php foreach ($tables as $table) : ?>
    <li><a href="#<?php echo htmlspecialchars($table); ?>"><?php echo htmlspecialchars($table); ?></a></li>
<?php endforeach; ?>
   </ul>
  </nav>
<?php foreach ($tables as $table) : ?>
<?php $desc = $db->query(sprintf('DESC `%s`', $db->escape_string($table))); ?>
  <table id="<?php echo htmlspecialchars($table); ?>">
   <caption><?php echo htmlspecialchars($table); ?></caption>
   <thead>
    <tr>
     <th>Field</th>
     <th>Type</th>
     <th>Null</th>
    </tr>
   </thead>
   <tbody>
<?php while ($row = $desc->fetch_object()) : ?>
    <tr>
     <td><?php echo htmlspecialchars($row->Field); ?></td>
     <td><?php echo htmlspecialchars($row->Type); ?></td>
     <td><?php echo htmlspecialchars($row->Null); ?></td>
    </tr>
<?php endwhile; ?>
   </tbody>
  </table>
<?php endforeach; ?>
 </body>
</html>

I'm combining the logic, configuration and the template which is not standard practice, but no need to make something simple overly complex.

May Coffee Update

AJ and Paul both commented on my last post suggesting I try Kenna. I missed this previously as it was on the bottom shelf but they do actually stock both the Everyday Blend and the Private Blend at the Spar next to the Virgin Active in George. I could not find the highly recommended Master Blend there unfortunately but managed to lay my hands on the last packet at the Pick 'n Pay at the Garden Route Mall.

I decided to try each of the different kinds, including the Everyday Blend. This one unfortunately has some chicory in it (25%) which I think spoiled the taste, but the aroma was very good so am definitely looking forward to trying both the Private Blend and the Master Blend (especially the latter).

Then I tried the Spar Wiener Mischung. It's a medium roast and personally I prefer a dark roast, but despite this it was actually very, very, very nice!

Firefox 3.5 CSS 3 nth-child support

As documented, Firefox 3.5 now supports the nth-child pseudo-class.

Here is an example page I set up:

<!DOCTYPE HTML>
<html lang="en">
 <head>
  <title>Firefox 3.5 nth-child Demo</title>
  <style type="text/css">
   body {
    font: 0.8em verdana, arial, sans-serif;
   }
   table {
    border: 2px solid black;
    border-collapse: collapse;
   }
   caption {
    margin: 0 -1px;
    padding: 8px;
    font-weight: bold;
    text-transform: uppercase;
    letter-spacing: 3px;
    color: white;
    background: #555;
   }
   th, td {
    padding: 5px 10px;
    text-align: left;
   }
   tbody tr:nth-child(odd) {
    background: #eee;
   }
  </style>
 </head>
 <body>
  <table>
   <caption>Microprocessors</caption>
   <thead>
    <tr>
     <th>Make</th>
     <th>Model</th>
     <th>Clockspeed</th>
     <th>Cache</th>
    </tr>
   </thead>
   <tbody>
    <tr>
     <td>Intel</td>
     <td>Celeron (Coppermine)</td>
     <td>949.883 MHz</td>
     <td>128 KB</td>
    </tr>
    <tr>
     <td>Intel</td>
     <td>Pentium III (Katmai)</td>
     <td>449.958 MHz</td>
     <td>512 KB</td>
    </tr>
    <tr>
     <td>Intel</td>
     <td>Celeron 2.66GHz</td>
     <td>2653.875 MHz</td>
     <td>256 KB</td>
    </tr>
    <tr>
     <td>Intel</td>
     <td>Pentium 4 3.00GHz</td>
     <td>2992.770 MHz</td>
     <td>2048 KB</td>
    </tr>
    <tr>
     <td>Intel</td>
     <td>Pentium 4 3.00GHz</td>
     <td>2992.602 MHz</td>
     <td>1024 KB</td>
    </tr>
   </tbody>
  </table>
 </body>
</html>

Here is how it looks in 3.5:

CSS nth-child in Firefox 3.5

The markup validates but when I try to validate the CSS using the W3C CSS Validator I get the following error:

CSS 3 nth-child

Since it is in the current Selectors Level 3 working draft, I guess the validator has not been updated yet.

Firefox 3.5 (Afrikaans)

Well done to the Mozilla and Translate.org.za teams - Firefox 3.5 is looking awesome!

Google Charts PHP Helper Function v2

I don't know why I didn't see this earlier, but after taking another look at the docs you can let Google do the data scaling for you by using the chds parameter. Here is the revised function, a bit simplified.

/**
 * Generates the URI to a Google Chart from the data provided.
 *
 * @author    Charl van Niekerk <charlvn@charlvn.com>
 * @copyright Copyleft 2009 Charl van Niekerk.
 * @license   http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
 * @param  array   $values  The values to be displayed on the chart.
 * @param  mixed   $default The default value to assume for missing keys.
 * @param  integer $width   The width of the chart in pixels.
 * @param  integer $height  The height of the chart in pixels.
 * @param  string  $type    The type of the chart.
 * @return string  The URI of the chart.
 */
function getGoogleChartUri($values, $default=0, $width=800, $height=120, $type='lc')
{
    // Determine the smallest and largest key.
    $keys   = array_keys($values);
    $minKey = min($keys);
    $maxKey = max($keys);

    // Determine the smallest and largest value.
    $minValue = min($values);
    $maxValue = max($values);

    // Fill in the blanks.
    for ($i = $minKey + 1; $i < $maxKey; $i++) {
        if (!array_key_exists($i, $values)) {
            $values[$i] = $default;
        }
    }

    // Sort the values according to their corresponding keys.
    ksort($values);

    // Generate the Google Chart API query.
    $params = array();
    $params['cht']  = $type;
    $params['chs']  = $width . 'x' . $height;
    $params['chxt'] = 'x,y';
    $params['chxr'] = "0,$minKey,$maxKey|1,$minValue,$maxValue";
    $params['chd']  = 't:' . implode(',', $values);
    $params['chds'] = "$minValue,$maxValue";
    $query = http_build_query($params);

    // Generate the Google Chart image URI.
    $uri = "http://chart.apis.google.com/chart?$query";

    return $uri;
}

Google Charts PHP Helper Function

I made the following helper function for a little mashup I am working on. Consider this pre-alpha.

/**
 * Generates the URI to a Google Chart from the data provided.
 *
 * @author    Charl van Niekerk <charlvn@charlvn.com>
 * @copyright Copyleft 2009 Charl van Niekerk.
 * @license   http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
 * @param  array   $values  The values to be displayed on the chart.
 * @param  mixed   $default The default value to assume for missing keys.
 * @param  integer $width   The width of the chart in pixels.
 * @param  integer $height  The height of the chart in pixels.
 * @param  string  $type    The type of the chart.
 * @return string  The URI of the chart.
 */
function getGoogleChartUri($values, $default=0, $width=800, $height=120, $type='lc')
{
    // Determine the smallest and largest key.
    $keys   = array_keys($values);
    $minKey = min($keys);
    $maxKey = max($keys);

    // Determine the smallest and largest value and the range between them.
    $minValue   = min($values);
    $maxValue   = max($values);
    $valueRange = $maxValue - $minValue;

    // Fill in the blanks.
    for ($i = $minKey + 1; $i < $maxKey; $i++) {
        if (!array_key_exists($i, $values)) {
            $values[$i] = $default;
        }
    }

    // Sort the values according to their corresponding keys.
    ksort($values);

    // Calculate the values as percentages.
    $percentages = array();
    foreach ($values as $key => $value) {
        $percentages[$key] = ($value - $minValue) / $valueRange * 100;
    }

    // Generate the Google Chart API query.
    $params = array();
    $params['cht']  = $type;
    $params['chs']  = $width . 'x' . $height;
    $params['chxt'] = 'x,y';
    $params['chxr'] = "0,$minKey,$maxKey|1,$minValue,$maxValue";
    $params['chd']  = 't:' . implode(',', $percentages);
    $query = http_build_query($params);

    // Generate the Google Chart image URI.
    $uri = "http://chart.apis.google.com/chart?$query";

    return $uri;
}

You could use it as follows:

<?php
 $values = array(1 => -30, 3 => 40, 4 => 20, 6 => 80);
 $uri = getGoogleChartUri($values, 0, 400);
?>
<img src="<?php echo htmlspecialchars($uri); ?>">

This might look something like the following:

Cached Syndication

I am busy shutting down the old charlvn.za.net for migrating over to www.charlvn.com.

One thing people asked me about is how I put together the home page with the syndication as well as the automatically updated social graph page. Actually, this is a really ugly hack, but I will give it to you for interest sake.

Firstly, I had the following script called update-index.php:

<?php
 error_reporting(E_NONE);
 function fetchXml($path) {
  try {
   $xml = new SimpleXMLElement(file_get_contents($path));
  } catch (Exception $e) {
   $xml = null;
  }
  return $xml;
 }
 $blog = fetchXml('http://blog.charlvn.za.net/feeds/posts/default?max-results=10');
 $twitter = fetchXml('http://twitter.com/statuses/user_timeline/charlvn.xml?count=10');
 $muti = fetchXml('http://muti.co.za/api/userinfo/charlvn');
 header('Content-Type: text/html; charset=UTF-8');
?>
<!DOCTYPE HTML>
<html lang="en">
 <head>
  <title>Charl van Niekerk</title>
  <meta name="microid" content="mailto+http:sha1:4009296a58052132ba5f2f3c101a64e2f0476803">
  <link rel="stylesheet" type="text/css" href="/stylesheets/index">
  <link rel="meta" type="application/rdf+xml" title="FOAF" href="/foaf">
  <link rel="openid.server" href="http://openid.claimid.com/server">
  <link rel="openid.delegate" href="http://openid.claimid.com/charlvn">
 </head>
 <body>
  <ul>
   <li><a href="/">Home</a></li>
   <li><a href="/socialgraph">Socialgraph</a></li>
   <li><a href="/contact">Contact</a></li>
   <li><a href="/about">About</a></li>
  </ul>
  <h1><img src="/images/logo-big" alt="charlvn.za.net"></h1>
  <div id="contacts">
   <h2>Contacts</h2>
   <ul>
    <li><a href="http://belinda.za.net/" rel="contact">Belinda</a></li>
    <li><a href="http://daveduarte.co.za/" rel="contact">Dave</a></li>
    <li><a href="http://jayx.co.za/" rel="contact">Jayx</a></li>
    <li><a href="http://jpgeek.blogspot.com/" rel="contact">Jean-Paul</a></li>
    <li><a href="http://krijnhoetmer.nl/" rel="contact">Krijn</a></li>
    <li><a href="http://lachy.id.au/" rel="contact">Lachlan</a></li>
    <li><a href="http://maxkaizen.com/" rel="contact">Max</a></li>
    <li><a href="http://nxsy.org/" rel="contact">Neil</a></li>
    <li><a href="http://twitter.com/thakadu" rel="contact">Neville</a></li>
    <li><a href="http://rafiq.co.za/" rel="contact">Rafiq</a></li>
    <li><a href="http://russell.rucus.net/" rel="contact">Russell</a></li>
    <li><a href="http://stii.za.net/" rel="contact">Stii</a></li>
   </ul>
  </div>
  <div id="blog">
   <h2>Blog</h2>
   <ul>
<?php foreach ($blog->entry as $entry): ?>
    <li><a href="<?php echo $entry->link[4]['href']; ?>"><?php echo htmlspecialchars($entry->title); ?></a></li>
<?php endforeach; ?>
   </ul>
  </div>
  <div id="twitter">
   <h2>Twitter</h2>
   <ul>
<?php foreach ($twitter->status as $status): ?>
    <li><a href="http://twitter.com/charlvn/statuses/<?php echo $status->id; ?>"><?php echo $status->text; ?></a></li>
<?php endforeach; ?>
   </ul>
  </div>
  <div id="muti">
   <h2>Muti</h2>
   <ul>
<?php foreach ($muti->link as $link): ?>
    <li><a href="<?php echo htmlspecialchars($link->url); ?>"><?php echo htmlspecialchars($link->title); ?></a></li>
<?php endforeach; ?>
   </ul>
  </div>
  <p id="footer">&copy; 2009 Charl van Niekerk. All rights reserved.</p>
 </body>
</html>

I also had another script called update-socialgraph.php:

<?php
 $json = @file_get_contents('http://socialgraph.apis.google.com/lookup?q=charlvn.za.net%2Cblog.charlvn.za.net&edi=1');
 $data = @json_decode($json, true);
 $me = array();
 $other = array();
 if ($data) {
  foreach ($data['nodes'] as $node) {
   foreach ($node['nodes_referenced_by'] as $domain => $ref) {
    if (in_array('me', $ref['types'])) {
     $me[] = $domain;
    } else {
     $other[$domain] = implode(', ', $ref['types']);
    }
   }
  }
 }
 header('Content-Type: text/html; charset=UTF-8');
?>
<!DOCTYPE HTML>
<html lang="en">
 <head>
  <title>Charl van Niekerk: Socialgraph</title>
  <link rel="stylesheet" type="text/css" href="/stylesheets/index">
 </head>
 <body>
  <ul>
   <li><a href="/">Home</a></li>
   <li><a href="/socialgraph">Socialgraph</a></li>
   <li><a href="/contact">Contact</a></li>
   <li><a href="/about">About</a></li>
  </ul>
  <h1>Socialgraph</h1>
<?php if ($me): ?>
  <h2>Me</h2>
  <ul>
<?php foreach ($me as $domain): ?>
   <li><a href="<?php echo htmlspecialchars($domain); ?>" rel="nofollow"><?php echo htmlspecialchars($domain); ?></a></li>
<?php endforeach; ?>
  </ul>
<?php endif; ?>
<?php if ($other): ?>
  <h2>Other</h2>
  <ul>
<?php foreach ($other as $domain => $relationship): ?>
   <li><a href="<?php echo htmlspecialchars($domain); ?>" rel="nofollow"><?php echo htmlspecialchars($domain); ?></a> <?php echo htmlspecialchars($relationship); ?></li>
<?php endforeach; ?>
  </ul>
<?php endif; ?>
  <p id="footer">&copy; 2009 Charl van Niekerk. All rights reserved.</p>
 </body>
</html>

Then, I had a bash script at /home/charlvn/bash/feedupdate:

#!/bin/bash
lynx -source http://charlvn.za.net/update-index.php > /home/charlvn/public_html/index-temp.html
mv /home/charlvn/public_html/index-temp.html /home/charlvn/public_html/index.html
lynx -source http://charlvn.za.net/update-socialgraph.php > /home/charlvn/public_html/socialgraph-temp.html
mv /home/charlvn/public_html/socialgraph-temp.html /home/charlvn/public_html/socialgraph.html

Lastly, I had the following cron jobs configured:

00 * * * * /home/charlvn/bash/feedupdate
15 * * * * /home/charlvn/bash/feedupdate
30 * * * * /home/charlvn/bash/feedupdate
45 * * * * /home/charlvn/bash/feedupdate

I know... Quick, dirty, ugly. But there you have it. New site is gonna be much more elegant (when I am done with it of course). ;)

Some obvious questions and their answers:

Why use lynx and not wget or curl?
Because lynx is all the server allowed me to use.
Why not use PHP shell scripts?
Server did not support this.
Why not use Python or Ruby?
Server did not support this.
Why not take out the common template elements?
FFS - the site is only a few pages, why bother? :P

Any other questions, please fire away. If you say "this code blows" I would reply "why state the obvious?"

RSS -> Myqron -> Delicious

Here is a script you can use with Myqron to post the new items from your RSS feed to Delicious. You can use this to, for example, "upstream" your bookmarks posted to another service such as Muti over to Delicious.

<?php

/* === Config Section === */
$username = 'myusername';
$password = 'mypassword';
/* === End of Config === */

// Ensure only HTTP POST requests are allowed.
if ($_SERVER['REQUEST_METHOD'] != 'POST') {
 header('HTTP/1.0 501 Not Implemented');
 exit;
}

// Encode the username and password for insertion into an URI.
$username = rawurlencode($username);
$password = rawurlencode($password);

// Retrieve post data into local variable. PHP has an ugly way unfortunately.
$postdata = file_get_contents('php://input');

// Parse the post data into a SimpleXML DOM.
$items = new SimpleXMLElement($postdata);

// Loop through the new items.
foreach ($items->item as $item) {
 // Generate the query to be sent to the del.icio.us API.
 $params = array();
 $params['url'] = (string) $item->link;
 $params['description'] = (string) $item->title;
 $query = http_build_query($params);

 // Put all the pieces together into one URI string.
 $uri = "https://$username:$password@api.del.icio.us/v1/posts/add?$query";

 // Perform the actual HTTP request to the del.icio.us servers.
 file_get_contents($uri);
}

Firefox Editor HTML 5 Page

Strangely enough, I actually like to use Firefox as a code editor when it comes to HTML pages with lots of text. If code highlighting is not a big priority, but spell checking is, then I personally find it quite convenient to open a page like the following and just use the textbox to type in.

<!DOCTYPE HTML>
<meta charset="UTF-8">
<title>Editor</title>
<style type="text/css">
 html, body, div {
  height: 99%;
 }
 textarea {
  width: 100%;
  height: 100%;
  font: 1em "DejaVu Sans Mono";
  color: #eee;
  background: black;
 }
</style>
<div><textarea></textarea></div>

This might look minimalist, but is valid.

George GeekDinner May 2009: Rainy Radicchio

A quick reminder about the upcoming George GeekDinner:

Hope to see you there!

Tags: geekdinner and georgegeekdinner.

hCalendar

More Ubuntu Jaunty Screenshots

Ok, these are my last Ubuntu screenshots (for now). Now on to other stuff.

Muti RSS Feeds

A bit earlier this evening, we made a small change to the way Muti serves its RSS feeds. This will be unnoticeable to most; unless you are a developer this probably does not bother you.

Previously, the feeds were served as application/xml but this has now been changed to application/rss+xml.

Although the former is valid for all XML vocabularies, using the (well-supported) latter says the document is, more specifically, an RSS feed and can be treated as such.

If you happen to have any questions, please fire away. :)

Ubuntu 9.04 (Jaunty) GDM Themes

Ubuntu Studio Look Update

As I wrote about a while ago, I wanted to install the Ubuntu Studio look on a normal Ubuntu Desktop, simply because I like it more. I only wanted the themes, not the actual Ubuntu Studio packages, so I installed the ubuntustudio-look package which ended up messing me around.

Afterwards, I spoke to Russell Cloran online and he told me to check that I still have the ubuntu-desktop package installed, which indeed it turns out I did not. I didn't read the aptitude output properly (yeah, I know...) and it actually uninstalled the ubuntu-desktop package, which in turn uninstalled gdm.

But now, if I want to install the ubuntu-desktop package, apt-get wants to uninstall the ubuntustudio-look package:

$ sudo apt-get install ubuntu-desktop
Reading package lists... Done
Building dependency tree       
Reading state information... Done
The following packages were automatically installed and are no longer required:
  tango-icon-theme ubuntustudio-gdm-theme ubuntustudio-theme ubuntustudio-wallpapers
  ubuntustudio-icon-theme
Use 'apt-get autoremove' to remove them.
The following extra packages will be installed:
  fast-user-switch-applet ubuntu-sounds
Suggested packages:
  xnest
The following packages will be REMOVED:
  ubuntustudio-look ubuntustudio-sounds
The following NEW packages will be installed:
  fast-user-switch-applet ubuntu-desktop ubuntu-sounds
0 upgraded, 3 newly installed, 2 to remove and 4 not upgraded.
Need to get 2656kB of archives.
After this operation, 2048kB of additional disk space will be used.
Do you want to continue [Y/n]?

So then I thought I should see what it would take to install the ubuntustudio-desktop package:

$ sudo apt-get install ubuntustudio-desktop
Reading package lists... Done
Building dependency tree       
Reading state information... Done
The following extra packages will be installed:
  libgconfmm-2.6-1c2 libglademm-2.4-1c2a libpulse-mainloop-glib0 padevchooser paman paprefs
  pavucontrol pavumeter pulseaudio-module-zeroconf scim-modules-table scim-tables-additional
  tango-icon-theme-common ubuntustudio-default-settings ubuntustudio-menu ubuntustudio-screensaver
  usplash-theme-ubuntustudio
The following NEW packages will be installed:
  libgconfmm-2.6-1c2 libglademm-2.4-1c2a libpulse-mainloop-glib0 padevchooser paman paprefs
  pavucontrol pavumeter pulseaudio-module-zeroconf scim-modules-table scim-tables-additional
  tango-icon-theme-common ubuntustudio-default-settings ubuntustudio-desktop ubuntustudio-menu
  ubuntustudio-screensaver usplash-theme-ubuntustudio
0 upgraded, 17 newly installed, 0 to remove and 4 not upgraded.
Need to get 992kB of archives.
After this operation, 7836kB of additional disk space will be used.
Do you want to continue [Y/n]?

And now I just want to klap myself for not trying this earlier. :)

(Do note that I did get away with it the first time...)

Moonlight + Popfly

Popfly + Firefox + Moonlight + Xubuntu

I checked out Popfly and decided to try out Moonlight 2.0 preview 1. When I opened the create mashup page, I just got an empty dialogue window. Quite strange.

Oh well, I guess we will have to wait a while until it stabilises. Very cool that Novell took the initiative to create an open source alternative to Silverlight though.

Google Mashup Editor Screenshots

Since the service is closing down, I thought it would be a pity not to take screenshots for those that might be interested but don't have an invite. You can find many more on Google Image Search.

Myqron PHP Test Script

I wrote the following simple script to accept data posted from Myqron and push it into a file:

<?php

if ($_SERVER['REQUEST_METHOD'] != 'POST') {
 header('HTTP/1.0 501 Not Implemented');
 exit;
}

$postdata = file_get_contents('php://input');
file_put_contents('dump.txt', $postdata);

Then, in the same directory as the script:

  1. $ touch dump.txt
  2. $ chmod 666 dump.txt

Once something is pushed, you get an actual example of the data in the format you can expect. A nice starting point to building your app. :)

Google Mashup Editor

I got a beta invite to the Google Mashup Editor about a year ago. More recently I decided to give it another spin and found out that they decided to can the project in favour of App Engine.

Although I would also normally prefer using App Engine above the Mashup Editor, simply for its power, I do think that the Mashup Editor had some use in that it did not require the (development) user to have any programming skill. All you needed was to be able to write markup. I guess it didn't fair too well in Google's cost-versus-benefit analysis.

Here is a small example I made displaying the muti hot list from the RSS feed using pagination:

<gm:page title="Muti Hot List">
  <div class="gm-app-header">
    <h1>Muti Hot List</h1>
  </div>
  <gm:list template="tmpl" data="http://muti.co.za/hot?output=rss" pagesize="10"/>
  <gm:template id="tmpl">
    <ul>
      <li repeat="true"><gm:link ref="atom:link/@href" labelref="atom:title"/></li>
    </ul>
    <gm:pager/>
  </gm:template>
</gm:page>

You can see a live demo of this until they totally shut down the service.

Muti AppJet Mashup

I needed to learn AppJet so decided to write a mashup retrieving and displaying the muti hot list. Please find the code below:

/* appjet:version 0.1 */

import("lib-xmldom");

var xml = wget("http://muti.co.za/hot?output=xml", null, true);
var dom = new XMLDoc(xml.data);
var links = dom.selectNode("");

var ul = UL();

for (var i = 0; i < links.children.length; i++) {
    var link = links.children[i];
    if (link.nodeType == "ELEMENT") {
        var title = link.selectNodeText("title");
        var url = link.selectNodeText("url");
        ul.push(LI(A({href:url}, title)));
    }
}

page.setTitle("Muti Hot List");
print(H1(A({href:"http://muti.co.za/hot"}, "Muti Hot List")));
print(ul);

/* appjet:css */

html {
    background: #eee;
}

body {
    margin: 20px auto;
    padding: 20px;
    border: 2px solid black;
    max-width: 500px;
    font: 0.8em / 1.6 verdana, arial, sans-serif;
    background: white;
}

h1 {
    font-size: 2em;
    font-weight: normal;
    text-align: center;
}

h1 :link, h1 :visited, h1 :link:hover, h1 :visited:hover {
    color: inherit;
    text-decoration: inherit;
}

ul {
    margin: 20px 30px 40px 30px;
    padding: 0;
}

:link {
        color: #0652ff;
}

:visited {
        color: #0637a6;
}

:link:hover, :visited:hover {
        color: #067bff;
}

Automatic Ranking of Microblog Posts

I just read quite an interesting post on Allan Kent's blog titled Refining twitter search. Here are two quotes from the post:

Our initial thoughts were around followers - the more followers I have, the more authority I should have. But that determines popularity. And after sitting through a year with Ms Popular in my Std 8 maths class, I can say with a high level of certainty that popularity does not automatically mean authority. I suspect though, that she was somewhat more authoritative after her second go at Std 8 maths.

[...]

Since I can spout any nonsense on Twitter that springs to mind (and I often do), we need a system that assigns authority based on what is said, not by who says it. There are already 2 ways to monitor this: favorites and re-tweeting. Both of these provide a quantitative way of gauging the popularity of an individual statement without being based on the popularity of the person making the statement. Of course popularity will always come into it - the more followers you have, the more opportunity you have for being favorited and re-tweeted. But hopefully it would create a level of relevance that would otherwise be missing.

Personally, I think this should be implemented as follows.

Each Twitter feed should have a general rating. Each incoming subscription increases the rating by a certain amount of points. The exact amount will be influenced by the rating of the subscriber. The ratio of incoming versus outgoing subscriptions may be taken into account as well as the frequency of posting.

Each individual post should also have its own rating. The rating can be determined by:

Each one of these can have a certain weighting. For example, a post being favourited by an individual might mean more than a post that is reposted by the same individual, or however you want to look at it. This is probably the most difficult part: to figure out which weights to apply. I don't think there really is a way to mathematically calculate the precise values; perhaps it's just one of those things that you need to "feel out" and refine as you go along, partly by trial and error. Perhaps there is some statistical way of measuring the performance of a particular combination of weights by measuring user satisfaction over the longer term.

Perhaps even a service like Amplify can be used in some way, for example to look at responses and try to judge whether they agree or disagree with the original post, although this might be quite tricky to do in reality. Some other ideas might include to use Akismet to help fight abuse.

Any thoughts on the above?

Zoopy HTML Image List PHP Generator

Sorry for the excessively long title, but what else am I supposed to call this? :)

I have a little script to pull down the RSS feed from my Zoopy profile and then generate an HTML list of thumbnails so that I can embed it into my blog posts (etc). For example, see my post Kongoni Aristotle Screenshots.

Note that this will generate an HTML list, not XHTML due to the shorttag issue with the img elements. Because XHTML is for noobs (just kidding).

<?php                                                           

// Set the URI of the RSS feed to fetch.
$rssUri = 'http://www.zoopy.com/search/rss/hoi';

// Load the items from the RSS feed.
$rss = new DOMDocument();           
$rss->load($rssUri);
$items = $rss->getElementsByTagName('item');      

// Create the HTML document and unordered list element.
$html = new DOMDocument('1.0', 'UTF-8');               
$ul = $html->createElement('ul');                      
$html->appendChild($ul);                               

// Extract the data from the RSS feed and insert into the unordered list.
for ($i = 0; $i < $items->length; $i++) {
 // Retrieve the current RSS item and extract the data from it into local variables.
 $item = $items->item($i);
 $title = $item->getElementsByTagName('title')->item(0)->textContent;
 $link = $item->getElementsByTagName('link')->item(0)->textContent;
 $image = $item->getElementsByTagName('content')->item(0)->getAttribute('url');
 preg_match_all('#/([0-9]+)/thumb#i', $image, $matches);
 $image = 'http://www.zoopy.com/data/media/' . $matches[1][0] . '/thumb-150x150f.jpg';

 // Generate the image element.
 $img = $html->createElement('img');
 $img->setAttribute('src', $image);
 $img->setAttribute('alt', $title);
 $img->setAttribute('title', $title);

 // Generate the anchor element and add the image element to it.
 $a = $html->createElement('a');
 $a->setAttribute('href', $link);
 $a->appendChild($img);

 // Generate the list item element and append it to the unordered list.
 $li = $html->createElement('li');
 $li->appendChild($a);
 $ul->appendChild($li);
}

// Serialise the DOM to HTML and output as such.
header('Content-Type: text/html; charset=UTF-8');
echo $html->saveHTML();

Ja, I probably should not be sending this as text/html as the output is not a valid HTML document (what about a title?). Whatever. :P

Floss.pro

FlossProFox

Been using Floss.pro the last few days instead of Twitter. Very nice indeed! It is a Laconi.ca install and has integration with Facebook, Twitter, XMPP (Jabber / Google Talk) and now even Firefox in the form of an add-on. You can follow me at http://floss.pro/charlvn.

Excellent work Karl!

Copyright © 2004-2009 Charl van Niekerk. All articles are released under the Creative Commons Attribution 2.5 South Africa licence, unless where otherwise stated.