Public transport probability

From Geohashing

This program uses OpenStreetMap data to calculate the chance of a hashpoint lying within X kilometres of a train station or bus stop. Bear in mind that it uses OSM data which is often incomplete.

Usage

First, get the OpenStreetMap extract for your graticule (replace 52,13 below with your own graticule).

$ wget 'http://www.informationfreeway.org/api/0.6/node[bbox=13,52,14,53][railway=station|stop|halt|tram_stop]'
$ wget 'http://www.informationfreeway.org/api/0.6/node[bbox=13,52,14,53][highway=bus_stop]'

The first command retrieves all stations and on-request halts. Usually it includes surface rail, underground services and trams. The second retrieves all bus stops.

Next, create a PHP file with the code below, editing the first few lines as appropriate. Run it to create an image file and to output the probabilities. For the first run, I suggest making the $distance very small so you can check whether the OSM data looks complete.

NB: Untested on negative graticules, let me know if you have problems!

Example

Public transport sample.gif

For the Berlin, Germany (52,13) graticule:

david@netman1:~/geohashing/berlin$ php process.php
Graticule size: E-W: 67.6534534537 km, N-S: 111.13296 km
Image resolution: 608, 1000
2km ellipse is: 35.9479062169, 35.9929223517
Total pixels: 608000
Train pixels: 167803
Bus pixels: 120821
Chance of train within 2km: 27.60%
Chance of train or bus within 2km; 47.47%

Code

<?php /* -*- C++ -*- */

// Calculates the probability of a hashpoint landing within X km of public transport
// See http://wiki.xkcd.com/geohashing/Public_transport_probability
//
// by David Croft, http://wiki.xkcd.com/geohashing/User:Davidc
// no copyright, released into the public domain


// Your downloaded OSM files
$bus_file = 'node[bbox=13,52,14,53][highway=bus_stop]';
$train_file = 'node[bbox=13,52,14,53][railway=station|stop|halt|tram_stop]';

// Output image file
$outfile = 'out.gif';

// Your graticule
$lat_min = 52; // n/s
$lon_min = 13; // e/w

// Distance to search in kilometres.
$distance = 2;

// You shouldn't need to edit anything below this line.

// Top right of the graticule
$lat_max = $lat_min + 1;
$lon_max = $lon_min + 1;

// Centre of the graticule.
$lat_centre = ($lat_min + $lat_max) / 2;
$lon_centre = ($lon_min + $lon_max) / 2;

// Spans (normally 1 unless you edit above)
$lat_span = $lat_max - $lat_min;
$lon_span = $lon_max - $lon_min;

// Graticule is rectangular using Mercator projection.

// All graticules are the same height.
$yres = 1000;

// <ekorren> north-south is always 60*1.852216 (because that's how the nautical mile is defined)
$ns_km = 60*1.852216;

// <ekorren> east-west is     39940.638 / 360 * cos($lat / 180 * 3.1415926) km
//$ew_km = 39940.638 / 360 * cos(deg2rad($lat_centre));

// Wikipedia formula:
$ew_km = deg2rad(cos(deg2rad($lat_centre)) * 6367.449);

echo "Graticule size: E-W: $ew_km km, N-S: $ns_km km\n";

$xres = (int)($yres * ($ew_km/$ns_km));

echo "Image resolution: $xres, $yres\n";

// Now, what is $distance at this latitude?
$ellipse_x = 2 * $xres * $distance / $ew_km;
$ellipse_y = 2 * $yres * $distance / $ns_km;

echo "${distance}km ellipse is: $ellipse_x, $ellipse_y\n";

// Create image and allocate colours
$img = ImageCreate($xres, $yres) or die();

$bg_col = ImageColorAllocate($img, 255,255,255);
$bus_col = ImageColorAllocate($img, 128, 128, 255);
$train_col = ImageColorAllocate($img, 255, 0, 0);

// Draw buses, then draw trains on top
draw_points($img, $bus_file, $bus_col);
draw_points($img, $train_file, $train_col);

// Output image
ImageGIF($img, $outfile);

// Now count pixels
// TODO adjust the weight of a pixel depending on its latitude (above the equator, higher
// pixels should have less effect on % because the physical width is less).

$total_pixels = $xres * $yres;
$train_pixels = 0;
$bus_pixels = 0;

for ($x = 0; $x < $xres; ++$x) {
  for ($y = 0; $y < $yres; ++$y) {
    $col = ImageColorAt($img, $x, $y);
    if ($col == $train_col)
      $train_pixels ++;
    else if ($col == $bus_col)
      $bus_pixels ++;
  }
 }

echo "Total pixels: $total_pixels\n";
echo "Train pixels: $train_pixels\n";
echo "Bus pixels: $bus_pixels\n";
echo "Chance of train within ${distance}km: "
  . number_format(100 * $train_pixels / $total_pixels, 2) . "%\n";
echo "Chance of train or bus within ${distance}km; "
  . number_format(100 * ($train_pixels + $bus_pixels) / $total_pixels, 2) . "%\n";
exit;


function draw_points($img, $file, $col)
{
  global $lon_min, $lon_span;
  global $lat_min, $lat_span;
  global $xres, $yres;
  global $ellipse_x, $ellipse_y;

  $doc = new DOMDocument();
  $doc->load($file) or die();

  $xpath = new DOMXpath($doc);
  $nodes = $xpath->query('/osm/node');

  for ($i=0; $i<$nodes->length; ++$i) {
    $node = $nodes->item($i);

    $lat = $node->getAttribute('lat');
    $lon = $node->getAttribute('lon');

    $x = $xres * (($lon - $lon_min) / $lon_span);
    $y = $yres - ($yres * (($lat - $lat_min) / $lat_span));

    ImageFilledEllipse($img, $x, $y, $ellipse_x, $ellipse_y, $col);
  }
}