Skip to content

Commit 2ee6ad1

Browse files
committedAug 14, 2023
[Central Beds] Use Singlepoint to find the nearest address.
1 parent f6e9574 commit 2ee6ad1

File tree

8 files changed

+242
-40
lines changed

8 files changed

+242
-40
lines changed
 

‎conf/council-centralbedfordshire_jadu.yml-example

+8-4
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
1-
api_base_url: ''
2-
api_key: ''
3-
username: ''
4-
password: ''
1+
jadu_api_base_url: ''
2+
jadu_api_key: ''
3+
jadu_username: ''
4+
jadu_password: ''
5+
6+
singlepoint_api_base_url: ''
7+
singlepoint_api_key: ''
8+
reverse_geocode_radius_meters: 100
59

610
case_type: ''
711
sys_channel: ''

‎perllib/Geocode/SinglePoint.pm

+79
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
package Geocode::SinglePoint;
2+
3+
use v5.14;
4+
use warnings;
5+
6+
use Data::Dumper;
7+
use LWP::UserAgent;
8+
use Moo;
9+
use XML::Simple qw(:strict);
10+
11+
with 'Role::Config';
12+
with 'Role::Logger';
13+
14+
has base_url => (
15+
is => 'lazy',
16+
default => sub { $_[0]->config->{singlepoint_api_base_url} }
17+
);
18+
19+
has api_key => (
20+
is => 'lazy',
21+
default => sub { $_[0]->config->{singlepoint_api_key} }
22+
);
23+
24+
has ua => (
25+
is => 'lazy',
26+
default => sub { LWP::UserAgent->new(agent => "FixMyStreet/open311-adapter") }
27+
);
28+
29+
sub _fail {
30+
my ($self, $message, $url, $response) = @_;
31+
my $log = sprintf(
32+
"%s
33+
Requested: %s
34+
Got: %s",
35+
$message, $url, $response->content
36+
);
37+
$self->logger->error($log);
38+
die $message;
39+
}
40+
41+
sub _get_field_value_for_tag {
42+
my ($self, $dom, $tag) = @_;
43+
return $self->xml_path_context->findvalue('./x:FieldItems/x:FieldInfo/x:Value[../x:Tag="'. $tag . '"]', $dom);
44+
}
45+
46+
sub get_nearest_addresses {
47+
my ($self, $easting, $northing, $radius_meters, $address_field_tags) = @_;
48+
my $url = sprintf(
49+
"%sSpatialRadialSearchByEastingNorthing?apiKey=%s&adapterName=LLPG&easting=%s&northing=%s&unit=Meter&distance=%s",
50+
$self->base_url,
51+
$self->api_key,
52+
$easting,
53+
$northing,
54+
$radius_meters,
55+
);
56+
my $response = $self->ua->get($url);
57+
if (!$response->is_success) {
58+
$self->_fail("Request failed.", $url, $response);
59+
}
60+
my $x = XML::Simple->new(ForceArray => ["SearchResultItem", "FieldInfo"], NoAttr => 1, SuppressEmpty => "", KeyAttr => ["Tag"]);
61+
my $xml = $x->XMLin($response->content);
62+
my $results = $xml->{Results}{Items}{SearchResultItem};
63+
64+
my @addresses;
65+
# Results are already ordered nearest-first.
66+
foreach my $result (@$results) {
67+
68+
my %address;
69+
foreach my $address_field_tag (@$address_field_tags) {
70+
my $field = $result->{FieldItems}{FieldInfo}{$address_field_tag};
71+
$address{$address_field_tag} = $field->{Value} if $field;
72+
}
73+
push @addresses, \%address if %address;
74+
}
75+
76+
return \@addresses;
77+
}
78+
79+
1;

‎perllib/Integrations/Jadu.pm

+4-4
Original file line numberDiff line numberDiff line change
@@ -16,22 +16,22 @@ with 'Role::Memcached';
1616

1717
has base_url => (
1818
is => 'lazy',
19-
default => sub { $_[0]->config->{api_base_url} }
19+
default => sub { $_[0]->config->{jadu_api_base_url} }
2020
);
2121

2222
has api_key => (
2323
is => 'lazy',
24-
default => sub { $_[0]->config->{api_key} }
24+
default => sub { $_[0]->config->{jadu_api_key} }
2525
);
2626

2727
has username => (
2828
is => 'lazy',
29-
default => sub { $_[0]->config->{username} }
29+
default => sub { $_[0]->config->{jadu_username} }
3030
);
3131

3232
has password => (
3333
is => 'lazy',
34-
default => sub { $_[0]->config->{password} }
34+
default => sub { $_[0]->config->{jadu_password} }
3535
);
3636

