diff options
author | Matthew Somerville <matthew@balti.ukcod.org.uk> | 2010-11-22 18:28:20 +0000 |
---|---|---|
committer | Matthew Somerville <matthew@balti.ukcod.org.uk> | 2010-11-23 16:54:56 +0000 |
commit | 191acb0664c2edb5c60887caffb4bbc299317df1 (patch) | |
tree | a33dc563550c70241eb9c89c64a3ab359d5fd0b9 | |
parent | aaf1099a48feb5e7d6a76c0560c6be12ee4da9df (diff) |
Factor out geocoding functions.
-rw-r--r-- | perllib/FixMyStreet/Geocode.pm | 168 | ||||
-rw-r--r-- | perllib/Page.pm | 152 | ||||
-rwxr-xr-x | t/Page.t | 5 | ||||
-rwxr-xr-x | web/alert.cgi | 5 | ||||
-rwxr-xr-x | web/index.cgi | 11 | ||||
-rwxr-xr-x | web/rss.cgi | 3 |
6 files changed, 184 insertions, 160 deletions
diff --git a/perllib/FixMyStreet/Geocode.pm b/perllib/FixMyStreet/Geocode.pm new file mode 100644 index 000000000..04e440cc6 --- /dev/null +++ b/perllib/FixMyStreet/Geocode.pm @@ -0,0 +1,168 @@ +#!/usr/bin/perl +# +# FixMyStreet::Geocode +# The geocoding functions for FixMyStreet. +# +# Copyright (c) 2010 UK Citizens Online Democracy. All rights reserved. +# Email: matthew@mysociety.org; WWW: http://www.mysociety.org/ + +package FixMyStreet::Geocode; + +use strict; +use Error qw(:try); +use File::Slurp; +use LWP::Simple; +use Digest::MD5 qw(md5_hex); +use URI::Escape; + +use Cobrand; +use Page; +use mySociety::Config; +use mySociety::GeoUtil; +use mySociety::MaPit; +use mySociety::PostcodeUtil; +use mySociety::Web qw(NewURL); + +BEGIN { + (my $dir = __FILE__) =~ s{/[^/]*?$}{}; + mySociety::Config::set_file("$dir/../../conf/general"); +} + +# lookup STRING QUERY +# Given a user-inputted string, try and convert it into co-ordinates using either +# MaPit if it's a postcode, or Google Maps API otherwise. Returns an array of +# data, including an error if there is one (which includes a location being in +# Northern Ireland). The information in the query may be used by cobranded versions +# of the site to diambiguate locations. +sub lookup { + my ($s, $q) = @_; + my ($x, $y, $easting, $northing, $error); + if ($s =~ /^\d+$/) { + $error = 'FixMyStreet is a UK-based website that currently works in England, Scotland, and Wales. Please enter either a postcode, or a Great British street name and area.'; + } elsif (mySociety::PostcodeUtil::is_valid_postcode($s)) { + my $location = mySociety::MaPit::call('postcode', $s); + unless ($error = Page::mapit_check_error($location)) { + $easting = $location->{easting}; + $northing = $location->{northing}; + my $xx = FixMyStreet::Map::os_to_tile($easting); + my $yy = FixMyStreet::Map::os_to_tile($northing); + $x = int($xx); + $y = int($yy); + $x += 1 if ($xx - $x > 0.5); + $y += 1 if ($yy - $y > 0.5); + } + } else { + ($x, $y, $easting, $northing, $error) = FixMyStreet::Geocode::string($s, $q); + } + return ($x, $y, $easting, $northing, $error); +} + +sub geocoded_string_coordinates { + my ($js, $q) = @_; + my ($x, $y, $easting, $northing, $error); + my ($accuracy) = $js =~ /"Accuracy" *: *(\d)/; + if ($accuracy < 4) { + $error = _('Sorry, that location appears to be too general; please be more specific.'); + } else { + + $js =~ /"coordinates" *: *\[ *(.*?), *(.*?),/; + my $lon = $1; my $lat = $2; + try { + ($easting, $northing) = mySociety::GeoUtil::wgs84_to_national_grid($lat, $lon, 'G'); + my $xx = FixMyStreet::Map::os_to_tile($easting); + my $yy = FixMyStreet::Map::os_to_tile($northing); + $x = int($xx); + $y = int($yy); + $x += 1 if ($xx - $x > 0.5); + $y += 1 if ($yy - $y > 0.5); + } catch Error::Simple with { + $error = shift; + $error = _('That location does not appear to be in Britain; please try again.') + if $error =~ /out of the area covered/; + } + } + return ($x, $y, $easting, $northing, $error); +} + +# string STRING QUERY +# Canonicalises, looks up on Google Maps API, and caches, a user-inputted location. +# Returns array of (TILE_X, TILE_Y, EASTING, NORTHING, ERROR), where ERROR is +# either undef, a string, or an array of matches if there are more than one. The +# information in the query may be used to disambiguate the location in cobranded versions +# of the site. +sub string { + my ($s, $q) = @_; + $s = lc($s); + $s =~ s/[^-&0-9a-z ']/ /g; + $s =~ s/\s+/ /g; + $s = URI::Escape::uri_escape_utf8($s); + $s = Cobrand::disambiguate_location(Page::get_cobrand($q), "q=$s", $q); + $s =~ s/%20/+/g; + my $url = 'http://maps.google.com/maps/geo?' . $s; + my $cache_dir = mySociety::Config::get('GEO_CACHE'); + my $cache_file = $cache_dir . md5_hex($url); + my ($js, $error, $x, $y, $easting, $northing); + if (-s $cache_file) { + $js = File::Slurp::read_file($cache_file); + } else { + $url .= ',+UK' unless $url =~ /united\++kingdom$/ || $url =~ /uk$/i; + $url .= '&sensor=false&gl=uk&key=' . mySociety::Config::get('GOOGLE_MAPS_API_KEY'); + $js = LWP::Simple::get($url); + File::Slurp::write_file($cache_file, $js) if $js && $js !~ /"code":6[12]0/; + } + if (!$js) { + $error = _('Sorry, we could not parse that location. Please try again.'); + } elsif ($js !~ /"code" *: *200/) { + $error = _('Sorry, we could not find that location.'); + } elsif ($js =~ /}, *{/) { # Multiple + my @js = split /}, *{/, $js; + my @valid_locations; + foreach (@js) { + next unless /"address" *: *"(.*?)"/s; + my $address = $1; + next unless Cobrand::geocoded_string_check(Page::get_cobrand($q), $address, $q); + next if $address =~ /BT\d/; + push (@valid_locations, $_); + push (@$error, $address); + } + if (scalar @valid_locations == 1) { + return geocoded_string_coordinates($valid_locations[0], $q); + } + $error = _('Sorry, we could not find that location.') unless $error; + } elsif ($js =~ /BT\d/) { + # Northern Ireland, hopefully + $error = _("We do not cover Northern Ireland, I'm afraid, as our licence doesn't include any maps for the region."); + } else { + ($x, $y, $easting, $northing, $error) = geocoded_string_coordinates($js, $q); + } + return ($x, $y, $easting, $northing, $error); +} + +# list_choices +# Prints response if there's more than one possible result +sub list_choices { + my ($choices, $page, $q) = @_; + my $url; + my $cobrand = Page::get_cobrand($q); + my $message = _('We found more than one match for that location. We show up to ten matches, please try a different search if yours is not here.'); + my $out = '<p>' . $message . '</p>'; + my $choice_list = '<ul>'; + foreach my $choice (@$choices) { + $choice =~ s/, United Kingdom//; + $choice =~ s/, UK//; + $url = Cobrand::url($cobrand, NewURL($q, -retain => 1, -url => $page, 'pc' => $choice), $q); + $url =~ s/%20/+/g; + $choice_list .= '<li><a href="' . $url . '">' . $choice . "</a></li>\n"; + } + $choice_list .= '</ul>'; + $out .= $choice_list; + my %vars = (message => $message, + choice_list => $choice_list, + header => _('More than one match'), + url_home => Cobrand::url($cobrand, '/', $q)); + my $cobrand_choice = Page::template_include('geocode-choice', $q, Page::template_root($q), %vars); + return $cobrand_choice if $cobrand_choice; + return $out; +} + +1; diff --git a/perllib/Page.pm b/perllib/Page.pm index 06bdb27a4..b3ab35b33 100644 --- a/perllib/Page.pm +++ b/perllib/Page.pm @@ -16,42 +16,31 @@ use Carp; use mySociety::CGIFast qw(-no_xhtml); use Error qw(:try); use File::Slurp; -use HTTP::Date; +use HTTP::Date; # time2str use Image::Magick; use Image::Size; -use LWP::Simple; -use Digest::MD5 qw(md5_hex); use POSIX qw(strftime); use URI::Escape; use Text::Template; use Memcached; use Problems; -use Utils; use Cobrand; use mySociety::Config; use mySociety::DBHandle qw/dbh select_all/; use mySociety::EvEl; -use mySociety::Gaze; -use mySociety::GeoUtil; use mySociety::Locale; use mySociety::MaPit; -use mySociety::PostcodeUtil; use mySociety::TempFiles; use mySociety::Tracking; use mySociety::WatchUpdate; -use mySociety::Web qw(ent NewURL); +use mySociety::Web qw(ent); BEGIN { (my $dir = __FILE__) =~ s{/[^/]*?$}{}; mySociety::Config::set_file("$dir/../conf/general"); } -use constant TILE_WIDTH => mySociety::Config::get('TILES_WIDTH'); -use constant TIF_SIZE_M => mySociety::Config::get('TILES_TIFF_SIZE_METRES'); -use constant TIF_SIZE_PX => mySociety::Config::get('TILES_TIFF_SIZE_PIXELS'); -use constant SCALE_FACTOR => TIF_SIZE_M / (TIF_SIZE_PX / TILE_WIDTH); - my $lastmodified; sub do_fastcgi { @@ -658,143 +647,6 @@ sub mapit_check_error { return 0; } -# geocode STRING QUERY -# Given a user-inputted string, try and convert it into co-ordinates using either -# MaPit if it's a postcode, or Google Maps API otherwise. Returns an array of -# data, including an error if there is one (which includes a location being in -# Northern Ireland). The information in the query may be used by cobranded versions -# of the site to diambiguate locations. -sub geocode { - my ($s, $q) = @_; - my ($x, $y, $easting, $northing, $error); - if ($s =~ /^\d+$/) { - $error = 'FixMyStreet is a UK-based website that currently works in England, Scotland, and Wales. Please enter either a postcode, or a Great British street name and area.'; - } elsif (mySociety::PostcodeUtil::is_valid_postcode($s)) { - my $location = mySociety::MaPit::call('postcode', $s); - unless ($error = mapit_check_error($location)) { - $easting = $location->{easting}; - $northing = $location->{northing}; - my $xx = FixMyStreet::Map::os_to_tile($easting); - my $yy = FixMyStreet::Map::os_to_tile($northing); - $x = int($xx); - $y = int($yy); - $x += 1 if ($xx - $x > 0.5); - $y += 1 if ($yy - $y > 0.5); - } - } else { - ($x, $y, $easting, $northing, $error) = geocode_string($s, $q); - } - return ($x, $y, $easting, $northing, $error); -} - -sub geocoded_string_coordinates { - my ($js, $q) = @_; - my ($x, $y, $easting, $northing, $error); - my ($accuracy) = $js =~ /"Accuracy" *: *(\d)/; - if ($accuracy < 4) { - $error = _('Sorry, that location appears to be too general; please be more specific.'); - } else { - - $js =~ /"coordinates" *: *\[ *(.*?), *(.*?),/; - my $lon = $1; my $lat = $2; - try { - ($easting, $northing) = mySociety::GeoUtil::wgs84_to_national_grid($lat, $lon, 'G'); - my $xx = FixMyStreet::Map::os_to_tile($easting); - my $yy = FixMyStreet::Map::os_to_tile($northing); - $x = int($xx); - $y = int($yy); - $x += 1 if ($xx - $x > 0.5); - $y += 1 if ($yy - $y > 0.5); - } catch Error::Simple with { - $error = shift; - $error = _('That location does not appear to be in Britain; please try again.') - if $error =~ /out of the area covered/; - } - } - return ($x, $y, $easting, $northing, $error); -} - -# geocode_string STRING QUERY -# Canonicalises, looks up on Google Maps API, and caches, a user-inputted location. -# Returns array of (TILE_X, TILE_Y, EASTING, NORTHING, ERROR), where ERROR is -# either undef, a string, or an array of matches if there are more than one. The -# information in the query may be used to disambiguate the location in cobranded versions -# of the site. -sub geocode_string { - my ($s, $q) = @_; - $s = lc($s); - $s =~ s/[^-&0-9a-z ']/ /g; - $s =~ s/\s+/ /g; - $s = URI::Escape::uri_escape_utf8($s); - $s = Cobrand::disambiguate_location(get_cobrand($q), "q=$s", $q); - $s =~ s/%20/+/g; - my $url = 'http://maps.google.com/maps/geo?' . $s; - my $cache_dir = mySociety::Config::get('GEO_CACHE'); - my $cache_file = $cache_dir . md5_hex($url); - my ($js, $error, $x, $y, $easting, $northing); - if (-s $cache_file) { - $js = File::Slurp::read_file($cache_file); - } else { - $url .= ',+UK' unless $url =~ /united\++kingdom$/ || $url =~ /uk$/i; - $url .= '&sensor=false&gl=uk&key=' . mySociety::Config::get('GOOGLE_MAPS_API_KEY'); - $js = LWP::Simple::get($url); - File::Slurp::write_file($cache_file, $js) if $js && $js !~ /"code":6[12]0/; - } - if (!$js) { - $error = _('Sorry, we could not parse that location. Please try again.'); - } elsif ($js !~ /"code" *: *200/) { - $error = _('Sorry, we could not find that location.'); - } elsif ($js =~ /}, *{/) { # Multiple - my @js = split /}, *{/, $js; - my @valid_locations; - foreach (@js) { - next unless /"address" *: *"(.*?)"/s; - my $address = $1; - next unless Cobrand::geocoded_string_check(get_cobrand($q), $address, $q); - next if $address =~ /BT\d/; - push (@valid_locations, $_); - push (@$error, $address); - } - if (scalar @valid_locations == 1) { - return geocoded_string_coordinates($valid_locations[0], $q); - } - $error = _('Sorry, we could not find that location.') unless $error; - } elsif ($js =~ /BT\d/) { - # Northern Ireland, hopefully - $error = _("We do not cover Northern Ireland, I'm afraid, as our licence doesn't include any maps for the region."); - } else { - ($x, $y, $easting, $northing, $error) = geocoded_string_coordinates($js, $q); - } - return ($x, $y, $easting, $northing, $error); -} - -# geocode_choice -# Prints response if there's more than one possible result -sub geocode_choice { - my ($choices, $page, $q) = @_; - my $url; - my $cobrand = Page::get_cobrand($q); - my $message = _('We found more than one match for that location. We show up to ten matches, please try a different search if yours is not here.'); - my $out = '<p>' . $message . '</p>'; - my $choice_list = '<ul>'; - foreach my $choice (@$choices) { - $choice =~ s/, United Kingdom//; - $choice =~ s/, UK//; - $url = Cobrand::url($cobrand, NewURL($q, -retain => 1, -url => $page, 'pc' => $choice), $q); - $url =~ s/%20/+/g; - $choice_list .= '<li><a href="' . $url . '">' . $choice . "</a></li>\n"; - } - $choice_list .= '</ul>'; - $out .= $choice_list; - my %vars = (message => $message, - choice_list => $choice_list, - header => _('More than one match'), - url_home => Cobrand::url($cobrand, '/', $q)); - my $cobrand_choice = Page::template_include('geocode-choice', $q, Page::template_root($q), %vars); - return $cobrand_choice if $cobrand_choice; - return $out; -} - sub short_name { my $name = shift; # Special case Durham as it's the only place with two councils of the same name @@ -20,6 +20,7 @@ use lib "$FindBin::Bin/../perllib"; use lib "$FindBin::Bin/../commonlib/perllib"; use Page; +use FixMyStreet::Geocode; use mySociety::MockQuery; use mySociety::Locale; @@ -40,14 +41,14 @@ sub test_geocode_string() { my $q = new MockQuery('nosite', \%params); # geocode a straightforward string, expect success - my ($x, $y, $easting, $northing, $error) = Page::geocode_string('Buckingham Palace', $q); + my ($x, $y, $easting, $northing, $error) = FixMyStreet::Geocode::string('Buckingham Palace', $q); ok($x == 3280, 'example x coordinate generated') or diag("Got $x"); ok($y == 1114, 'example y coordinate generated') or diag("Got $y");; ok($easting == 529044, 'example easting generated') or diag("Got $easting"); ok($northing == 179619, 'example northing generated') or diag("Got $northing"); ok(! defined($error), 'should not generate error for simple example') or diag("Got $error"); # expect a failure message for Northern Ireland - ($x, $y, $easting, $northing, $error) = Page::geocode_string('Falls Road, Belfast', $q); + ($x, $y, $easting, $northing, $error) = FixMyStreet::Geocode::string('Falls Road, Belfast', $q); ok($error eq "We do not cover Northern Ireland, I'm afraid, as our licence doesn't include any maps for the region.", 'error message produced for NI location') or diag("Got $error"); } diff --git a/web/alert.cgi b/web/alert.cgi index 46eef082b..70b9d1873 100755 --- a/web/alert.cgi +++ b/web/alert.cgi @@ -14,6 +14,7 @@ use Digest::SHA1 qw(sha1_hex); use Error qw(:try); use CrossSell; use FixMyStreet::Alert; +use FixMyStreet::Geocode; use mySociety::AuthToken; use mySociety::Config; use mySociety::DBHandle qw(select_all); @@ -84,12 +85,12 @@ sub alert_list { $n = FixMyStreet::Map::tile_to_os($input{y}); } else { try { - ($x, $y, $e, $n, $error) = Page::geocode($input{pc}, $q); + ($x, $y, $e, $n, $error) = FixMyStreet::Geocode::lookup($input{pc}, $q); } catch Error::Simple with { $error = shift; }; } - return Page::geocode_choice($error, '/alert', $q) if ref($error) eq 'ARRAY'; + return FixMyStreet::Geocode::list_choices($error, '/alert', $q) if ref($error) eq 'ARRAY'; return alert_front_page($q, $error) if $error; my $pretty_pc = $input_h{pc}; diff --git a/web/index.cgi b/web/index.cgi index 00778c442..1ffce4660 100755 --- a/web/index.cgi +++ b/web/index.cgi @@ -16,6 +16,7 @@ use CGI::Carp; use URI::Escape; use CrossSell; +use FixMyStreet::Geocode; use mySociety::AuthToken; use mySociety::Config; use mySociety::DBHandle qw(select_all); @@ -469,7 +470,7 @@ sub display_form { $easting = FixMyStreet::Map::tile_to_os($input{x}); $northing = FixMyStreet::Map::tile_to_os($input{y}); } else { - my ($x, $y, $e, $n, $error) = Page::geocode($input{pc}, $q); + my ($x, $y, $e, $n, $error) = FixMyStreet::Geocode::lookup($input{pc}, $q); $easting = $e; $northing = $n; } } elsif ($pin_x && $pin_y) { @@ -485,11 +486,11 @@ sub display_form { } elsif ($input{partial} && $input{pc} && !$input{easting} && !$input{northing}) { my ($x, $y, $error); try { - ($x, $y, $easting, $northing, $error) = Page::geocode($input{pc}, $q); + ($x, $y, $easting, $northing, $error) = FixMyStreet::Geocode::lookup($input{pc}, $q); } catch Error::Simple with { $error = shift; }; - return Page::geocode_choice($error, '/', $q) if ref($error) eq 'ARRAY'; + return FixMyStreet::Geocode::list_choices($error, '/', $q) if ref($error) eq 'ARRAY'; return front_page($q, $error) if $error; $input{x} = int(FixMyStreet::Map::os_to_tile($easting)); $input{y} = int(FixMyStreet::Map::os_to_tile($northing)); @@ -794,12 +795,12 @@ sub display_location { return front_page($q, @errors) unless $x || $y || $input{pc}; if (!$x && !$y) { try { - ($x, $y, $easting, $northing, $error) = Page::geocode($input{pc}, $q); + ($x, $y, $easting, $northing, $error) = FixMyStreet::Geocode::lookup($input{pc}, $q); } catch Error::Simple with { $error = shift; }; } - return Page::geocode_choice($error, '/', $q) if (ref($error) eq 'ARRAY'); + return FixMyStreet::Geocode::list_choices($error, '/', $q) if (ref($error) eq 'ARRAY'); return front_page($q, $error) if ($error); if (!$easting || !$northing) { diff --git a/web/rss.cgi b/web/rss.cgi index effa15f08..623314e7f 100755 --- a/web/rss.cgi +++ b/web/rss.cgi @@ -13,6 +13,7 @@ use Error qw(:try); use Standard; use URI::Escape; use FixMyStreet::Alert; +use FixMyStreet::Geocode; use mySociety::MaPit; use mySociety::GeoUtil; use mySociety::Gaze; @@ -93,7 +94,7 @@ sub rss_local_problems { } elsif ($pc) { my $error; try { - ($x, $y, $e, $n, $error) = Page::geocode($pc, $q); + ($x, $y, $e, $n, $error) = FixMyStreet::Geocode::lookup($pc, $q); } catch Error::Simple with { $error = shift; }; |