package Geo::ShapeFile::Point;
# TODO - add dimension operators (to specify if 2 or 3 dimensional point)
use strict;
use warnings;
use Math::Trig;
use Carp;
our $VERSION = '2.53_001';

use overload
    '==' => 'eq',
    'eq' => 'eq',
    '""' => 'stringify',
    '+'  => \&add,
    '-'  => \&subtract,
    '*'  => \&multiply,
    '/'  => \&divide,
    fallback => 1
;

my %config = (
    comp_includes_z => 1,
    comp_includes_m => 1,
);

sub new {
    my $proto = shift;
    my $class = ref($proto) || $proto;

    my $self = {@_};

    bless $self, $class;

    return $self;
}

sub var {
    my $self = shift;
    my $var = shift;

    if(@_) {
        return $self->{$var} = shift;
    }
    else {
        return $self->{$var};
    }
}

#  these could be factory generated
sub X { shift()->var('X',@_); }
sub Y { shift()->var('Y',@_); }
sub Z { shift()->var('Z',@_); }
sub M { shift()->var('M',@_); }

sub x_min { shift()->var('X'); }
sub x_max { shift()->var('X'); }
sub y_min { shift()->var('Y'); }
sub y_max { shift()->var('Y'); }
sub z_min { shift()->var('Z'); }
sub z_max { shift()->var('Z'); }
sub m_min { shift()->var('M'); }
sub m_max { shift()->var('M'); }

sub import {
    my $self = shift;
    my %args = @_;

    foreach(keys %args) { $config{$_} = $args{$_}; }
}

sub eq {
    my $left  = shift;
    my $right = shift;

    if ($config{comp_includes_z} && (defined $left->Z || defined $right->Z)) {
        return 0 unless defined $left->Z && defined $right->Z;
        return 0 unless $left->Z == $right->Z;
    }
    if ($config{comp_includes_m} && (defined $left->M || defined $right->M)) {
        return 0 unless defined $left->M && defined $right->M;
        return 0 unless $left->M == $right->M;
    }

    return ($left->X == $right->X && $left->Y == $right->Y);
}

sub stringify {
    my $self = shift;

    my @foo = ();
    foreach(qw/X Y Z M/) {
        if(defined $self->$_()) {
            push @foo, "$_=" . $self->$_();
        }
    }
    my $r = 'Point(' . join(',', @foo) . ')';
}

sub distance_from {
    my ($p1,$p2) = @_;

    my $dp = $p2->subtract($p1);
    return sqrt ( ($dp->X ** 2) + ($dp->Y **2) );
}

sub distance_to { distance_from(@_); }

sub angle_to {
    my ($p1, $p2) = @_;

    my $dp = $p2->subtract ($p1);

    #  could use atan2 here, surely?
    if ($dp->Y) {
        # two distinct points
        return rad2deg ( atan( $dp->Y / $dp->X ) )
          if $dp->X;

        # same X value
        return $dp->Y > 0 ? 90 : -90;
    }

    # same point
    return 0;
}

sub add {      mathemagic('add',      @_); }
sub subtract { mathemagic('subtract', @_); }
sub multiply { mathemagic('multiply', @_); }
sub divide {   mathemagic('divide',   @_); }

sub mathemagic {
    my ($op, $l, $r, $reverse) = @_;

    if ($reverse) {  # put them back in the right order
        ($l, $r) = ($r, $l);
    }
    my ($left, $right);

    if (UNIVERSAL::isa($l, 'Geo::ShapeFile::Point')) { $left  = 'point'; }
    if (UNIVERSAL::isa($r, 'Geo::ShapeFile::Point')) { $right = 'point'; }

    if ($l =~ /^[\d\.]+$/) { $left  = 'number'; }
    if ($r =~ /^[\d\.]+$/) { $right = 'number'; }

    unless ($left)  { croak "Couldn't identify $l for $op"; }
    unless ($right) { croak "Couldn't identify $r for $op"; }

    my $function = join '_', $op, $left, $right;

    croak "Don't know how to $op $left and $right"
      if !defined &{$function};

    do {
        no strict 'refs';
        return $function->($l,$r);
    }
}

sub add_point_point {
    my ($p1, $p2) = @_;

    my $z;
    if(defined($p2->Z) && defined($p1->Z)) { $z = ($p2->Z + $p1->Z); }

    Geo::ShapeFile::Point->new(
        X => ($p2->X + $p1->X),
        Y => ($p2->Y + $p1->Y),
        Z =>  $z,
    );
}

sub add_point_number {
    my ($p1, $n) = @_;

    my $z;
    if (defined($p1->Z)) { $z = ($p1->Z + $n); }

    Geo::ShapeFile::Point->new(
        X => ($p1->X + $n),
        Y => ($p1->Y + $n),
        Z => $z,
    );
}
sub add_number_point { add_point_number(@_); }

