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

=head1 NAME

Geo::Coder::GeocodeFarm - Geocode addresses with the GeocodeFarm API

=head1 SYNOPSIS

  use Geo::Coder::GeocodeFarm;

  my $geocoder = Geo::Coder::GeocodeFarm->new(
      key => '3d517dd448a5ce1c2874637145fed69903bc252a',
  );
  my $result = $geocoder->geocode(
      location => '530 W Main St Anoka MN 55303 US',
      lang     => 'en',
      count    => 1,
  );
  printf "%f,%f",
      $result->{RESULTS}{COORDINATES}{latitude},
      $result->{RESULTS}{COORDINATES}{longitude};

=head1 DESCRIPTION

The C<Geo::Coder::GeocodeFarm> module provides an interface to the geocoding
functionality of the GeocodeFarm API v3.

=for readme stop

=cut


use 5.006;
use strict;
use warnings;

our $VERSION = '0.0402';

use Carp qw(croak);
use Encode;
use HTTP::Tiny;
use URI;
use URI::QueryParam;
use JSON;
use Scalar::Util qw(blessed);

use constant DEBUG => !! $ENV{PERL_GEO_CODER_GEOCODEFARM_DEBUG};


=head1 METHODS

=head2 new

  $geocoder = Geo::Coder::GeocodeFarm->new(
      key    => '3d517dd448a5ce1c2874637145fed69903bc252a',
      url    => 'https://www.geocode.farm/v3/',
      ua     => HTTP::Tiny->new,
      parser => JSON->new->utf8,
      raise_failure => 1,
  );

Creates a new geocoding object with optional arguments.

An API key is optional and can be obtained at
L<https://www.geocode.farm/dashboard/login/>

C<url> argument is optional and then the default address is http-based if
C<key> argument is missing and https-based if C<key> is provided.

C<ua> argument is a L<HTTP::Tiny> object by default and can be also set to
L<LWP::UserAgent> object.

New account can be registered at L<https://www.geocode.farm/register/>

=cut

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

    my $self = bless +{
        ua     => $args{ua} || HTTP::Tiny->new(
            agent => __PACKAGE__ . "/$VERSION",
        ),
        url    => sprintf('%s://www.geocode.farm/v3/', $args{key} ? 'https' : 'http'),
        parser => $args{parser} || JSON->new->utf8,
        raise_failure => $args{raise_failure} || 1,
        %args,
    } => $class;

    return $self;
}


=head2 geocode

  $result = $geocoder->geocode(
      location => $location,
      lang     => 'en',  # optional: 'en' or 'de'
      country  => 'US',  # optional
      count    => 1,     # optional
  )

Forward geocoding takes a provided address or location and returns the
coordinate set for the requested location as a nested list:

    {
        "geocoding_results": {
            "LEGAL_COPYRIGHT": {
                "copyright_notice": "Copyright (c) 2015 Geocode.Farm - All Rights Reserved.",
                "copyright_logo": "https:\/\/www.geocode.farm\/images\/logo.png",
                "terms_of_service": "https:\/\/www.geocode.farm\/policies\/terms-of-service\/",
                "privacy_policy": "https:\/\/www.geocode.farm\/policies\/privacy-policy\/"
            },
            "STATUS": {
                "access": "FREE_USER, ACCESS_GRANTED",
                "status": "SUCCESS",
                "address_provided": "530 W Main St Anoka MN 55303 US",
                "result_count": 1
            },
            "ACCOUNT": {
                "ip_address": "1.2.3.4",
                "distribution_license": "NONE, UNLICENSED",
                "usage_limit": "250",
                "used_today": "26",
                "used_total": "26",
                "first_used": "26 Mar 2015"
            },
            "RESULTS": [
                {
                    "result_number": 1,
                    "formatted_address": "530 West Main Street, Anoka, MN 55303, USA",
                    "accuracy": "EXACT_MATCH",
                    "ADDRESS": {
                        "street_number": "530",
                        "street_name": "West Main Street",
                        "locality": "Anoka",
                        "admin_2": "Anoka County",
                        "admin_1": "Minnesota",
                        "postal_code": "55303",
                        "country": "United States"
                    },
                    "LOCATION_DETAILS": {
                        "elevation": "UNAVAILABLE",
                        "timezone_long": "UNAVAILABLE",
                        "timezone_short": "America\/Menominee"
                    },
                    "COORDINATES": {
                        "latitude": "45.2041251174690",
                        "longitude": "-93.4003513528652"
                    },
                    "BOUNDARIES": {
                        "northeast_latitude": "45.2041251778513",
                        "northeast_longitude": "-93.4003513845523",
                        "southwest_latitude": "45.2027761197097",
                        "southwest_longitude": "-93.4017002802923"
                    }
                }
            ],
            "STATISTICS": {
                "https_ssl": "DISABLED, INSECURE"
            }
        }
    }

