1 package OpenILS::Application::Geo;
6 use OpenSRF::AppSession;
7 use OpenILS::Application;
8 use base qw/OpenILS::Application/;
10 use OpenILS::Utils::CStoreEditor qw/:funcs/;
11 use OpenILS::Utils::Fieldmapper;
12 use OpenILS::Application::AppUtils;
13 my $U = "OpenILS::Application::AppUtils";
15 use OpenSRF::Utils::Logger qw/$logger/;
19 use Geo::Coder::Google;
21 use Math::Trig qw(great_circle_distance deg2rad);
23 sub calculate_distance {
24 my ($self, $conn, $pointA, $pointB) = @_;
26 return new OpenILS::Event("BAD_PARAMS", "desc" => "Missing coordinates") unless $pointA;
27 return new OpenILS::Event("BAD_PARAMS", "desc" => "Missing coordinates") unless $pointB;
28 return new OpenILS::Event("BAD_PARAMS", "desc" => "Malformed coordinates") unless scalar(@{ $pointA }) == 2;
29 return new OpenILS::Event("BAD_PARAMS", "desc" => "Malformed coordinates") unless scalar(@{ $pointB }) == 2;
31 sub NESW { deg2rad($_[0]), deg2rad(90 - $_[1]) }
32 my @A = NESW( $pointA->[0], $pointA->[1] );
33 my @B = NESW( $pointB->[0], $pointB->[1] );
34 my $km = great_circle_distance(@A, @B, 6378);
38 __PACKAGE__->register_method(
39 method => "calculate_distance",
40 api_name => "open-ils.geo.calculate_distance",
43 {type => 'array', desc => 'An array containing latitude and longitude for point A'},
44 {type => 'array', desc => 'An array containing latitude and longitude for point B'}
46 return => { desc => '"Great Circle (as the crow flies)" distance between points A and B in kilometers'}
50 sub sort_orgs_by_distance_from_coordinate {
51 my ($self, $conn, $pointA, $orgs) = @_;
53 return new OpenILS::Event("BAD_PARAMS", "desc" => "Missing coordinates") unless $pointA;
54 return new OpenILS::Event("BAD_PARAMS", "desc" => "Malformed coordinates") unless scalar(@{ $pointA }) == 2;
55 return new OpenILS::Event("BAD_PARAMS", "desc" => "Missing org list") unless $orgs;
56 return new OpenILS::Event("BAD_PARAMS", "desc" => "Empty org list") unless scalar(@{ $orgs }) > 0;
58 my $e = new_editor(xact => 1);
60 my $fleshed_orgs = $e->search_actor_org_unit([
65 "flesh_fields" => {"aou" => ["billing_address"]}
67 ]) or return (undef, $e->die_event);
69 my @orgs_with_coordinates = grep {
70 defined $_->billing_address
71 && defined $_->billing_address->latitude
72 && defined $_->billing_address->longitude } @$fleshed_orgs;
73 my @orgs_without_coordinates = grep {
74 !defined $_->billing_address
75 || !defined $_->billing_address->latitude
76 || !defined $_->billing_address->longitude } @$fleshed_orgs;
78 my @org_ids_with_distances = map {
79 [ $_->id, calculate_distance($self, $conn, $pointA, [
80 $_->billing_address->latitude,
81 $_->billing_address->longitude
83 } @orgs_with_coordinates;
85 my @sorted_orgs = sort { $a->[1] <=> $b->[1] } @org_ids_with_distances;
86 push @sorted_orgs, map { [ $_->id, -1 ] } sort { $a->name cmp $b->name } @orgs_without_coordinates;
87 my @sorted_org_ids = map { $_->[0] } @sorted_orgs;
89 return $self->api_name =~ /include_distances/ ? \@sorted_orgs : \@sorted_org_ids;
91 __PACKAGE__->register_method(
92 method => "sort_orgs_by_distance_from_coordinate",
93 api_name => "open-ils.geo.sort_orgs_by_distance_from_coordinate",
96 {type => 'array', desc => 'An array containing latitude and longitude for the reference point'},
97 {type => 'array', desc => 'An array of Context Organizational Unit IDs'}
99 return => { desc => 'An array of Context Organizational Unit IDs sorted by geographic proximity to the reference point (closest first). Units without coordinates are appended to the end of the list in alphabetical order by name relative to each other.'}
102 __PACKAGE__->register_method(
103 method => "sort_orgs_by_distance_from_coordinate",
104 api_name => "open-ils.geo.sort_orgs_by_distance_from_coordinate.include_distances",
107 {type => 'array', desc => 'An array containing latitude and longitude for the reference point'},
108 {type => 'array', desc => 'An array of Context Organizational Unit IDs'}
110 return => { desc => 'An array of Context Organizational Unit IDs and distances (each pair itself an array) sorted by geographic proximity to the reference point (closest first). Units without coordinates are appended to the end of the list in alphabetical order by name relative to each other and given a distance of -1.'}
115 sub retrieve_coordinates { # invoke 3rd party API for latitude/longitude lookup
116 my ($self, $conn, $org, $address) = @_;
118 my $e = new_editor(xact => 1);
119 # TODO: if we're not going to require authentication, we may want to consider
120 # implementing some options for limiting outgoing geo-coding API calls
121 # return $e->die_event unless $e->checkauth;
123 my $use_geo = $e->retrieve_config_global_flag('opac.use_geolocation');
124 $use_geo = ($use_geo and $U->is_true($use_geo->enabled));
125 return new OpenILS::Event("GEOCODING_NOT_ENABLED") unless ($U->is_true($use_geo));
127 return new OpenILS::Event("BAD_PARAMS", "desc" => "No org ID supplied") unless $org;
128 my $service_id = $U->ou_ancestor_setting_value($org, 'opac.geographic_location_service_for_address');
129 return new OpenILS::Event("GEOCODING_NOT_ALLOWED") unless ($U->is_true($service_id));
131 my $service = $e->retrieve_config_geolocation_service($service_id);
132 return new OpenILS::Event("GEOCODING_NOT_ALLOWED") unless ($U->is_true($service));
134 return new OpenILS::Event("BAD_PARAMS", "desc" => "No address supplied") unless $address;
137 if ($service->service_code eq 'Free') {
138 $logger->debug("Using Geo::Coder::Free (service id $service_id)");
139 $geo_coder = Geo::Coder::Free->new();
140 } elsif ($service->service_code eq 'Google') {
141 $logger->debug("Using Geo::Coder::Google (service id $service_id)");
142 $geo_coder = Geo::Coder::Google->new(key => $service->api_key);
144 $logger->debug("Using Geo::Coder::OSM (service id $service_id)");
145 $geo_coder = Geo::Coder::OSM->new();
148 if ($@ || !$geo_coder) {
149 $logger->error("geosort: problem creating Geo::Coder instance : $@");
150 return OpenILS::Event->new('GEOCODING_LOCATION_NOT_FOUND');
154 $location = $geo_coder->geocode(location => $address);
157 $logger->error("geosort: problem invoking location lookup : $@");
158 return OpenILS::Event->new('GEOCODING_LOCATION_NOT_FOUND');
161 my $latitude; my $longitude;
162 return new OpenILS::Event("GEOCODING_LOCATION_NOT_FOUND") unless ($U->is_true($location));
163 if ($service->service_code eq 'Free') {
164 $latitude = $location->{'latitude'};
165 $longitude = $location->{'longitude'};
166 } elsif ($service->service_code eq 'Google') {
167 $latitude = $location->{'geometry'}->{'location'}->{'lat'};
168 $longitude = $location->{'geometry'}->{'location'}->{'lng'};
170 $latitude = $location->{lat};
171 $longitude = $location->{lon};
174 return { latitude => $latitude, longitude => $longitude }
176 __PACKAGE__->register_method(
177 method => "retrieve_coordinates",
178 api_name => "open-ils.geo.retrieve_coordinates",
181 {type => 'number', desc => 'Context Organizational Unit'},
182 {type => 'string', desc => 'Address to look-up as a text string'}
184 return => { desc => 'Hash/object containing latitude and longitude for the provided address.'}