diff options
author | Matthew Somerville <matthew@mysociety.org> | 2014-03-11 17:34:38 +0000 |
---|---|---|
committer | Matthew Somerville <matthew@mysociety.org> | 2014-03-11 17:34:38 +0000 |
commit | 4df8d9979a4325bf2b47f401b4095654dd43d3a9 (patch) | |
tree | 64984c71916683830a1b1eef4deee8a04307cd80 /perllib | |
parent | eee9a69a1959f060fdbe7d346d594a76a3b39909 (diff) | |
parent | de47a7fad977156c67748deed6b3f154fc849035 (diff) |
Merge remote branch 'origin/sweden-rebase'
Conflicts:
bin/update-schema
locale/sv_SE.UTF-8/LC_MESSAGES/FixMyStreet.po
perllib/FixMyStreet/DB/Result/Body.pm
Diffstat (limited to 'perllib')
-rw-r--r-- | perllib/FixMyStreet/App/Controller/Report/New.pm | 1 | ||||
-rwxr-xr-x | perllib/FixMyStreet/App/Controller/Static.pm | 4 | ||||
-rw-r--r-- | perllib/FixMyStreet/Cobrand/FixaMinGata.pm | 272 | ||||
-rw-r--r-- | perllib/FixMyStreet/DB/Result/Body.pm | 2 | ||||
-rw-r--r-- | perllib/FixMyStreet/Geocode/FixaMinGata.pm | 204 | ||||
-rw-r--r-- | perllib/FixMyStreet/Map/OSM/MapQuest.pm | 4 | ||||
-rw-r--r-- | perllib/Open311.pm | 25 | ||||
-rw-r--r-- | perllib/Open311/GetUpdates.pm | 84 |
8 files changed, 583 insertions, 13 deletions
diff --git a/perllib/FixMyStreet/App/Controller/Report/New.pm b/perllib/FixMyStreet/App/Controller/Report/New.pm index fcab4e49a..a419e9cc1 100644 --- a/perllib/FixMyStreet/App/Controller/Report/New.pm +++ b/perllib/FixMyStreet/App/Controller/Report/New.pm @@ -678,6 +678,7 @@ sub setup_categories_and_bodies : Private { # put results onto stash for display $c->stash->{bodies} = \%bodies; $c->stash->{all_body_names} = [ map { $_->name } values %bodies ]; + $c->stash->{all_body_urls} = [ map { $_->external_url } values %bodies ]; $c->stash->{bodies_to_list} = [ keys %bodies_to_list ]; $c->stash->{category_options} = \@category_options; $c->stash->{category_extras} = \%category_extras; diff --git a/perllib/FixMyStreet/App/Controller/Static.pm b/perllib/FixMyStreet/App/Controller/Static.pm index 6cc22aede..40e2431ea 100755 --- a/perllib/FixMyStreet/App/Controller/Static.pm +++ b/perllib/FixMyStreet/App/Controller/Static.pm @@ -57,6 +57,10 @@ sub iphone : Global : Args(0) { my ( $self, $c ) = @_; } +sub council : Global : Args(0) { + my ( $self, $c ) = @_; +} + __PACKAGE__->meta->make_immutable; 1; diff --git a/perllib/FixMyStreet/Cobrand/FixaMinGata.pm b/perllib/FixMyStreet/Cobrand/FixaMinGata.pm new file mode 100644 index 000000000..f17f7716f --- /dev/null +++ b/perllib/FixMyStreet/Cobrand/FixaMinGata.pm @@ -0,0 +1,272 @@ +package FixMyStreet::Cobrand::FixaMinGata; +use base 'FixMyStreet::Cobrand::Default'; + +use strict; +use warnings; + +use Carp; +use mySociety::MaPit; +use FixMyStreet::Geocode::FixaMinGata; +use DateTime; + + +DateTime->DefaultLocale('sv_SE'); + +sub site_title { + my ($self) = @_; + return 'Fixa Min Gata'; +} + +sub path_to_web_templates { + my $self = shift; + return [ + FixMyStreet->path_to( 'templates/web', $self->moniker )->stringify, + FixMyStreet->path_to( 'templates/web/fixmystreet' )->stringify + ]; +} + +sub country { + return 'SE'; +} + +sub languages { [ 'en-gb,English,en_GB', 'sv,Swedish,sv_SE' ] } +sub language_override { 'sv' } + +sub enter_postcode_text { + my ( $self ) = @_; + return _('Enter a nearby postcode, or street name and area'); +} + +# Is also adding language parameter +sub disambiguate_location { + return { + lang => 'sv', + country => 'se', # Is this the right format? /Rikard + }; +} + +sub area_types { + [ 'KOM' ]; +} + +sub admin_base_url { + return 'http://www.fixamingata.se/admin/'; +} + +# If lat/lon are present in the URL, OpenLayers will use that to centre the map. +# Need to specify a zoom to stop it defaulting to null/0. +sub uri { + my ( $self, $uri ) = @_; + + $uri->query_param( zoom => 3 ) + if $uri->query_param('lat') && !$uri->query_param('zoom'); + + return $uri; +} + +sub geocode_postcode { + my ( $self, $s ) = @_; + # Most people write Swedish postcodes like this: + #+ XXX XX, so let's remove the space + # Is this the right place to do this? //Rikard + # This is the right place! // Jonas + $s =~ s/\ //g; # Rikard, remove space in postcode + if ($s =~ /^\d{5}$/) { + my $location = mySociety::MaPit::call('postcode', $s); + if ($location->{error}) { + return { + error => $location->{code} =~ /^4/ + ? _('That postcode was not recognised, sorry.') + : $location->{error} + }; + } + return { + latitude => $location->{wgs84_lat}, + longitude => $location->{wgs84_lon}, + }; + } + return {}; +} + +# Vad gör den här funktionen? Är "Sverige" rätt här? +sub geocoded_string_check { + my ( $self, $s ) = @_; + return 1 if $s =~ /, Sverige/; + return 0; +} + +sub find_closest { + my ( $self, $latitude, $longitude ) = @_; + return FixMyStreet::Geocode::OSM::closest_road_text( $self, $latitude, $longitude ); +} + +# Used by send-reports, calling find_closest, calling OSM geocoding +sub guess_road_operator { + my ( $self, $inforef ) = @_; + + my $highway = $inforef->{highway} || "unknown"; + my $refs = $inforef->{ref} || "unknown"; + return "Trafikverket" + if $highway eq "trunk" || $highway eq "primary"; + + for my $ref (split(/;/, $refs)) { + return "Trafikverket" + if $ref =~ m/E ?\d+/ || $ref =~ m/Fv\d+/i; + } + return ''; +} + +sub remove_redundant_councils { + my $self = shift; + my $all_councils = shift; + + # Oslo is both a kommune and a fylke, we only want to show it once + # Jag tror inte detta är applicerbart på Sverige ;-) //Rikard + #delete $all_councils->{301} # + # if $all_councils->{3}; +} + +sub filter_all_council_ids_list { + my $self = shift; + my @all_councils_ids = @_; + + # as above we only want to show Oslo once + # Rikard kommenterar ut detta. + # return grep { $_ != 301 } @all_councils_ids; + # Rikard: + return @all_councils_ids; # Är detta rätt? //Rikard +} + +sub short_name { + my $self = shift; + my ($area, $info) = @_; + + # Rikard kommenterar ut följande tills vidare... + #if ($area->{name} =~ /^(Os|Nes|V\xe5ler|Sande|B\xf8|Her\xf8y)$/) { + # my $parent = $info->{$area->{parent_area}}->{name}; + # return URI::Escape::uri_escape_utf8("$area->{name}, $parent"); + #} + + my $name = $area->{name}; + $name =~ s/ & / and /; + $name = URI::Escape::uri_escape_utf8($name); + $name =~ s/%20/+/g; + return $name; +} + +# Vad ska vi göra för svenska förhållanden här??? //Rikard +sub council_rss_alert_options { + my $self = shift; + my $all_councils = shift; + my $c = shift; + + my ( @options, @reported_to_options, $fylke, $kommune ); + + foreach ( values %$all_councils ) { + if ( $_->{type} eq 'NKO' ) { + $kommune = $_; + } + else { + $fylke = $_; + } + } + + if ( $fylke->{id} == 3 ) { # Oslo + my $short_name = $self->short_name($fylke, $all_councils); + ( my $id_name = $short_name ) =~ tr/+/_/; + + push @options, + { + type => 'council', + id => sprintf( 'council:%s:%s', $fylke->{id}, $id_name ), + rss_text => + sprintf( _('RSS feed of problems within %s'), $fylke->{name} ), + text => sprintf( _('Problems within %s'), $fylke->{name} ), + uri => $c->uri_for( '/rss/reports', $short_name ), + }; + } + else { + my $short_kommune_name = $self->short_name($kommune, $all_councils); + ( my $id_kommune_name = $short_kommune_name ) =~ tr/+/_/; + + my $short_fylke_name = $self->short_name($fylke, $all_councils); + ( my $id_fylke_name = $short_fylke_name ) =~ tr/+/_/; + + push @options, + { + type => 'area', + id => sprintf( 'area:%s:%s', $kommune->{id}, $id_kommune_name ), + rss_text => + sprintf( _('RSS feed of %s'), $kommune->{name} ), + text => $kommune->{name}, + uri => $c->uri_for( '/rss/area', $short_kommune_name ), + }, + { + type => 'area', + id => sprintf( 'area:%s:%s', $fylke->{id}, $id_fylke_name ), + rss_text => + sprintf( _('RSS feed of %s'), $fylke->{name} ), + text => $fylke->{name}, + uri => $c->uri_for( '/rss/area', $short_fylke_name ), + }; + + push @reported_to_options, + { + type => 'council', + id => sprintf( 'council:%s:%s', $kommune->{id}, $id_kommune_name ), + rss_text => + sprintf( _('RSS feed of %s'), $kommune->{name} ), + text => $kommune->{name}, + uri => $c->uri_for( '/rss/reports', $short_kommune_name ), + }, + { + type => 'council', + id => sprintf( 'council:%s:%s', $fylke->{id}, $id_fylke_name ), + rss_text => + sprintf( _('RSS feed of %s'), $fylke->{name} ), + text => $fylke->{name}, + uri => $c->uri_for( '/rss/reports/', $short_fylke_name ), + }; + } + + return ( + \@options, @reported_to_options + ? \@reported_to_options + : undef + ); + +} + +# Vad ska vi göra för svenska förhållanden här??? //Rikard +sub reports_council_check { + my ( $self, $c, $council ) = @_; + + if ($council eq 'Oslo') { + + # There are two Oslos (kommune and fylke), we only want one of them. + $c->stash->{council} = mySociety::MaPit::call('area', 3); + return 1; + + } elsif ($council =~ /,/) { + + # Some kommunes have the same name, use the fylke name to work out which. + my ($kommune, $fylke) = split /\s*,\s*/, $council; + my $area_types = $c->cobrand->area_types; + my $areas_k = mySociety::MaPit::call('areas', $kommune, type => $area_types); + my $areas_f = mySociety::MaPit::call('areas', $fylke, type => $area_types); + if (keys %$areas_f == 1) { + ($fylke) = values %$areas_f; + foreach (values %$areas_k) { + if ($_->{name} eq $kommune && $_->{parent_area} == $fylke->{id}) { + $c->stash->{council} = $_; + return 1; + } + } + } + # If we're here, we've been given a bad name. + $c->detach( 'redirect_index' ); + + } +} + +1; diff --git a/perllib/FixMyStreet/DB/Result/Body.pm b/perllib/FixMyStreet/DB/Result/Body.pm index c2b0555fb..be4adeca9 100644 --- a/perllib/FixMyStreet/DB/Result/Body.pm +++ b/perllib/FixMyStreet/DB/Result/Body.pm @@ -42,6 +42,8 @@ __PACKAGE__->add_columns( { data_type => "boolean", default_value => \"false", is_nullable => 0 }, "deleted", { data_type => "boolean", default_value => \"false", is_nullable => 0 }, + "external_url", + { data_type => "text", is_nullable => 0 }, ); __PACKAGE__->set_primary_key("id"); __PACKAGE__->has_many( diff --git a/perllib/FixMyStreet/Geocode/FixaMinGata.pm b/perllib/FixMyStreet/Geocode/FixaMinGata.pm new file mode 100644 index 000000000..2db25f504 --- /dev/null +++ b/perllib/FixMyStreet/Geocode/FixaMinGata.pm @@ -0,0 +1,204 @@ +#!/usr/bin/perl +# +# FixMyStreet:Geocode::FixaMinGata +# OpenStreetmap forward and reverse geocoding for FixMyStreet. +# +# Copyright (c) 2011 Petter Reinholdtsen. Some rights reserved. +# Email: pere@hungry.com + +# This module is a slightly derived version of OSM.pm. + +# As of January 2014, the FixaMinGata developers are considering to make further +# changes related to OSM, so it's probably best to keep this module separate +# from the OSM module for now. + +package FixMyStreet::Geocode::FixaMinGata; + +use warnings; +use strict; +use Data::Dumper; + +use Digest::MD5 qw(md5_hex); +use Encode; +use File::Slurp; +use File::Path (); +use LWP::Simple qw($ua); +use Memcached; +use XML::Simple; +use mySociety::Locale; + +my $osmapibase = "http://www.openstreetmap.org/api/"; +my $nominatimbase = "http://nominatim.openstreetmap.org/"; + +# string STRING CONTEXT +# Looks up on Nominatim, and caches, a user-inputted location. +# Returns array of (LAT, LON, 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, $c ) = @_; + + my $params = $c->cobrand->disambiguate_location($s); + + $s = FixMyStreet::Geocode::escape($s); + # $s .= '+' . $params->{town} if $params->{town} and $s !~ /$params->{town}/i; + + my $url = "${nominatimbase}search?"; + my %query_params = ( + q => $s, + format => 'json', + addressdetails => 1, + limit => 20, + #'accept-language' => '', + email => 'info' . chr(64) . 'morus.se', + ); + # $query_params{viewbox} = $params->{bounds}[1] . ',' . $params->{bounds}[2] . ',' . $params->{bounds}[3] . ',' . $params->{bounds}[0] + # if $params->{bounds}; + $query_params{countrycodes} = $params->{country} + if $params->{country}; + $url .= join('&', map { "$_=$query_params{$_}" } keys %query_params); + + my $cache_dir = FixMyStreet->config('GEO_CACHE') . 'osm/'; + my $cache_file = $cache_dir . md5_hex($url); + my $js; + if (-s $cache_file) { + $js = File::Slurp::read_file($cache_file); + } else { + $ua->timeout(15); + $js = LWP::Simple::get($url); + $js = encode_utf8($js) if utf8::is_utf8($js); + File::Path::mkpath($cache_dir); + File::Slurp::write_file($cache_file, $js) if $js; + } + + if (!$js) { + return { error => _('Sorry, we could not find that location.') }; + } + + $js = JSON->new->utf8->allow_nonref->decode($js); + + my ( %locations, $error, @valid_locations, $latitude, $longitude ); + foreach (@$js) { + # These co-ordinates are output as query parameters in a URL, make sure they have a "." + next if $_->{class} eq "boundary"; + + my @s = split(/,/, $_->{display_name}); + + my $address = join(",", @s[0,1,2]); + + $locations{$address} = [$_->{lat}, $_->{lon}]; + } + + my ($key) = keys %locations; + + return { latitude => $locations{$key}[0], longitude => $locations{$key}[1] } if scalar keys %locations == 1; + return { error => _('Sorry, we could not find that location.') } if scalar keys %locations == 0; + + foreach $key (keys %locations) { + ( $latitude, $longitude ) = ($locations{$key}[0], $locations{$key}[1]); + mySociety::Locale::in_gb_locale { + push (@$error, { + address => $key, + latitude => sprintf('%0.6f', $latitude), + longitude => sprintf('%0.6f', $longitude) + }); + }; + } + + return { error => $error }; +} + +sub reverse_geocode { + my ($latitude, $longitude, $zoom) = @_; + my $url = + "${nominatimbase}reverse?format=xml&zoom=$zoom&lat=$latitude&lon=$longitude"; + my $key = "OSM:reverse_geocode:$url"; + my $result = Memcached::get($key); + unless ($result) { + my $j = LWP::Simple::get($url); + if ($j) { + Memcached::set($key, $j, 3600); + my $ref = XMLin($j); + return $ref; + } else { + print STDERR "No reply from $url\n"; + } + return undef; + } + return XMLin($result); +} + +sub _osmxml_to_hash { + my ($xml, $type) = @_; + my $ref = XMLin($xml); + my %tags; + if ('ARRAY' eq ref $ref->{$type}->{tag}) { + map { $tags{$_->{'k'}} = $_->{'v'} } @{$ref->{$type}->{tag}}; + return \%tags; + } else { + return undef; + } +} + +sub get_object_tags { + my ($type, $id) = @_; + my $url = "${osmapibase}0.6/$type/$id"; + my $key = "OSM:get_object_tags:$url"; + my $result = Memcached::get($key); + unless ($result) { + my $j = LWP::Simple::get($url); + if ($j) { + Memcached::set($key, $j, 3600); + return _osmxml_to_hash($j, $type); + } else { + print STDERR "No reply from $url\n"; + } + return undef; + } + return _osmxml_to_hash($result, $type); +} + +# A better alternative might be +# http://www.geonames.org/maps/osm-reverse-geocoder.html#findNearbyStreetsOSM +sub get_nearest_road_tags { + my ( $cobrand, $latitude, $longitude ) = @_; + my $inforef = reverse_geocode($latitude, $longitude, 16); + if (exists $inforef->{result}->{osm_type} + && 'way' eq $inforef->{result}->{osm_type}) { + my $osmtags = get_object_tags('way', + $inforef->{result}->{osm_id}); + unless ( exists $osmtags->{operator} ) { + $osmtags->{operatorguess} = $cobrand->guess_road_operator( $osmtags ); + } + return $osmtags; + } + return undef; +} + +sub closest_road_text { + my ( $cobrand, $latitude, $longitude ) = @_; + my $str = ''; + my $osmtags = get_nearest_road_tags( $cobrand, $latitude, $longitude ); + if ($osmtags) { + my ($name, $ref) = ('',''); + $name = $osmtags->{name} if exists $osmtags->{name}; + $ref = " ($osmtags->{ref})" if exists $osmtags->{ref}; + if ($name || $ref) { + $str .= _('The following information about the nearest road might be inaccurate or irrelevant, if the problem is close to several roads or close to a road without a name registered in OpenStreetMap.') . "\n\n"; + $str .= sprintf(_("Nearest named road to the pin placed on the map (automatically generated using OpenStreetMap): %s%s"), + $name, $ref) . "\n\n"; + + if (my $operator = $osmtags->{operator}) { + $str .= sprintf(_("Road operator for this named road (from OpenStreetMap): %s"), + $operator) . "\n\n"; + } elsif ($operator = $osmtags->{operatorguess}) { + $str .= sprintf(_("Road operator for this named road (derived from road reference number and type): %s"), + $operator) . "\n\n"; + } + } + } + return $str; +} + +1; + diff --git a/perllib/FixMyStreet/Map/OSM/MapQuest.pm b/perllib/FixMyStreet/Map/OSM/MapQuest.pm index ff314a4da..a7f1b334e 100644 --- a/perllib/FixMyStreet/Map/OSM/MapQuest.pm +++ b/perllib/FixMyStreet/Map/OSM/MapQuest.pm @@ -15,6 +15,10 @@ sub map_type { return 'OpenLayers.Layer.OSM.MapQuestOpen'; } +sub map_template { + return 'mapquest-attribution'; +} + sub map_tiles { my ( $self, %params ) = @_; my ( $x, $y, $z ) = ( $params{x_tile}, $params{y_tile}, $params{zoom_act} ); diff --git a/perllib/Open311.pm b/perllib/Open311.pm index 25119b867..c8289a442 100644 --- a/perllib/Open311.pm +++ b/perllib/Open311.pm @@ -172,19 +172,18 @@ sub _generate_service_request_description { my $problem = shift; my $extra = shift; - my $description = <<EOT; -detail: @{[$problem->detail()]} - -url: $extra->{url} - -Submitted via FixMyStreet -EOT -; - if ($self->extended_description ne 'oxfordshire') { - $description = <<EOT . $description; -title: @{[$problem->title()]} - -EOT + my $description = ""; + if ($problem->cobrand eq 'fixmystreet') { + $description .= "detail: " . $problem->detail . "\n\n"; + $description .= "url: " . $extra->{url} . "\n\n"; + $description .= "Submitted via FixMyStreet\n"; + if ($self->extended_description ne 'oxfordshire') { + $description = "title: " . $problem->title . "\n\n$description"; + } + } else { + $description .= $problem->title . "\n\n"; + $description .= $problem->detail . "\n\n"; + $description .= $extra->{url} . "\n"; } return $description; diff --git a/perllib/Open311/GetUpdates.pm b/perllib/Open311/GetUpdates.pm new file mode 100644 index 000000000..db5d8ef35 --- /dev/null +++ b/perllib/Open311/GetUpdates.pm @@ -0,0 +1,84 @@ +package Open311::GetUpdates; + +use Moose; +use Open311; +use FixMyStreet::App; + +has council_list => ( is => 'ro' ); +has system_user => ( is => 'ro' ); + +sub get_updates { + my $self = shift; + + while ( my $council = $self->council_list->next ) { + my $open311 = Open311->new( + endpoint => $council->endpoint, + jurisdiction => $council->jurisdiction, + api_key => $council->api_key + ); + + my $area_id = $council->area_id; + + my $council_details = mySociety::MaPit::call( 'area', $area_id ); + + my $reports = FixMyStreet::App->model('DB::Problem')->search( + { + council => { like => "\%$area_id\%" }, + state => { 'IN', [qw/confirmed fixed/] }, + -and => [ + external_id => { '!=', undef }, + external_id => { '!=', '' }, + ], + } + ); + + my @report_ids = (); + while ( my $report = $reports->next ) { + push @report_ids, $report->external_id; + } + + next unless @report_ids; + + $self->update_reports( \@report_ids, $open311, $council_details ); + } +} + +sub update_reports { + my ( $self, $report_ids, $open311, $council_details ) = @_; + + my $service_requests = $open311->get_service_requests( $report_ids ); + + my $requests; + + # XML::Simple is a bit inconsistent in how it structures + # things depending on the number of children an element has :( + if ( ref $service_requests->{request} eq 'ARRAY' ) { + $requests = $service_requests->{request}; + } + else { + $requests = [ $service_requests->{request} ]; + } + + for my $request (@$requests) { + # if it's a ref that means it's an empty element + # however, if there's no updated date then we can't + # tell if it's newer that what we have so we should skip it + next if ref $request->{updated_datetime} || ! exists $request->{updated_datetime}; + + my $request_id = $request->{service_request_id}; + + my $problem = + FixMyStreet::App->model('DB::Problem') + ->search( { external_id => $request_id, } ); + + if (my $p = $problem->first) { + my $cobrand = FixMyStreet::Cobrand->get_class_for_moniker($p->cobrand)->new(); + $cobrand->set_lang_and_domain($p->lang, 1); + $p->update_from_open311_service_request( $request, $council_details, $self->system_user ); + } + } + + return 1; +} + +1; |