1 package OpenILS::Application::Geo;
6 use OpenSRF::AppSession;
7 use OpenILS::Application;
8 use base qw/OpenILS::Application/;
10 use OpenSRF::Utils::SettingsClient;
11 use OpenILS::Utils::CStoreEditor qw/:funcs/;
12 use OpenILS::Utils::Fieldmapper;
13 use OpenSRF::Utils::Cache;
14 use OpenILS::Application::AppUtils;
15 my $U = "OpenILS::Application::AppUtils";
17 use OpenSRF::Utils::Logger qw/$logger/;
21 use Geo::Coder::Google;
23 use Math::Trig qw(great_circle_distance deg2rad);
24 use Digest::SHA qw(sha256_base64);
30 my $conf = OpenSRF::Utils::SettingsClient->new;
32 $cache_timeout = $conf->config_value(
33 "apps", "open-ils.geo", "app_settings", "cache_timeout" ) || 300;
36 $cache = OpenSRF::Utils::Cache->new('global');
39 sub calculate_distance {
40 my ($self, $conn, $pointA, $pointB) = @_;
42 return new OpenILS::Event("BAD_PARAMS", "desc" => "Missing coordinates") unless $pointA;
43 return new OpenILS::Event("BAD_PARAMS", "desc" => "Missing coordinates") unless $pointB;
44 return new OpenILS::Event("BAD_PARAMS", "desc" => "Malformed coordinates") unless scalar(@{ $pointA }) == 2;
45 return new OpenILS::Event("BAD_PARAMS", "desc" => "Malformed coordinates") unless scalar(@{ $pointB }) == 2;
47 sub NESW { deg2rad($_[1]), deg2rad(90 - $_[0]) } # longitude, latitude
48 my @A = NESW( $pointA->[0], $pointA->[1] );
49 my @B = NESW( $pointB->[0], $pointB->[1] );
50 my $km = great_circle_distance(@A, @B, 6378);
54 __PACKAGE__->register_method(
55 method => "calculate_distance",
56 api_name => "open-ils.geo.calculate_distance",
59 {type => 'array', desc => 'An array containing latitude and longitude for point A'},
60 {type => 'array', desc => 'An array containing latitude and longitude for point B'}
62 return => { desc => '"Great Circle (as the crow flies)" distance between points A and B in kilometers'}
66 sub sort_orgs_by_distance_from_coordinate {
67 my ($self, $conn, $pointA, $orgs) = @_;
69 return new OpenILS::Event("BAD_PARAMS", "desc" => "Missing coordinates") unless $pointA;
70 return new OpenILS::Event("BAD_PARAMS", "desc" => "Malformed coordinates") unless scalar(@{ $pointA }) == 2;
71 return new OpenILS::Event("BAD_PARAMS", "desc" => "Missing org list") unless $orgs;
72 return new OpenILS::Event("BAD_PARAMS", "desc" => "Empty org list") unless scalar(@{ $orgs }) > 0;
74 my $e = new_editor(xact => 1);
76 my $fleshed_orgs = $e->search_actor_org_unit([
81 "flesh_fields" => {"aou" => ["billing_address"]}
83 ]) or return (undef, $e->die_event);
85 my @orgs_with_coordinates = grep {
86 defined $_->billing_address
87 && defined $_->billing_address->latitude
88 && defined $_->billing_address->longitude } @$fleshed_orgs;
89 my @orgs_without_coordinates = grep {
90 !defined $_->billing_address
91 || !defined $_->billing_address->latitude
92 || !defined $_->billing_address->longitude } @$fleshed_orgs;
94 my @org_ids_with_distances = map {
95 [ $_->id, calculate_distance($self, $conn, $pointA, [
96 $_->billing_address->latitude,
97 $_->billing_address->longitude
99 } @orgs_with_coordinates;
101 my @sorted_orgs = sort { $a->[1] <=> $b->[1] } @org_ids_with_distances;
102 push @sorted_orgs, map { [ $_->id, -1 ] } sort { $a->name cmp $b->name } @orgs_without_coordinates;
103 my @sorted_org_ids = map { $_->[0] } @sorted_orgs;
105 return $self->api_name =~ /include_distances/ ? \@sorted_orgs : \@sorted_org_ids;
107 __PACKAGE__->register_method(
108 method => "sort_orgs_by_distance_from_coordinate",
109 api_name => "open-ils.geo.sort_orgs_by_distance_from_coordinate",
112 {type => 'array', desc => 'An array containing latitude and longitude for the reference point'},
113 {type => 'array', desc => 'An array of Context Organizational Unit IDs'}
115 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.'}
118 __PACKAGE__->register_method(
119 method => "sort_orgs_by_distance_from_coordinate",
120 api_name => "open-ils.geo.sort_orgs_by_distance_from_coordinate.include_distances",
123 {type => 'array', desc => 'An array containing latitude and longitude for the reference point'},
124 {type => 'array', desc => 'An array of Context Organizational Unit IDs'}
126 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.'}
131 sub retrieve_coordinates { # invoke 3rd party API for latitude/longitude lookup
132 my ($self, $conn, $org, $address) = @_;
134 my $e = new_editor(xact => 1);
135 # TODO: if we're not going to require authentication, we may want to consider
136 # implementing some options for limiting outgoing geo-coding API calls
137 # return $e->die_event unless $e->checkauth;
139 my $use_geo = $e->retrieve_config_global_flag('opac.use_geolocation');
140 $use_geo = ($use_geo and $U->is_true($use_geo->enabled));
141 return new OpenILS::Event("GEOCODING_NOT_ENABLED") unless ($U->is_true($use_geo));
143 return new OpenILS::Event("BAD_PARAMS", "desc" => "No org ID supplied") unless $org;
144 my $service_id = $U->ou_ancestor_setting_value($org, 'opac.geographic_location_service_for_address');
145 return new OpenILS::Event("GEOCODING_NOT_ALLOWED") unless ($U->is_true($service_id));
147 my $service = $e->retrieve_config_geolocation_service($service_id);
148 return new OpenILS::Event("GEOCODING_NOT_ALLOWED") unless ($U->is_true($service));
150 $address =~ s/^\s+//;
151 $address =~ s/\s+$//;
152 return new OpenILS::Event("BAD_PARAMS", "desc" => "No address supplied") unless $address;
154 # Return cached coordinates if available. We're assuming that any
155 # geolocation service will give roughly equivalent results, so we're
156 # using a hash of the user-supplied address as the cache key, not
158 my $cache_key = 'geo.address.' . sha256_base64($address);
159 my $coords = OpenSRF::Utils::JSON->JSON2perl($cache->get_cache($cache_key));
160 return $coords if $coords;
164 if ($service->service_code eq 'Free') {
165 $logger->debug("Using Geo::Coder::Free (service id $service_id)");
166 $geo_coder = Geo::Coder::Free->new();
167 } elsif ($service->service_code eq 'Google') {
168 $logger->debug("Using Geo::Coder::Google (service id $service_id)");
169 $geo_coder = Geo::Coder::Google->new(key => $service->api_key);
171 $logger->debug("Using Geo::Coder::OSM (service id $service_id)");
172 $geo_coder = Geo::Coder::OSM->new();
175 if ($@ || !$geo_coder) {
176 $logger->error("geosort: problem creating Geo::Coder instance : $@");
177 return OpenILS::Event->new('GEOCODING_LOCATION_NOT_FOUND');
181 $location = $geo_coder->geocode(location => $address);
184 $logger->error("geosort: problem invoking location lookup : $@");
185 return OpenILS::Event->new('GEOCODING_LOCATION_NOT_FOUND');
188 my $latitude; my $longitude;
189 return new OpenILS::Event("GEOCODING_LOCATION_NOT_FOUND") unless ($U->is_true($location));
190 if ($service->service_code eq 'Free') {
191 $latitude = $location->{'latitude'};
192 $longitude = $location->{'longitude'};
193 } elsif ($service->service_code eq 'Google') {
194 $latitude = $location->{'geometry'}->{'location'}->{'lat'};
195 $longitude = $location->{'geometry'}->{'location'}->{'lng'};
197 $latitude = $location->{lat};
198 $longitude = $location->{lon};
200 $coords = { latitude => $latitude, longitude => $longitude };
201 $cache->put_cache($cache_key, OpenSRF::Utils::JSON->perl2JSON($coords), $cache_timeout);
205 __PACKAGE__->register_method(
206 method => "retrieve_coordinates",
207 api_name => "open-ils.geo.retrieve_coordinates",
210 {type => 'number', desc => 'Context Organizational Unit'},
211 {type => 'string', desc => 'Address to look-up as a text string'}
213 return => { desc => 'Hash/object containing latitude and longitude for the provided address.'}