Method throws an error (or returns failure as nested list if raise_failure
argument is false) if the service failed to find coordinates or wrong key was
used.

Methods throws an error if there was an other problem.

=cut

sub geocode {
    my ($self, %args) = @_;

    my ($addr) = do {
        if (defined $args{location}) {
            $args{location};
        }
        elsif (defined $args{addr}) {
            $args{addr};
        }
        else {
            croak "Attribute (location) or attribute (addr) is required";
        }
    };

    return $self->_request('forward', %args, addr => $addr);
};


=head2 reverse_geocode

  $result = $geocoder->reverse_geocode(
      lat      => $latitude,
      lon      => $longtitude,
      lang     => 'en',  # optional: 'en' or 'de'
      country  => 'US',  # optional
      count    => 1,     # optional
  )

or

  $result = $geocoder->reverse_geocode(
      latlng => "$latitude,$longtitude",
      # ... optional args
  )

Reverse geocoding takes a provided coordinate set and returns the address for
the requested coordinates as a nested list. Its format is the same as for
L</geocode> method.

Method throws an error (or returns failure as nested list if raise_failure
argument is false) if the service failed to find coordinates or wrong key was
used.

Method throws an error if there was an other problem.

=cut

sub reverse_geocode {
    my ($self, %args) = @_;

    my ($lat, $lon) = do {
        if (defined $args{latlng}) {
            my @latlng = split ',', $args{latlng};
            croak "Attribute (latlng) is invalid" unless @latlng == 2;
            @latlng;
        }
        elsif (defined $args{lat} and defined $args{lon}) {
            @args{qw(lat lon)};
        }
        else {
            croak "Attribute (latlng) or attributes (lat) and (lon) are required";
        }
    };

    return $self->_request('reverse', %args, lat => $lat, lon => $lon);
};


sub _request {
    my ($self, $type, %args) = @_;

    my $url = URI->new_abs(sprintf('json/%s/', $type), $self->{url});

    if ($type eq 'forward') {
        $url->query_param_append(addr => $args{addr});
    } elsif ($type eq 'reverse') {
        $url->query_param_append(lat => $args{lat});
        $url->query_param_append(lon => $args{lon});
    } else {
        croak "Unknown type for request";
    }

    $url->query_param_append(key => $self->{key}) if $self->{key};
    warn $url if DEBUG;

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

    my $content = do {
        if (blessed $res and $res->isa('HTTP::Response')) {
            croak $res->status_line unless $res->is_success;
            $res->decoded_content;
        } elsif (ref $res eq 'HASH') {
            croak "@{[$res->{status}, $res->{reason}]}" unless $res->{success};
            $res->{content};
        } else {
            croak "Wrong response $res ";
        }
    };

    warn $content if DEBUG;
    return unless $content;

    my $data = eval { $self->{parser}->decode($content) };
    croak $content if $@;

    croak "GeocodeFarm API returned status: ", $data->{geocoding_results}{STATUS}{status}
        if ($self->{raise_failure} and ($data->{geocoding_results}{STATUS}{status}||'') ne 'SUCCESS');

    return $data->{geocoding_results};
};


1;


=for readme continue

=head1 SEE ALSO

L<https://www.geocode.farm/>

=head1 BUGS

If you find the bug or want to implement new features, please report it at
L<https://github.com/dex4er/perl-Geo-Coder-GeocodeFarm/issues>

The code repository is available at
L<http://github.com/dex4er/perl-Geo-Coder-GeocodeFarm>

=head1 AUTHOR

Piotr Roszatycki <dexter@cpan.org>

=head1 LICENSE

Copyright (c) 2013, 2015 Piotr Roszatycki <dexter@cpan.org>.

This is free software; you can redistribute it and/or modify it under
the same terms as perl itself.

See L<http://dev.perl.org/licenses/artistic.html>