<field name="street2" reporter:label="Street2" reporter:datatype="text"/>
<field name="valid" reporter:label="Is Valid?" reporter:datatype="bool" oils_obj:required="true"/>
<field name="san" reporter:label="SAN" reporter:datatype="text"/>
+ <field name="latitude" reporter:label="Latitude" reporter:datatype="float"/>
+ <field name="longitude" reporter:label="Longitude" reporter:datatype="float"/>
</fields>
<links>
<link field="org_unit" reltype="has_a" key="id" map="" class="aou"/>
</actions>
</permacrud>
</class>
+ <class id="cgs" controller="open-ils.cstore open-ils.pcrud" oils_obj:fieldmapper="config::geolocation_service" oils_persist:tablename="config.geolocation_service" reporter:label="Geographic Location Service">
+ <fields oils_persist:primary="id" oils_persist:sequence="config.geolocation_service_id_seq">
+ <field name="id" reporter:selector="name" reporter:datatype="id" reporter:label="ID"/>
+ <field reporter:label="Active" name="active" reporter:datatype="bool" oils_obj:required="true"/>
+ <field reporter:label="Owner" name="owner" reporter:datatype="link" oils_obj:required="true"/>
+ <field name="name" reporter:datatype="text" oils_persist:i18n="true" reporter:label="Name"/>
+ <field reporter:label="Service Code" name="service_code" reporter:datatype="text"/>
+ <field reporter:label="API Key" name="api_key" reporter:datatype="text"/>
+ </fields>
+ <links>
+ <link field="owner" reltype="has_a" key="id" class="aou"/>
+ </links>
+ <permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1">
+ <actions>
+ <create permission="ADMIN_GEOLOCATION_SERVICES" global_required="true"/>
+ <retrieve permission="VIEW_GEOLOCATION_SERVICES" global_required="true"/>
+ <update permission="ADMIN_GEOLOCATION_SERVICES" global_required="true"/>
+ <delete permission="ADMIN_GEOLOCATION_SERVICES" global_required="true"/>
+ </actions>
+ </permacrud>
+ </class>
<!-- ********************************************************************************************************************* -->
</IDL>
</app_settings>
</open-ils.booking>
+ <open-ils.geo>
+ <keepalive>5</keepalive>
+ <stateless>1</stateless>
+ <language>perl</language>
+ <implementation>OpenILS::Application::Geo</implementation>
+ <max_requests>199</max_requests>
+ <unix_config>
+ <unix_sock>open-ils.geo_unix.sock</unix_sock>
+ <unix_pid>open-ils.geo_unix.pid</unix_pid>
+ <max_requests>1000</max_requests>
+ <unix_log>open-ils.geo_unix.log</unix_log>
+ <min_children>1</min_children>
+ <max_children>15</max_children>
+ <min_spare_children>1</min_spare_children>
+ <max_spare_children>5</max_spare_children>
+ </unix_config>
+ <app_settings>
+ </app_settings>
+ </open-ils.geo>
+
<open-ils.cat>
<keepalive>5</keepalive>
<stateless>1</stateless>
<appname>open-ils.ebook_api</appname>
<appname>open-ils.courses</appname>
<appname>open-ils.curbside</appname>
+ <appname>open-ils.geo</appname>
</activeapps>
</localhost>
</hosts>
<service>open-ils.courses</service>
<service>open-ils.curbside</service>
<service>open-ils.fielder</service>
+ <service>open-ils.geo</service>
<service>open-ils.pcrud</service>
<service>open-ils.permacrud</service>
<service>open-ils.reporter</service>
url="/eg/staff/admin/server/actor/org_unit_custom_tree"></eg-link-table-link>
<eg-link-table-link i18n-label label="Floating Groups"
routerLink="/staff/admin/server/config/floating_group"></eg-link-table-link>
+ <eg-link-table-link i18n-label label="Geographic Location Service"
+ routerLink="/staff/admin/server/config/geolocation_service"></eg-link-table-link>
<eg-link-table-link i18n-label label="Global Flags"
routerLink="/staff/admin/server/config/global_flag"></eg-link-table-link>
<eg-link-table-link i18n-label label="Hard Due Date Changes"
[record]="addr(type)"
fieldOrder="address_type,street1,street2,city,county,state,country,post_code,san,valid"
>
+ <eg-fm-record-editor-action i18n-label label="Get Coordinates"
+ (actionClick)="getCoordinates($event)">
+ </eg-fm-record-editor-action>
<eg-fm-record-editor-action i18n-label label="Delete" *ngIf="!addr(type).isnew()"
(actionClick)="deleteAddress($event)" buttonCss="btn-warning">
</eg-fm-record-editor-action>
import {IdlService, IdlObject} from '@eg/core/idl.service';
import {OrgService} from '@eg/core/org.service';
import {PcrudService} from '@eg/core/pcrud.service';
+import {NetService} from '@eg/core/net.service';
+import {AuthService} from '@eg/core/auth.service';
import {NgbTabChangeEvent} from '@ng-bootstrap/ng-bootstrap';
const ADDR_TYPES =
constructor(
private idl: IdlService,
private org: OrgService,
- private pcrud: PcrudService
+ private pcrud: PcrudService,
+ private auth: AuthService,
+ private net: NetService
) {
this.addrChange = new EventEmitter<IdlObject>();
this.tabName = 'billing_address';
return org;
}
+ getCoordinates($event: any) {
+ const addr = $event.record;
+
+ this.net.request(
+ 'open-ils.actor',
+ 'open-ils.actor.geo.retrieve_coordinates',
+ this.auth.token(),
+ addr.org_unit(),
+ addr.street1() + ' ' + addr.street2() + ', '
+ + addr.city() + ', ' + addr.state() + ' ' + addr.post_code()
+ + ' ' + addr.country()
+ ).subscribe(
+ (res) => {
+ addr.latitude( res.latitude );
+ addr.longitude( res.longitude );
+ },
+ (err) => {
+ console.error(err);
+ }
+ );
+ }
}
<event code='7032' textcode='CURBSIDE_EXISTS'>
<desc xml:lang="en-US">A scheduled, unfilled curbside request already exists</desc>
</event>
+ <event code='7033' textcode='GEOCODING_NOT_ENABLED'>
+ <desc xml:lang="en-US">Geo-Coding is not enabled for this installation</desc>
+ </event>
+ <event code='7034' textcode='GEOCODING_NOT_ALLOWED'>
+ <desc xml:lang="en-US">A Geographic Location Service is not configured for this library</desc>
+ </event>
+ <event code='7035' textcode='GEOCODING_LOCATION_NOT_FOUND'>
+ <desc xml:lang="en-US">No location returned by Geographic Location Service</desc>
+ </event>
<!-- ================================================================ -->
serve-cgi-bin
export CPAN_MODULES = \
+ Geo::Coder::Free \
+ Geo::Coder::OSM \
+ Geo::Coder::Google \
Business::OnlinePayment::PayPal \
Email::Send
serve-cgi-bin
export CPAN_MODULES = \
+ Geo::Coder::Free \
+ Geo::Coder::OSM \
+ Geo::Coder::Google \
Business::OnlinePayment::PayPal \
Email::Send
serve-cgi-bin
export CPAN_MODULES = \
+ Geo::Coder::Free \
+ Geo::Coder::OSM \
+ Geo::Coder::Google \
Business::OnlinePayment::PayPal \
Email::Send
yaz
export CPAN_MODULES = \
+ Geo::Coder::Free \
+ Geo::Coder::OSM \
+ Geo::Coder::Google \
Excel::Writer::XLSX \
Business::ISSN \
Net::Z3950::ZOOM \
serve-cgi-bin
export CPAN_MODULES = \
+ Geo::Coder::Free \
+ Geo::Coder::OSM \
+ Geo::Coder::Google \
Business::OnlinePayment::PayPal \
Email::Send \
MARC::Charset \
serve-cgi-bin
export CPAN_MODULES = \
+ Geo::Coder::Free \
+ Geo::Coder::OSM \
+ Geo::Coder::Google \
Business::OnlinePayment::PayPal \
Email::Send \
MARC::Charset \
$org_id );
}
+__PACKAGE__->register_method(
+ method => "retrieve_coordinates",
+ api_name => "open-ils.actor.geo.retrieve_coordinates",
+ signature => {
+ params => [
+ {desc => 'Authentication token', type => 'string' },
+ {type => 'number', desc => 'Context Organizational Unit'},
+ {type => 'string', desc => 'Address to look-up as a text string'}
+ ],
+ return => { desc => 'Hash/object containing latitude and longitude for the provided address.'}
+ }
+);
+
+sub retrieve_coordinates {
+ my( $self, $client, $auth, $org_id, $addr_string ) = @_;
+ my $e = new_editor(authtoken=>$auth);
+ return $e->event unless $e->checkauth;
+ $org_id = $e->requestor->ws_ou unless defined $org_id;
+
+ return $apputils->simple_scalar_request(
+ "open-ils.geo",
+ "open-ils.geo.retrieve_coordinates",
+ $org_id, $addr_string );
+}
__PACKAGE__->register_method(
method => "patron_adv_search",
--- /dev/null
+package OpenILS::Application::Geo;
+
+use strict;
+use warnings;
+
+use OpenSRF::AppSession;
+use OpenILS::Application;
+use base qw/OpenILS::Application/;
+
+use OpenILS::Utils::CStoreEditor qw/:funcs/;
+use OpenILS::Utils::Fieldmapper;
+use OpenILS::Application::AppUtils;
+my $U = "OpenILS::Application::AppUtils";
+
+use OpenSRF::Utils::Logger qw/$logger/;
+
+use Geo::Coder::Free;
+use Geo::Coder::OSM;
+use Geo::Coder::Google;
+
+use Math::Trig qw(great_circle_distance deg2rad);
+
+sub calculate_distance {
+ my ($self, $conn, $pointA, $pointB) = @_;
+
+ return new OpenILS::Event("BAD_PARAMS", "desc" => "Missing coordinates") unless $pointA;
+ return new OpenILS::Event("BAD_PARAMS", "desc" => "Missing coordinates") unless $pointB;
+ return new OpenILS::Event("BAD_PARAMS", "desc" => "Malformed coordinates") unless scalar(@{ $pointA }) == 2;
+ return new OpenILS::Event("BAD_PARAMS", "desc" => "Malformed coordinates") unless scalar(@{ $pointB }) == 2;
+
+ sub NESW { deg2rad($_[0]), deg2rad(90 - $_[1]) }
+ my @A = NESW( $pointA->[0], $pointA->[1] );
+ my @B = NESW( $pointB->[0], $pointB->[1] );
+ my $km = great_circle_distance(@A, @B, 6378);
+
+ return $km;
+}
+__PACKAGE__->register_method(
+ method => "calculate_distance",
+ api_name => "open-ils.geo.calculate_distance",
+ signature => {
+ params => [
+ {type => 'array', desc => 'An array containing latitude and longitude for point A'},
+ {type => 'array', desc => 'An array containing latitude and longitude for point B'}
+ ],
+ return => { desc => '"Great Circle (as the crow flies)" distance between points A and B in kilometers'}
+ }
+);
+
+sub sort_orgs_by_distance_from_coordinate {
+ my ($self, $conn, $pointA, $orgs) = @_;
+
+ return new OpenILS::Event("BAD_PARAMS", "desc" => "Missing coordinates") unless $pointA;
+ return new OpenILS::Event("BAD_PARAMS", "desc" => "Malformed coordinates") unless scalar(@{ $pointA }) == 2;
+ return new OpenILS::Event("BAD_PARAMS", "desc" => "Missing org list") unless $orgs;
+ return new OpenILS::Event("BAD_PARAMS", "desc" => "Empty org list") unless scalar(@{ $orgs }) > 0;
+
+ my $e = new_editor(xact => 1);
+
+ my $fleshed_orgs = $e->search_actor_org_unit([
+ {
+ "id" => $orgs
+ }, {
+ "flesh" => 1,
+ "flesh_fields" => {"aou" => ["billing_address"]}
+ }
+ ]) or return (undef, $e->die_event);
+
+ my @orgs_with_coordinates = grep {
+ defined $_->billing_address
+ && defined $_->billing_address->latitude
+ && defined $_->billing_address->longitude } @$fleshed_orgs;
+ my @orgs_without_coordinates = grep {
+ !defined $_->billing_address
+ || !defined $_->billing_address->latitude
+ || !defined $_->billing_address->longitude } @$fleshed_orgs;
+
+ my @org_ids_with_distances = map {
+ [ $_->id, calculate_distance($self, $conn, $pointA, [
+ $_->billing_address->latitude,
+ $_->billing_address->longitude
+ ]) ]
+ } @orgs_with_coordinates;
+
+ my @sorted_orgs = sort { $a->[1] <=> $b->[1] } @org_ids_with_distances;
+ push @sorted_orgs, map { [ $_->id, -1 ] } sort { $a->name cmp $b->name } @orgs_without_coordinates;
+ my @sorted_org_ids = map { $_->[0] } @sorted_orgs;
+
+ return $self->api_name =~ /include_distances/ ? \@sorted_orgs : \@sorted_org_ids;
+}
+__PACKAGE__->register_method(
+ method => "sort_orgs_by_distance_from_coordinate",
+ api_name => "open-ils.geo.sort_orgs_by_distance_from_coordinate",
+ signature => {
+ params => [
+ {type => 'array', desc => 'An array containing latitude and longitude for the reference point'},
+ {type => 'array', desc => 'An array of Context Organizational Unit IDs'}
+ ],
+ 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.'}
+ }
+);
+__PACKAGE__->register_method(
+ method => "sort_orgs_by_distance_from_coordinate",
+ api_name => "open-ils.geo.sort_orgs_by_distance_from_coordinate.include_distances",
+ signature => {
+ params => [
+ {type => 'array', desc => 'An array containing latitude and longitude for the reference point'},
+ {type => 'array', desc => 'An array of Context Organizational Unit IDs'}
+ ],
+ 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.'}
+ }
+);
+
+
+sub retrieve_coordinates { # invoke 3rd party API for latitude/longitude lookup
+ my ($self, $conn, $org, $address) = @_;
+
+ my $e = new_editor(xact => 1);
+ # TODO: if we're not going to require authentication, we may want to consider
+ # implementing some options for limiting outgoing geo-coding API calls
+ # return $e->die_event unless $e->checkauth;
+
+ my $use_geo = $e->retrieve_config_global_flag('opac.use_geolocation');
+ $use_geo = ($use_geo and $U->is_true($use_geo->enabled));
+ return new OpenILS::Event("GEOCODING_NOT_ENABLED") unless ($U->is_true($use_geo));
+
+ return new OpenILS::Event("BAD_PARAMS", "desc" => "No org ID supplied") unless $org;
+ my $service_id = $U->ou_ancestor_setting_value($org, 'opac.geographic_location_service_for_address');
+ return new OpenILS::Event("GEOCODING_NOT_ALLOWED") unless ($U->is_true($service_id));
+
+ my $service = $e->retrieve_config_geolocation_service($service_id);
+ return new OpenILS::Event("GEOCODING_NOT_ALLOWED") unless ($U->is_true($service));
+
+ return new OpenILS::Event("BAD_PARAMS", "desc" => "No address supplied") unless $address;
+ my $geo_coder;
+ eval {
+ if ($service->service_code eq 'Free') {
+ $logger->debug("Using Geo::Coder::Free (service id $service_id)");
+ $geo_coder = Geo::Coder::Free->new();
+ } elsif ($service->service_code eq 'Google') {
+ $logger->debug("Using Geo::Coder::Google (service id $service_id)");
+ $geo_coder = Geo::Coder::Google->new(key => $service->api_key);
+ } else {
+ $logger->debug("Using Geo::Coder::OSM (service id $service_id)");
+ $geo_coder = Geo::Coder::OSM->new();
+ }
+ };
+ if ($@ || !$geo_coder) {
+ $logger->error("geosort: problem creating Geo::Coder instance : $@");
+ return OpenILS::Event->new('GEOCODING_LOCATION_NOT_FOUND');
+ }
+ my $location;
+ eval {
+ $location = $geo_coder->geocode(location => $address);
+ };
+ if ($@) {
+ $logger->error("geosort: problem invoking location lookup : $@");
+ return OpenILS::Event->new('GEOCODING_LOCATION_NOT_FOUND');
+ }
+
+ my $latitude; my $longitude;
+ return new OpenILS::Event("GEOCODING_LOCATION_NOT_FOUND") unless ($U->is_true($location));
+ if ($service->service_code eq 'Free') {
+ $latitude = $location->{'latitude'};
+ $longitude = $location->{'longitude'};
+ } elsif ($service->service_code eq 'Google') {
+ $latitude = $location->{'geometry'}->{'location'}->{'lat'};
+ $longitude = $location->{'geometry'}->{'location'}->{'lng'};
+ } else {
+ $latitude = $location->{lat};
+ $longitude = $location->{lon};
+ }
+
+ return { latitude => $latitude, longitude => $longitude }
+}
+__PACKAGE__->register_method(
+ method => "retrieve_coordinates",
+ api_name => "open-ils.geo.retrieve_coordinates",
+ signature => {
+ params => [
+ {type => 'number', desc => 'Context Organizational Unit'},
+ {type => 'string', desc => 'Address to look-up as a text string'}
+ ],
+ return => { desc => 'Hash/object containing latitude and longitude for the provided address.'}
+ }
+);
+
+1;
$ctx->{carousel_loc} = $self->get_carousel_loc;
$ctx->{physical_loc} = $self->get_physical_loc;
+ my $geo_sort = $e->retrieve_config_global_flag('opac.use_geolocation');
+ $geo_sort = ($geo_sort && $U->is_true($geo_sort->enabled));
+ my $geo_org = $ctx->{physical_loc} || $self->cgi->param('loc') || $ctx->{aou_tree}->()->id;
+ my $geo_sort_for_org = $ctx->{get_org_setting}->($geo_org, 'opac.holdings_sort_by_geographic_proximity');
+ $ctx->{geo_sort} = $geo_sort && $U->is_true($geo_sort_for_org);
# capture some commonly accessed pages
$ctx->{home_page} = $ctx->{proto} . '://' . $ctx->{hostname} . $self->ctx->{opac_root} . "/home";
--- /dev/null
+#!perl
+use strict; use warnings;
+use Test::More tests => 8;
+use OpenILS::Utils::TestUtils;
+use OpenILS::Const qw(:const);
+use OpenILS::Utils::CStoreEditor qw/:funcs/;
+use OpenILS::Utils::Fieldmapper;
+
+diag("test geocoding");
+
+my $U = 'OpenILS::Application::AppUtils';
+my $script = OpenILS::Utils::TestUtils->new();
+$script->bootstrap;
+
+my $geo_session = $script->session('open-ils.geo');
+
+my $request = $geo_session->request(
+ 'open-ils.geo.retrieve_coordinates',
+ 4,
+ '30016'
+);
+my $result = $request->recv();
+my $content = $result->content();
+is($content->{textcode},'GEOCODING_NOT_ENABLED','received expected GEOCODING_NOT_ENABLED');
+
+my $e = new_editor(xact => 1);
+$e->init;
+
+my $flag = $e->retrieve_config_global_flag('opac.use_geolocation');
+$flag->enabled('t');
+my $stat = $e->update_config_global_flag($flag);
+ok($stat, 'opac.use_geolocation enabled');
+$e->xact_commit;
+
+$request = $geo_session->request(
+ 'open-ils.geo.retrieve_coordinates',
+ 4,
+ '30016'
+);
+$result = $request->recv();
+$content = $result->content();
+is($content->{textcode},'GEOCODING_NOT_ALLOWED','received expected GEOCODING_NOT_ALLOWED');
+
+$e->xact_begin;
+my $cgs = Fieldmapper::config::geolocation_service->new;
+$cgs->active('t');
+$cgs->owner(1);
+$cgs->name('OSM');
+$cgs->service_code('OSM');
+$stat = $e->create_config_geolocation_service($cgs);
+ok($stat, 'Geolocation service created successfully');
+$e->xact_commit;
+
+$script->authenticate({
+ username => 'admin',
+ password => 'demo123',
+ type => 'staff'});
+
+my $authtoken = $script->authtoken;
+ok($authtoken, 'Have an authtoken');
+
+my $setting_value = $U->simplereq(
+ 'open-ils.actor',
+ 'open-ils.actor.org_unit.settings.update',
+ $authtoken,
+ 4,
+ {'opac.geographic_location_service_for_address', 1}
+);
+ok(
+ ! ref $setting_value,
+ 'opac.geographic_location_service_for_address set for BR1'
+);
+
+$request = $geo_session->request(
+ 'open-ils.geo.retrieve_coordinates',
+ 4,
+ '30016'
+);
+$result = $request->recv();
+$content = $result->content();
+use Data::Dumper;
+diag(Dumper($content));
+ok(
+ $content->{latitude},
+ 'Result contains latitude'
+);
+ok(
+ $content->{latitude},
+ 'Result contains longitude'
+);
+$request->finish();
+
SELECT SETVAL('config.carousel_type_id_seq'::TEXT, 100);
+CREATE TABLE config.geolocation_service (
+ id SERIAL PRIMARY KEY,
+ active BOOLEAN,
+ owner INT NOT NULL, -- REFERENCES actor.org_unit (id)
+ name TEXT,
+ service_code TEXT,
+ api_key TEXT
+);
+
COMMIT;
state TEXT,
country TEXT NOT NULL,
post_code TEXT NOT NULL,
- san TEXT
+ san TEXT,
+ latitude FLOAT,
+ longitude FLOAT
);
CREATE INDEX actor_org_address_org_unit_idx ON actor.org_address (org_unit);
ALTER TABLE config.print_template ADD CONSTRAINT cpt_owner_fkey
FOREIGN KEY (owner) REFERENCES actor.org_unit(id) DEFERRABLE INITIALLY DEFERRED;
+ALTER TABLE config.geolocation_service ADD CONSTRAINT cgs_owner_fkey
+ FOREIGN KEY (owner) REFERENCES actor.org_unit(id) DEFERRABLE INITIALLY DEFERRED;
+
COMMIT;
'Manage batch (subscription) hold events', 'ppl', 'description')),
( 629, 'ADMIN_LIBRARY_GROUPS', oils_i18n_gettext(629,
'Administer library groups', 'ppl', 'description'))
+ ( 630, 'VIEW_GEOLOCATION_SERVICES', oils_i18n_gettext(630,
+ 'View geographic location services', 'ppl', 'description')),
+ ( 631, 'ADMIN_GEOLOCATION_SERVICES', oils_i18n_gettext(631,
+ 'Administer geographic location services', 'ppl', 'description'))
;
'Curbside', 'Confirm that curbside pickup is enabled for the hold pickup library'
);
+-- geosort
+
+INSERT INTO config.global_flag (name, value, enabled, label)
+VALUES (
+ 'opac.use_geolocation',
+ NULL,
+ FALSE,
+ oils_i18n_gettext(
+ 'opac.use_geolocation',
+ 'Offer use of geographic location services in the public catalog',
+ 'cgf', 'label'
+ )
+);
+
+INSERT INTO config.org_unit_setting_type (name, label, grp, description, datatype)
+VALUES (
+ 'opac.holdings_sort_by_geographic_proximity',
+ oils_i18n_gettext('opac.holdings_sort_by_geographic_proximity',
+ 'Enable Holdings Sort by Geographic Proximity',
+ 'coust', 'label'),
+ 'opac',
+ oils_i18n_gettext('opac.holdings_sort_by_geographic_proximity',
+ 'When set to TRUE, will cause the record details page to display the controls for sorting holdings by geographic proximity. This also depends on the global flag opac.use_geolocation being enabled.',
+ 'coust', 'description'),
+ 'bool'
+);
+
+INSERT INTO config.org_unit_setting_type (name, label, grp, description, datatype)
+VALUES (
+ 'opac.geographic_proximity_in_miles',
+ oils_i18n_gettext('opac.geographic_proximity_in_miles',
+ 'Show Geographic Proximity in Miles',
+ 'coust', 'label'),
+ 'opac',
+ oils_i18n_gettext('opac.geographic_proximity_in_miles',
+ 'When set to TRUE, will cause the record details page to show distances for geographic proximity in miles instead of kilometers.',
+ 'coust', 'description'),
+ 'bool'
+);
+
+INSERT INTO config.org_unit_setting_type (name, label, grp, description, datatype, fm_class)
+VALUES (
+ 'opac.geographic_location_service_for_address',
+ oils_i18n_gettext('opac.geographic_location_service_for_address',
+ 'Geographic Location Service to use for Addresses',
+ 'coust', 'label'),
+ 'opac',
+ oils_i18n_gettext('opac.geographic_location_service_for_address',
+ 'Specifies which geographic location service to use for converting address input to geographic coordinates.',
+ 'coust', 'description'),
+ 'link', 'cgs'
+);
+
+INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable)
+ SELECT
+ pgt.id, perm.id, aout.depth, TRUE
+ FROM
+ permission.grp_tree pgt,
+ permission.perm_list perm,
+ actor.org_unit_type aout
+ WHERE
+ (pgt.name = 'Global Administrator' OR pgt.name = 'System Administrator') AND
+ aout.name = 'Consortium' AND
+ (perm.code = 'ADMIN_GEOLOCATION_SERVICES' OR perm.code = 'VIEW_GEOLOCATION_SERVICES');
+
------------------- Disabled example A/T defintions ------------------------------
-- Create a "dummy" slot when applicable, and trigger the "offer curbside" events
--- /dev/null
+BEGIN;
+
+-- check whether patch can be applied
+SELECT evergreen.upgrade_deps_block_check('XXXX', :eg_version);
+
+-- 005.schema.actors.sql
+
+-- CREATE TABLE actor.org_address (
+-- ...
+-- latitude FLOAT,
+-- longitude FLOAT
+-- );
+
+ALTER TABLE actor.org_address ADD COLUMN latitude FLOAT;
+ALTER TABLE actor.org_address ADD COLUMN longitude FLOAT;
+
+-- 002.schema.config.sql
+
+CREATE TABLE config.geolocation_service (
+ id SERIAL PRIMARY KEY,
+ active BOOLEAN,
+ owner INT NOT NULL, -- REFERENCES actor.org_unit (id)
+ name TEXT,
+ service_code TEXT,
+ api_key TEXT
+);
+
+-- 800.fkeys.sql
+
+ALTER TABLE config.geolocation_service ADD CONSTRAINT cgs_owner_fkey
+ FOREIGN KEY (owner) REFERENCES actor.org_unit(id) DEFERRABLE INITIALLY DEFERRED;
+
+-- 950.data.seed-values.sql
+
+INSERT INTO config.global_flag (name, value, enabled, label)
+VALUES (
+ 'opac.use_geolocation',
+ NULL,
+ FALSE,
+ oils_i18n_gettext(
+ 'opac.use_geolocation',
+ 'Offer use of geographic location services in the public catalog',
+ 'cgf', 'label'
+ )
+);
+
+INSERT INTO config.org_unit_setting_type (name, label, grp, description, datatype)
+VALUES (
+ 'opac.holdings_sort_by_geographic_proximity',
+ oils_i18n_gettext('opac.holdings_sort_by_geographic_proximity',
+ 'Enable Holdings Sort by Geographic Proximity',
+ 'coust', 'label'),
+ 'opac',
+ oils_i18n_gettext('opac.holdings_sort_by_geographic_proximity',
+ 'When set to TRUE, will cause the record details page to display the controls for sorting holdings by geographic proximity. This also depends on the global flag opac.use_geolocation being enabled.',
+ 'coust', 'description'),
+ 'bool'
+);
+
+INSERT INTO config.org_unit_setting_type (name, label, grp, description, datatype)
+VALUES (
+ 'opac.geographic_proximity_in_miles',
+ oils_i18n_gettext('opac.geographic_proximity_in_miles',
+ 'Show Geographic Proximity in Miles',
+ 'coust', 'label'),
+ 'opac',
+ oils_i18n_gettext('opac.geographic_proximity_in_miles',
+ 'When set to TRUE, will cause the record details page to show distances for geographic proximity in miles instead of kilometers.',
+ 'coust', 'description'),
+ 'bool'
+);
+
+INSERT INTO config.org_unit_setting_type (name, label, grp, description, datatype, fm_class)
+VALUES (
+ 'opac.geographic_location_service_for_address',
+ oils_i18n_gettext('opac.geographic_location_service_for_address',
+ 'Geographic Location Service to use for Addresses',
+ 'coust', 'label'),
+ 'opac',
+ oils_i18n_gettext('opac.geographic_location_service_for_address',
+ 'Specifies which geographic location service to use for converting address input to geographic coordinates.',
+ 'coust', 'description'),
+ 'link', 'cgs'
+);
+
+INSERT INTO permission.perm_list ( id, code, description ) VALUES
+ ( 630, 'VIEW_GEOLOCATION_SERVICES', oils_i18n_gettext(630,
+ 'View geographic location services', 'ppl', 'description')),
+ ( 631, 'ADMIN_GEOLOCATION_SERVICES', oils_i18n_gettext(631,
+ 'Administer geographic location services', 'ppl', 'description'))
+;
+
+COMMIT;
total_copies = ctx.copy_summary.$depth.count;
%]
[% use_courses = (ctx.get_org_setting(ctx.aou_tree.id, 'circ.course_materials_opt_in') == 1) ? 1 : 0 %]
+[% IF ctx.geo_sort %]
+<th colspan="6">
+ [% l("Sort by distance from:") %]
+ <input type="text" id="geographic-location-box" name="geographic-location" aria-label="[% l('Enter address or postal code') %]" placeholder="[% l('Enter address/postal code') %]" class="search-box" x-webkit-speech=""></input>
+ <input id="geographic-location-submit-go" type="submit" value="[% l('Go') %]" class="opac-button" onclick=""></input>
+</th>
+[% END %]
<table class="table_no_border_space table_no_cell_pad table_no_border" width="100%" id="rdetails_status">
<thead>
<tr>
--- /dev/null
+Sort Holdings by Geographical Proximity
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+This functionality integrates 3rd party geographic lookup services to allow patrons
+to enter an address on the record details page in the OPAC and sort the holdings
+for that record based on proximity of their circulating libraries to the entered
+address. To support this, latitude and longitude coordinates may be associated with
+each org unit. Care is given to not log or leak patron provided addresses or the
+context in which they are used.
+
+Requires the following Perl modules: Geo::Coder::Free, Geo::Coder::Google, and Geo::Coder::OSM