The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
package Geo::Distance::Google;

use strict;
use warnings;

our $VERSION = '0.01';

use Carp;
use Data::Dumper qw( Dumper );
use Encode;
use JSON;
use HTTP::Request;
use LWP::UserAgent;
use Params::Validate;
use URI;

sub new {
    my($class, %param) = @_;

    my $ua       = delete $param{ua}       || LWP::UserAgent->new(agent => __PACKAGE__ . "/$VERSION");
    my $host     = delete $param{host}     || 'maps.googleapis.com';

    my $language = delete $param{language} || delete $param{hl};
    my $region   = delete $param{region}   || delete $param{gl};
    my $oe       = delete $param{oe}       || 'utf8';
    my $sensor   = delete $param{sensor}   || 0;
    my $client   = delete $param{client}   || '';
    my $key      = delete $param{key}      || '';
    my $units    = delete $param{units}    || '';
    my $mode     = delete $param{mode};
    my $avoid    = delete $param{avoid};
    my $https    = delete $param{https}    || 0;
    my $debug    = delete $param{debug}    || 0;
   
    bless { 
        ua => $ua, 
        host => $host, 
        language => $language, 
        region => $region, 
        oe => $oe, 
        sensor => $sensor,
        client => $client, 
        key => $key, 
        units  => $units,
        mode => $mode,
        avoid => $avoid,
        https => $https,
        __debug__ => $debug
    }, $class;
}

sub debug_level {
    my $self = shift;

    if ( @_ ) { $self->{__debug__} = shift; }

    return $self->{__debug__};
}

sub ua {
    my $self = shift;
    if (@_) {
        $self->{ua} = shift;
    }
    $self->{ua};
}

sub raw_distance {
    my $self = shift;

    $self->{__raw_response__} = shift if @_;

    return $self->{__raw_response__};
}

sub distance {
    my $self = shift;
    my %p    = validate @_, { 
        origins      => 1, 
        destinations => 1, 
        mode         => 0,
        avoid        => 0,
        units        => 0
    };

    my $origins;
    my $destinations;

    # both can be array refs or single items
    foreach my $k ( qw( origins destinations ) ) {
        # convert to google format

        # TODO: in future allow seperate lat & long in hash ref
        if ( ref $p{$k} ne 'ARRAY' ) {
            $p{$k} = [ $p{$k} ];
        }
    }

    $origins      = join '|', @{ $p{origins} };
    $destinations = join '|', @{ $p{destinations} };


    $origins      = Encode::is_utf8( $origins ) ? Encode::encode_utf8( $origins ) : $origins;
    $destinations = Encode::is_utf8( $destinations ) ? Encode::encode_utf8( $destinations ) : $destinations;

    my $url = sprintf "%s://%s/maps/api/distancematrix/json",
        ( $self->{https} ? 'https' : 'http' ), $self->{host};

    my $uri = URI->new($url);

    # build query
    my %query_parameters = (
        origins      => $origins,
        destinations => $destinations,
        oe           => $self->{oe},
        sensor       => ( $self->{sensor} ? 'true' : 'false' ),
        # optional parameters
        ( $self->{units} ? ( units => $self->{units} ) : () ),
        ( $p{mode}       ? ( mode => $p{mode} ) 
            : defined $self->{mode} ? ( mode => $self->{mode} ) : ()),
        # TODO: add support for avoid as list ref process too
        ( $p{avoid} ? ( avoid => $p{avoid} )
            : defined $self->{avoid} ? ( avoid => $self->{avoid} ) : () )
    );

    # not sure about these
    $query_parameters{language} = $self->{language} if defined $self->{language};
    $query_parameters{region}   = $self->{region}   if defined $self->{region};

    $uri->query_form(%query_parameters);

    # setup request
    $url = $uri->as_string;

    # Process Maps Premier account info
    if ($self->{client} and $self->{key}) {
        $query_parameters{client} = $self->{client};
        $uri->query_form(%query_parameters);

        my $signature = $self->make_signature($uri);
        # signature must be last parameter in query string or you get 403's
        $url = $uri->as_string;
        $url .= '&signature='.$signature if $signature;
    }

    $self->debug( "Sending request: $url" );

    my $res = $self->{ua}->get($url);

    if ($res->is_error) {
        Carp::croak("Google Maps API returned error: " . $res->status_line);
    }

    if ( $res->headers->content_type !~ /json/ ) {
        my $ct = $res->headers->content_type;
        croak "Invalid content-type '$ct' returned from webserver";
    }

    my $json = JSON->new->utf8;
    my $data = $json->decode($res->content);

    $self->raw_distance( $data );

    $self->debug( "data: " . Dumper( $data ) );

    if ( ! defined $data->{status} || $data->{status} ne 'OK' ) {
        croak "Google Maps API status error: " . ( $data->{status} || 'Invalid status' );
    }

    # reprocess to make more friendly (IMO)
    my $distance = [];

    # origins[0] correspond to rows[0]
    # destinations[0] correspond to rows->[x]->elements[0]
    for ( my $oid = 0; $oid < scalar( @{ $p{origins} } ); $oid++ ) {

        # verify origin information
        next unless defined $data->{origin_addresses} &&
            defined $data->{origin_addresses}->[$oid];

        # missing return data
        next unless defined $data->{rows} && $data->{rows}->[$oid];

        $distance->[$oid]->{origin_address} = $data->{origin_addresses}->[$oid];

        my $elements = $data->{rows}->[$oid]->{elements};

        # loop through each destination address
        foreach ( my $did = 0; $did < scalar( @{ $p{destinations} } ); $did++ ) {
            next unless defined $elements->[$did];

            # reformat it to be a bit nicer for the consumer
            $distance->[$oid]->{destinations}->[$did] = {
                address  => $data->{destination_addresses}->[$did],
                distance => $elements->[$did]->{distance}, 
                duration => $elements->[$did]->{duration}, 
                status   => $elements->[$did]->{status}
            };
        }

    }

    $self->debug( "distance: " . Dumper($distance) );

    return $distance;
}