sub subtract_point_point {
    my($p1, $p2) = @_;

    my $z;
    if(defined($p2->Z) && defined($p1->Z)) { $z = ($p2->Z - $p1->Z); }

    my $result = Geo::ShapeFile::Point->new(
        X => ($p1->X - $p2->X),
        Y => ($p1->Y - $p2->Y),
        Z =>  $z,
    );
    return $result;
}
sub subtract_point_number {
    my($p1, $n) = @_;

    my $z;
    if (defined $p1->Z) {
        $z = ($p1->Z - $n);
    }

    Geo::ShapeFile::Point->new(
        X => ($p1->X - $n),
        Y => ($p1->Y - $n),
        Z =>  $z,
    );
}
sub subtract_number_point { subtract_point_number(reverse @_); }

sub multiply_point_point {
    my ($p1, $p2) = @_;

    my $z;
    if (defined $p2->Z and defined $p1->Z) {
        $z = $p2->Z * $p1->Z;
    }

    Geo::ShapeFile::Point->new(
        X => ($p2->X * $p1->X),
        Y => ($p2->Y * $p1->Y),
        Z =>  $z,
    );
}
sub multiply_point_number {
    my($p1, $n) = @_;

    my $z;
    if (defined $p1->Z) {
        $z = $p1->Z * $n;
    }

    Geo::ShapeFile::Point->new(
        X => ($p1->X * $n),
        Y => ($p1->Y * $n),
        Z =>  $z,
    );
}
sub multiply_number_point { multiply_point_number(reverse @_); }

sub divide_point_point {
    my($p1, $p2) = @_;

    my $z;
    if (defined $p2->Z and defined $p1->Z) {
        $z = $p1->Z / $p2->Z;
    }

    Geo::ShapeFile::Point->new(
        X => ($p1->X / $p2->X),
        Y => ($p1->Y / $p2->Y),
        Z =>  $z,
    );
}
sub divide_point_number {
    my ($p1, $n) = @_;

    my $z;
    if (defined $p1->Z) {
        $z = $p1->Z / $n;
    }

    Geo::ShapeFile::Point->new(
        X => ($p1->X / $n),
        Y => ($p1->Y / $n),
        Z =>  $z,
    );
}
sub divide_number_point { divide_point_number(reverse @_); }

1;
__END__
=head1 NAME

Geo::ShapeFile::Point - Geo::ShapeFile utility class.

=head1 SYNOPSIS

  use Geo::ShapeFile::Point;
  use Geo::ShapeFile;

  my $point = Geo::ShapeFile::Point->new(X => 12345, Y => 54321);

=head1 ABSTRACT

  This is a utility class, used by L<Geo::ShapeFile>.

=head1 DESCRIPTION

This is a utility class, used by L<Geo::ShapeFile> to represent point data,
you should see the Geo::ShapeFile documentation for more information.

=head2 EXPORT

Nothing.

=head2 IMPORT NOTE

This module uses overloaded operators to allow you to use == or eq to compare
two point objects.  By default points are considered to be equal only if their
X, Y, Z, and M attributes are equal.  If you want to exclude the Z or M
attributes when comparing, you should use comp_includes_z or comp_includes_m 
when importing the object.  Note that you must do this before you load the
Geo::ShapeFile module, or it will pass it's own arguments to import, and you
will get the default behavior:

  DO:

  use Geo::ShapeFile::Point comp_includes_m => 0, comp_includes_z => 0;
  use Geo::ShapeFile;

  DONT:

  use Geo::ShapeFile;
  use Geo::ShapeFile::Point comp_includes_m => 0, comp_includes_z => 0;
  (Geo::ShapeFile already imported Point for you)

=head1 METHODS

=over 4

=item new(X => $x, Y => $y)

Creates a new Geo::ShapeFile::Point object, takes a has consisting of X, Y, Z,
and/or M values to be assigned to the point.

=item X() Y() Z() M()

Set/retrieve the X, Y, Z, or M values for this object.

=item x_min() x_max() y_min() y_max()

=item z_min() z_max() m_min() m_max()

These methods are provided for compatibility with Geo::ShapeFile::Shape, but
for points simply return the X, Y, Z, or M coordinates as appropriate.

=item distance_from($point)

Returns the distance between this point and the specified point.  Only
considers the two-dimensional distance, altitude is not included in the
calculation.

=item angle_to($point);

Returns the angle (in degress) from this point to some other point.  Returns
0 if the two points are in the same location.

=back

=head1 REPORTING BUGS

Please send any bugs, suggestions, or feature requests to
  L<https://github.com/shawnlaffan/Geo-ShapeFile/issues>.

=head1 SEE ALSO

L<Geo::ShapeFile>

=head1 AUTHOR

Jason Kohles, E<lt>email@jasonkohles.comE<gt>

Shawn Laffan, E<lt>shawnlaffan@gmail.comE<gt>


=head1 COPYRIGHT AND LICENSE

Copyright 2002-2013 by Jason Kohles

Copyright 2014 by Shawn Laffan

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

=cut
