From 191acb0664c2edb5c60887caffb4bbc299317df1 Mon Sep 17 00:00:00 2001 From: Matthew Somerville Date: Mon, 22 Nov 2010 18:28:20 +0000 Subject: Factor out geocoding functions. --- perllib/FixMyStreet/Geocode.pm | 168 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 168 insertions(+) create mode 100644 perllib/FixMyStreet/Geocode.pm (limited to 'perllib/FixMyStreet/Geocode.pm') 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 = '

' . $message . '

'; + my $choice_list = ''; + $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; -- cgit v1.2.3 From 8690e19d0fb7cc88f8e13a3f6cc46681c1ee95a4 Mon Sep 17 00:00:00 2001 From: Matthew Somerville Date: Tue, 23 Nov 2010 15:55:17 +0000 Subject: Have FixMyStreet::Geocode only deal in real co-ordinates. --- perllib/FixMyStreet/Geocode.pm | 28 ++++++++-------------------- 1 file changed, 8 insertions(+), 20 deletions(-) (limited to 'perllib/FixMyStreet/Geocode.pm') diff --git a/perllib/FixMyStreet/Geocode.pm b/perllib/FixMyStreet/Geocode.pm index 04e440cc6..9e89b4f7b 100644 --- a/perllib/FixMyStreet/Geocode.pm +++ b/perllib/FixMyStreet/Geocode.pm @@ -36,7 +36,7 @@ BEGIN { # of the site to diambiguate locations. sub lookup { my ($s, $q) = @_; - my ($x, $y, $easting, $northing, $error); + my ($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)) { @@ -44,22 +44,16 @@ sub lookup { 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); + ($easting, $northing, $error) = FixMyStreet::Geocode::string($s, $q); } - return ($x, $y, $easting, $northing, $error); + return ($easting, $northing, $error); } sub geocoded_string_coordinates { my ($js, $q) = @_; - my ($x, $y, $easting, $northing, $error); + my ($easting, $northing, $error); my ($accuracy) = $js =~ /"Accuracy" *: *(\d)/; if ($accuracy < 4) { $error = _('Sorry, that location appears to be too general; please be more specific.'); @@ -69,19 +63,13 @@ sub geocoded_string_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); + return ($easting, $northing, $error); } # string STRING QUERY @@ -101,7 +89,7 @@ sub string { 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); + my ($js, $error, $easting, $northing); if (-s $cache_file) { $js = File::Slurp::read_file($cache_file); } else { @@ -133,9 +121,9 @@ sub string { # 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); + ($easting, $northing, $error) = geocoded_string_coordinates($js, $q); } - return ($x, $y, $easting, $northing, $error); + return ($easting, $northing, $error); } # list_choices -- cgit v1.2.3