# methods below adapted from 
# http://gmaps-samples.googlecode.com/svn/trunk/urlsigning/urlsigner.pl
sub decode_urlsafe_base64 {
  my ($self, $content) = @_;

  $content =~ tr/-/\+/;
  $content =~ tr/_/\//;

  return MIME::Base64::decode_base64($content);
}

sub encode_urlsafe{
  my ($self, $content) = @_;
  $content =~ tr/\+/\-/;
  $content =~ tr/\//\_/;

  return $content;
}

sub make_signature {
  my ($self, $uri) = @_;

  require Digest::HMAC_SHA1;
  require MIME::Base64;

  my $key = $self->decode_urlsafe_base64($self->{key});
  my $to_sign = $uri->path_query;

  my $digest = Digest::HMAC_SHA1->new($key);
  $digest->add($to_sign);
  my $signature = $digest->b64digest;

  return $self->encode_urlsafe($signature);
}

# search input hash ref, then self for defined parameter or
# return empty list
sub _get_multiple {
    my $self = shift;
    my $p    = shift || return (); # params
    my $key  = shift || return (); # key

    return () unless ref $p eq 'HASH';

    return defined $p->{$key} ? ( $key => $p->{$key} ) 
        : defined $self->{$key} ? ( $key => $self->{$key} ) : ();
}

sub debug {
    my $self = shift;
    my $f = (caller(1))[3];

    return unless $self->debug_level;

    printf STDERR "%s [%s] %s\n", scalar( localtime ), $f, shift;
}


1;

__END__

=head1 NAME

Geo::Distance::Google - Google Maps Distance API

=head1 SYNOPSIS

  use Geo::Distance::Google;

  my $geo = Geo::Distance::Google->new; 

  my $distance = $geo->distance(
    # sears tower... wacker tower whatever
    origins      => '233 S. Wacker Drive Chicago, Illinois 60606',
    destinations => '1600 Amphitheatre Parkway, Mountain View, CA'
  );

  printf "The distance between: %s and %s is %s\n",
     $distance->[0]->{origin_address},
     $distance->[0]->{destinations}->[0]->{address},
     $distance->[0]->{destinations}->[0]->{distance}->{text};

=head1 DESCRIPTION

Geo::Distance::Google provides a distance and duration functionality using Google Maps API.

=head1 METHODS

=head2 new

Create new geo distance object.

  $geo = Geo::Distance::Google->new( https => 1 );
  $geo = Geo::Distance::Google->new( language => 'ru' );
  $geo = Geo::Distance::Google->new( gl => 'ca' );

=head3 Parameters (all are optional)

=over 4

=item * ua - L<LWP::UserAgent> object or compatiable 

=item * host - url of api host

=item * language - Google's response will be in this language (when possible by google)

=item * region - region for usage 

=item * sensor - true (1) when pulling data from GPS sensor

=item * mode - mode of transport (default: driving)

=item * avoid - restrictions applied to directions (supports 'tolls' or 'highways')

=item * https - set to true will make requests with https

=item * debug - true will output internal debugging info

=back 

