Public transport probability

From Geo Hashing
Jump to: navigation, search

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.

[edit] 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!

[edit] 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%

[edit] 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);
  }
}