3737
has ua => (

‎perllib/Open311/Endpoint/Integration/UK/CentralBedfordshire/Jadu.pm

+60-6
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ use DateTime::Format::ISO8601;
2828
use DateTime::Format::W3CDTF;
2929
use Fcntl qw(:flock);
3030
use File::Temp qw(tempfile);
31+
use Geocode::SinglePoint;
3132
use Integrations::Jadu;
3233
use JSON::MaybeXS qw(decode_json encode_json);
3334
use LWP::Simple;
@@ -46,6 +47,11 @@ has jurisdiction_id => (
4647
default => 'centralbedfordshire_jadu',
4748
);
4849

50+
has singlepoint => (
51+
is => 'lazy',
52+
default => sub { Geocode::SinglePoint->new(config_filename => $_[0]->jurisdiction_id) }
53+
);
54+
4955
has jadu => (
5056
is => 'lazy',
5157
default => sub { Integrations::Jadu->new(config_filename => $_[0]->jurisdiction_id) }
@@ -68,6 +74,17 @@ sub get_integration {
6874
return $_[0]->jadu;
6975
}
7076

77+
=head2 reverse_geocode_radius_meters
78+
79+
This is the radius in meters of the area around the report location to search for addresses.
80+
81+
=cut
82+
83+
has reverse_geocode_radius_meters => (
84+
is => 'lazy',
85+
default => sub { $_[0]->endpoint_config->{reverse_geocode_radius_meters} }
86+
);
87+
7188
=head2 sys_channel
7289
7390
This is the value to set for the 'sys-channel' field when creating a new Fly Tipping case.
@@ -94,6 +111,7 @@ has case_type => (
94111
95112
This is a mapping from any town associated with an address in Central Bedfordshire to
96113
the value that should be set in the 'eso-officer' field when creating a new Fly Tipping case.
114+
Towns must be specified in lowercase.
97115
98116
=cut
99117

@@ -204,9 +222,45 @@ sub post_service_request {
204222
my ($self, $service, $args) = @_;
205223
my $attributes = $args->{attributes};
206224

207-
my $officer = $self->town_to_officer->{$attributes->{town}};
208-
if (!$officer) {
209-
die "No officer found for town " . $attributes->{town};
225+
my $addresses = $self->singlepoint->get_nearest_addresses(
226+
$attributes->{easting},
227+
$attributes->{northing},
228+
$self->reverse_geocode_radius_meters,
229+
['STREET', 'TOWN', 'USRN'],
230+
);
231+
232+
if ($addresses == 0) {
233+
die sprintf(
234+
"No addresses found within %dm of easting: %d northing: %d",
235+
$self->reverse_geocode_radius_meters,
236+
$attributes->{easting},
237+
$attributes->{northing},
238+
);
239+
}
240+
241+
my $officer;
242+
my $nearest_valid_address;
243+
foreach my $address (@$addresses) {
244+
my $usrn = $address->{USRN};
245+
my $street = $address->{STREET};
246+
my $town = $address->{TOWN};
247+
248+
unless ($usrn && $street && $town) {
249+
$self->logger->warn("Skipping address missing one or more of USRN, STREET and TOWN");
250+
next;
251+
}
252+
253+
$officer = $self->town_to_officer->{lc $town};
254+
if (!$officer) {
255+
$self->logger->warn("Skipping address with unmapped town: " . $town);
256+
next;
257+
}
258+
$nearest_valid_address = $address;
259+
last;
260+
}
261+
262+
if (!$nearest_valid_address) {
263+
die "None of the addresses found were valid.";
210264
}
211265

212266
my $google_street_view_url = sprintf(
@@ -226,9 +280,9 @@ sub post_service_request {
226280
'ens-latitude' => $args->{lat},
227281
'ens-longitude' => $args->{long},
228282
'ens-google-street-view-url' => $google_street_view_url,
229-
'usrn' => $attributes->{usrn},
230-
'ens-street' => $attributes->{street},
231-
'sys-town' => $attributes->{town},
283+
'usrn' => $nearest_valid_address->{USRN},
284+
'ens-street' => $nearest_valid_address->{STREET},
285+
'sys-town' => $nearest_valid_address->{TOWN},
232286
'eso-officer' => $officer,
233287
'ens-location-description' => $attributes->{title},
234288
'ens-land-type' => $attributes->{land_type},

‎perllib/Open311/Endpoint/Service/UKCouncil/CentralBedfordshireFlytipping.pm

+1-22
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
package Open311::Endpoint::Service::UKCouncil::CentralBedfordshireFlytipping;
22
use Moo;
3-
extends 'Open311::Endpoint::Service';
3+
extends 'Open311::Endpoint::Service::UKCouncil';
44

55
use Open311::Endpoint::Service::Attribute;
66

@@ -31,27 +31,6 @@ sub _build_attributes {
3131
description => "Report URL",
3232
automated => 'server_set'
3333
),
34-
Open311::Endpoint::Service::Attribute->new(
35-
code => "usrn",
36-
description => "USRN",
37-
datatype => "string",
38-
required => 1,
39-
automated => 'server_set'
40-
),
41-
Open311::Endpoint::Service::Attribute->new(
42-
code => "street",
43-
description => "Street",
44-
datatype => "string",
45-
required => 1,
46-
automated => 'server_set'
47-
),
48-
Open311::Endpoint::Service::Attribute->new(
49-
code => "town",
50-
description => "Town",
51-
datatype => "string",
52-
required => 1,
53-
automated => 'server_set'
54-
),
5534
Open311::Endpoint::Service::Attribute->new(
5635
code => "land_type",
5736
variable => 1,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<SearchResultData xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://www.aligned-assets.co.uk/SinglePoint/Search/WebServices/V2/SearchService">
3+
<Results>
4+
<Items>
5+
<SearchResultItem>
6+
<FieldItems>
7+
<FieldInfo>
8+
<Tag>USRN</Tag>
9+
<Value>25202550</Value>
10+
</FieldInfo>
11+
<FieldInfo>
12+
<Tag>STREET</Tag>
13+
<Value>Monks Walk</Value>
14+
</FieldInfo>
15+
<FieldInfo>
16+
<Tag>TOWN</Tag>
17+
<Value>Chicksands</Value>
18+
</FieldInfo>
19+
</FieldItems>
20+
</SearchResultItem>
21+
<SearchResultItem>
22+
<FieldItems>
23+
<FieldInfo>
24+
<Tag>USRN</Tag>
25+
<Value>25201736</Value>
26+
</FieldInfo>
27+
<FieldInfo>
28+
<Tag>STREET</Tag>
29+
<Value>Northwood End Road</Value>
30+
</FieldInfo>
31+
<FieldInfo>
32+
<Tag>TOWN</Tag>
33+
<Value>Haynes</Value>
34+
</FieldInfo>
35+
</FieldItems>
36+
</SearchResultItem>
37+
</Items>
38+
</Results>
39+
</SearchResultData>

‎t/geocode/singlepoint.t

+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
use strict;
2+
use warnings;
3+
4+
use Test::MockModule;
5+
use Test::More;
6+
7+
use Geocode::SinglePoint;
8+
use LWP::UserAgent;
9+
use Moo;
10+
use Path::Tiny;
11+
12+
BEGIN { $ENV{TEST_MODE} = 1; }
13+
14+
subtest "get_nearest_addresses" => sub {
15+
my $ua = Test::MockModule->new('LWP::UserAgent');
16+
$ua->mock('get', sub {
17+
my $resp = HTTP::Response->new(200);
18+
$resp->content(path(__FILE__)->sibling("files/singlepoint/spatial_radial_search_by_easting_and_northing_response.xml")->slurp);
19+
return $resp;
20+
});
21+
my $singlepoint = Geocode::SinglePoint->new();
22+
# Call with arbitrary easting, northing and radius.
23+
my $addresses = $singlepoint->get_nearest_addresses(0, 0, 0, ["STREET", "USRN", "TOWN"]);
24+
25+
is_deeply $addresses, [
26+
{
27+
'TOWN' => 'Chicksands',
28+
'STREET' => 'Monks Walk',
29+
'USRN' => '25202550'
30+
},
31+
{
32+
'TOWN' => 'Haynes',
33+
'STREET' => 'Northwood End Road',
34+
'USRN' => '25201736'
35+
},
36+
];
37+
};
38+
39+
done_testing;

‎t/open311/endpoint/centralbedfordshire_jadu.t

+12-4
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,12 @@ my $config_string = '
3333
update_storage_file: "%s"
3434
update_storage_max_age_days: 365
3535
town_to_officer:
36-
"Chicksands": "area_1"';
36+
"chicksands": "area_1"';
3737

3838
my $config = Test::MockFile->file( "/config.yml", sprintf($config_string, $status_tracking_file, $update_storage_file));
3939

4040
my $integration = Test::MockModule->new('Integrations::Jadu');
41+
my $geocode = Test::MockModule->new('Geocode::SinglePoint');
4142

4243
my $centralbedfordshire_jadu = Test::MockModule->new('Open311::Endpoint::Integration::UK::CentralBedfordshire::Jadu');
4344
$centralbedfordshire_jadu->mock('_build_config_file', sub { '/config.yml' });
@@ -52,6 +53,16 @@ subtest "POST service request" => sub {
5253
return "test_case_reference";
5354
});
5455

56+
$geocode->mock('get_nearest_addresses', sub {
57+
return [
58+
{
59+
USRN => 25202550,
60+
STREET => "Monk's Walk",
61+
TOWN => "Chicksands",
62+
}
63+
]
64+
});
65+
5566
my $res = $endpoint->run_test_request(
5667
POST => '/requests.json',
5768
jurisdiction_id => 'centralbedfordshire_jadu',
@@ -75,9 +86,6 @@ subtest "POST service request" => sub {
7586
'attribute[fly_tip_witnessed]' => 'Yes',
7687
'attribute[fly_tip_date_and_time]' => '2023-07-03T14:30:36Z',
7788
'attribute[description_of_alleged_offender]' => 'Stealthy.',
78-
'attribute[usrn]' => '25202550',
79-
'attribute[street]' => "Monk's Walk",
80-
'attribute[town]' => "Chicksands",
8189
);
8290

8391
ok $res->is_success, 'valid request'

0 commit comments

Comments
 (0)
Please sign in to comment.