You can optionally use your Maps Premier Client ID, by passing your client
code as the C<client> parameter and your private key as the C<key> parameter.
The URL signing for Premier Client IDs requires the I<Digest::HMAC_SHA1>
and I<MIME::Base64> modules. To test your client, set the environment
variables GMAP_CLIENT and GMAP_KEY before running 02_v3_live.t

  GMAP_CLIENT=your_id GMAP_KEY='your_key' make test

=head2 distance 

  $distance = $geo->distance( 
    origins      => '233 S. Wacker Drive Chicago, Illinois 60606',
    destinations => '1600 Amphitheatre Parkway, Mountain View, CA'
  )

  # multiple origins
  $distance = $geo->distance( 
    origins      => [ 
      'One MetLife Stadium Drive, East Rutherford, New Jersey 07073, United States',
      '602 Jamestown Avenue, San Francisco, California 94124'
    ],
    destinations => '1265 Lombardi Avenue, Green Bay, Wisconsin 54304'
  );

  # lat and lng
  $distance => $geo->distance( 
    origins      => '34.101063,-118.3385319',
    destinations => '34.1613743,118.1676149'
  );

Queries I<$geo> to Google Maps distance API and returns list
reference that contains each origin to destination mapping.

Data returned from second example above looks like:

  [
    {
      'origin_address' => 'MetLife Stadium, 102 Stadium Rd, East Rutherford, NJ 07073, USA',
      'destinations' => [
        {
          'distance' => {
             'value' => 1587392,
              'text' => '1,587 km'
           },
           'status'   => 'OK',
           'duration' => {
             'value' => 60332,
             'text'  => '16 hours 46 mins'
           },
           'address' => '1265 Lombardi Ave, Green Bay, WI 54304, USA'
         }
       ]
     },
     {
       'origin_address' => '602 Jamestown Ave, San Francisco, CA 94124, USA',
       'destinations' => [
         {
           'distance' => {
             'value' => 3615562,
             'text'  => '3,616 km'
           },
           'status' => 'OK',
           'duration' => {
             'value' => 127697,
             'text'  => '1 day 11 hours'
           },
           'address' => '1265 Lombardi Ave, Green Bay, WI 54304, USA'
         }
       ]
     }
  ]

When you'd like to pass non-ascii string as a location, you should
pass it as either UTF-8 bytes or Unicode flagged string.

=head2 ua

Accessor method to get and set UserAgent object used internally. You
can call I<env_proxy> for example, to get the proxy information from
environment variables:

  $coder->ua->env_proxy;

You can also set your own User-Agent object:

  $coder->ua( LWPx::ParanoidAgent->new );

=head2 debug_level

Set to true to get extra debugging information

=head2 encode_urlsafe

Encodes url 

=head2 decode_urlsafe_base64 

Decodes url 

=head2 make_signature

Creates google friendly signature

=head2 raw_distance

Returns raw json response from google API request


=head1 ACKNOWLEDGEMENTS

I borrowed much of this inital code from C<Geo::Coder::Google>, it sped up much of 
my work and was a great help. Thanks.

=head1 AUTHOR

Lee Carmichael, C<< <lcarmich at cpan.org> >>

=head1 BUGS

Please report any bugs or feature requests to C<bug-geo-distance-google at rt.cpan.org>, 
or through
the web interface at L<http://rt.cpan.org/NoAuth/ReportBug.html?Queue=Geo-Distance-Google>.  
I will be notified, and then you'll
automatically be notified of progress on your bug as I make changes.

=head1 SUPPORT

You can find documentation for this module with the perldoc command.

    perldoc Geo::Distance::Google

You can also look for information at:

=over 4

=item * RT: CPAN's request tracker (report bugs here)

L<https://github.com/lecar-red/Geo-Distance-Google/issues>

=item * AnnoCPAN: Annotated CPAN documentation

L<http://annocpan.org/dist/Geo-Distance-Google>

=item * CPAN Ratings

L<http://cpanratings.perl.org/d/Geo-Distance-Google>

=item * Search CPAN

L<http://search.cpan.org/dist/Geo-Distance-Google/>

=back

=head1 TODO

=over 4

=item * Add support to distance method for explicit latitude and longtitude parameters (support by hand encoding as 'lat|lng'

=item * Add tests for method based changes to attributes L<avoid>, L<mode> and L<units>

=item * Add tests for API keys

=item * Add tests for imperial units

=back

=head1 SEE ALSO

L<Geo::Distance>, L<Geo::Coder::Google>

List of supported languages: L<http://spreadsheets.google.com/pub?key=p9pdwsai2hDMsLkXsoM05KQ&gid=1>

API Docs: L<http://code.google.com/apis/maps/documentation/distancematrix/>,

=cut