package WebService::GoogleMaps;

# GoogleMaps (GMaps)
# a perl interface to google maps
# Copyright (c) 2005 - Karl Lohner	<karllohner+googlemaps@gmail.com>

our $VERSION = '0.01';

use warnings;
use strict;
use GD;
use LWP::UserAgent;
use HTTP::Request;
# use HTTP::Cookies;
use POSIX qw/floor ceil/;

sub new {
	my $class = shift;
	my %opts;
	if (scalar @_ == 2 && $_[0] =~ /^\d+$/ && $_[1] =~ /^\d+$/) {
		$opts{width}=$_[0];
		$opts{height}=$_[1];
	}else{
		%opts = @_;
	}
	my $self = bless(\%opts,$class);

	$self->{debug} && print STDERR "In new()\n";

	$self->{agent} ||= "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.7.5) Gecko/20041106 Firefox/1.0";  # fake as FireFox 1.0
	$self->{error} = '';
	$self->{debug} && print STDERR "Setting user-agent to [$opts{agent}]\n";
	$self->{ua} = LWP::UserAgent->new();
	$self->{ua}->agent($self->{agent});

	# $self->{debug} && print STDERR "Setting up cookie jar (HTTP\:\:Cookies)\n";
	# $self->{ua}->cookie_jar(HTTP::Cookies->new(%{$self->{cookies}}));
	
	$self->{gm_version} ||= ".1";
	$self->{tile_res_x}	||= 128;
	$self->{tile_res_y}	||= 128;

	$self->{debug} && print STDERR "new() complete\n";
	
	return $self;
}

sub get_tile_http {
	my ($self, $x, $y, $z) = @_;

	$self->{debug} && print STDERR "In get_tile_http($x, $y, $z), making request...\n";
	
	my $req = HTTP::Request->new(GET => sprintf("http://mt.google.com/mt?v=%s&x=%d&y=%d&zoom=%d", $self->{gm_version}, $x, $y, $z));
	my $res = $self->{ua}->request($req);
	
	$self->{debug} && print STDERR "Got request\n";
	
	if ($res->is_success) {
		$self->{debug} && print STDERR "Request success\n";
		return $res->content;
	} else {
		$self->{debug} && print STDERR "Request failure error[".$res->status_line."]\n";
		$self->{error} = $res->status_line;
	}
	
	return undef;
}

sub get_tile_cache {
	my $self = shift;
	my ($x, $y, $z) = @_;

	my $gifdata;
	if (defined($self->{cache_dir}) && -d $self->{cache_dir}) {
		# my $tile_cache_filename = sprintf("%s%d/%d_%d_%d.gif", $self->{cache_dir}, $z, $z, $x, $y);
		my $tile_cache_filename = sprintf("%s/gm_%s_%02d_%d_%d.gif", $self->{cache_dir}, $self->{gm_version}, $z, $x, $y, );
		if (-e($tile_cache_filename)) {
			if (open (GM_CACHE, "<", $tile_cache_filename)) {
				binmode GM_CACHE;
				$gifdata = do { local $/; <GM_CACHE> };
				close(GM_CACHE);
			}
		}
	}

	return $gifdata;
}	

sub put_tile_cache {
	my ($self, $gifdata, $x, $y, $z) = @_;

	if (defined($self->{cache_dir}) && -d $self->{cache_dir}) {
		# my $tile_cache_filename = sprintf("%s%d/%d_%d_%d.gif", $self->{cache_dir}, $z, $z, $x, $y);
		my $tile_cache_filename = sprintf("%s/gm_%s_%02d_%d_%d.gif", $self->{cache_dir}, $self->{gm_version}, $z, $x, $y, );
		# !!! create directory structure if necessary
		if (open (GM_CACHE, ">", $tile_cache_filename)) {
			binmode GM_CACHE;
			print GM_CACHE $gifdata;
			close(GM_CACHE);
		}
	}
	
	return $gifdata;
}	

sub get_tile {
	my $self = shift;
	my ($x, $y, $z) = @_;
	my $gifdata = $self->get_tile_cache($x, $y, $z);
	if (!defined($gifdata)) {
		$gifdata = $self->get_tile_http($x, $y, $z);
		if (defined($gifdata)) {
			$self->put_tile_cache($gifdata, $x, $y, $z);
		}
	}
	return $gifdata;
}

sub set {
	my $self = shift;
	my %opts = @_;
	my $opt_count = 0;
	foreach (keys %opts) {
		$self->{$_}=$opts{$_};
		$opt_count++;
	}
	return $opt_count;
}

sub get {
	my $self = shift;
	if (exists $self->{$_[0]}) {
		return $self->{$_[0]};
	}
	return undef;
}

sub latlonzoom_to_gmap_xy_float {
	my $self = shift;
	# figure out which tile is at a given latitude and longitude for a specific zoom_level
	return [ (($_[1] + 98.35) * (0x20000 >> $_[2])  * 0.77162458338772) / $self->{tile_res_x},
			 ((39.5 - $_[0])  * (0x20000 >> $_[2]))					 / $self->{tile_res_y} ];
}

sub generate_gd {
	my $self = shift;
	my $latitude   = defined($_[0]) ? $_[0] : $self->{latitude};
	my $longitude  = defined($_[1]) ? $_[1] : $self->{longitude};
	my $zoom_level = defined($_[2]) ? $_[2] : $self->{zoom_level};
	
	my $gd = GD::Image->new($self->{width}, $self->{height}, 1);
	
	$self->{debug} && print STDERR "In generate( $latitude, $longitude, $zoom_level)\n";

	# figure out which tile is at current latlong
	my $gmap_center_tile_xy_float = $self->latlonzoom_to_gmap_xy_float($latitude, $longitude, $zoom_level);
	my $gmap_center_tile_xy	   = [ POSIX::floor($gmap_center_tile_xy_float->[0]), POSIX::floor($gmap_center_tile_xy_float->[1]) ];
	
	# figure out where this tile should go in our gd image
	my $gd_pan_offset = [ (defined($self->{pan_x}) ? $self->{pan_x} : 0), (defined($self->{pan_y}) ? $self->{pan_y} : 0) ];

	my $gd_center_tile_xy = [ int( ( ($self->{width}  / 2) + $gd_pan_offset->[0] ) - 
								   ( ($gmap_center_tile_xy_float->[0] - $gmap_center_tile_xy->[0]) * $self->{tile_res_x} ) 
								 ),
							  int( ( ($self->{height} / 2) + $gd_pan_offset->[1] ) - 
								   ( ($gmap_center_tile_xy_float->[1] - $gmap_center_tile_xy->[1]) * $self->{tile_res_y} )
								 ) ];

	# set up a hash to track some info about our current gd generation
	my $tile_hash = {};
	# We need to limit our tile requests to within a specific viewport of offset ranges.
	# As we process, we may need to further reduce this viewport if there isn't data on the server to get some tiles.
	$tile_hash->{viewport}{0}{-1} = 0 - POSIX::ceil(($gd_center_tile_xy->[0] / $self->{tile_res_x}));
	$tile_hash->{viewport}{0}{1}  =	 POSIX::ceil((($self->{width}  - $gd_center_tile_xy->[0]) / $self->{tile_res_x})) - 1;
	$tile_hash->{viewport}{1}{-1} = 0 - POSIX::ceil(($gd_center_tile_xy->[1] / $self->{tile_res_y}));
	$tile_hash->{viewport}{1}{1}  =	 POSIX::ceil((($self->{height} - $gd_center_tile_xy->[1]) / $self->{tile_res_y})) - 1;
	
	# We need to set up a queue of tile offsets to fetch relative to our desired latitude/longitude.
	# This is also influenced by our panning
	# We also need to track tile offset coordinates that get added to our tile fetch queue so we don't redundantly request tiles
	# We'll need to track which adjacent tile requested that the tile get added to the queue, so we know which wall we gmap boundary we hit if we hit one.
	# Start with adding the tile at the center of the viewport to the queue
	my $tile_xy_offset = [ int(($tile_hash->{viewport}{0}{-1}+$tile_hash->{viewport}{0}{1})/2),
						   int(($tile_hash->{viewport}{1}{-1}+$tile_hash->{viewport}{1}{1})/2) ];
	push (@{$tile_hash->{tile_queue}}, [$tile_xy_offset->[0], $tile_xy_offset->[1]]);

	# Set the vector of the tile referring this tile to the queue.  In this initial case, use [0,0] because it referred itself.
	$tile_hash->{queued}{$tile_xy_offset->[0]}{$tile_xy_offset->[1]} = [0, 0];

	while ($tile_xy_offset = shift(@{$tile_hash->{tile_queue}})) {
		# skip if it's outside our viewport
		next if ($tile_xy_offset->[0]) < $tile_hash->{viewport}{0}{-1};
		next if ($tile_xy_offset->[0]) > $tile_hash->{viewport}{0}{1};
		next if ($tile_xy_offset->[1]) < $tile_hash->{viewport}{1}{-1};
		next if ($tile_xy_offset->[1]) > $tile_hash->{viewport}{1}{1};
		# get the tile
		my $gifdata = $self->get_tile($gmap_center_tile_xy->[0] + $tile_xy_offset->[0], $gmap_center_tile_xy->[1] + $tile_xy_offset->[1], $zoom_level);
		if ($gifdata) {
			# place it into our gd viewport
			my $mt = GD::Image->newFromGifData($gifdata);
			$gd->copy($mt,$gd_center_tile_xy->[0] + ($tile_xy_offset->[0] * $self->{tile_res_x}), $gd_center_tile_xy->[1] + ($tile_xy_offset->[1] * $self->{tile_res_y}), 0, 0, $self->{tile_res_x}, $self->{tile_res_y});
			# queue the top, bottom, left, right tiles if they're within our viewport and within available range
			foreach my $vector ([0,-1],[0,1],[-1,0],[1,0]) {
				# make sure it's not already queued
				next if ( exists ( $tile_hash->{queued}{$tile_xy_offset->[0] + $vector->[0]} ) 
					   && exists ( $tile_hash->{queued}{$tile_xy_offset->[0] + $vector->[0]}{$tile_xy_offset->[1] + $vector->[1]} ) );
				push (@{$tile_hash->{tile_queue}}, [$tile_xy_offset->[0]+$vector->[0], $tile_xy_offset->[1]+$vector->[1]]);
				$tile_hash->{queued}{$tile_xy_offset->[0] + $vector->[0]}{$tile_xy_offset->[1] + $vector->[1]} = [ $vector->[0], $vector->[1] ];
			}
		}else{
			# tile not available.
			my $from_vector = $tile_hash->{queued}{$tile_xy_offset->[0]}{$tile_xy_offset->[1]};
			# abort here if this happened on the first tile requested
			if (!$from_vector->[0] && !$from_vector->[1]) {
				$self->{error} = "tile at center of image is not available";
				last;
			}
			# reduce our allowed viewport if we've hit a image source boundary
			foreach my $axis ("0","1") {
				if ($from_vector->[$axis]) {
					# hit a boundary along an axis
					$self->{debug} && print STDERR "hit a boundary\n";
					$self->{debug} && print STDERR "was:	viewport [$axis,$from_vector->[$axis]] = $tile_hash->{viewport}{$axis}{$from_vector->[$axis]}\n";
					$tile_hash->{viewport}{$axis}{$from_vector->[$axis]}=$tile_xy_offset->[$axis]-$from_vector->[$axis];
					$self->{debug} && print STDERR "now:	viewport [$axis,$from_vector->[$axis]] = $tile_hash->{viewport}{$axis}{$from_vector->[$axis]}\n";
				}
			}
		}
	}

	# Add copyright notice
	my $black = $gd->colorAllocate(0,0,0);
	$gd->string(gdTinyFont,5,$self->{height}-10,"(c) 2005 Google",$black);
	$gd->string(gdTinyFont,$self->{width}-200,$self->{height}-10,"Map data (c) 2005 NAVTEQ(tm), TeleAtlas",$black);

	return $gd;
}

sub generate_html {
	my $self = shift;
	my $latitude   = defined($_[0]) ? $_[0] : $self->{latitude};
	my $longitude  = defined($_[1]) ? $_[1] : $self->{longitude};
	my $zoom_level = defined($_[2]) ? $_[2] : $self->{zoom_level};
	
	return "<div>Planned for next version...</div>";
}


1; # End of WebService::GoogleMaps


__END__

=head1 NAME

WebService::GoogleMaps - Retrieves maps from http://maps.google.com/

=head1 VERSION

Version 0.01

=cut

=head1 SYNOPSIS


	use WebService::GoogleMaps;

	my $gmap = WebService::GoogleMaps->new( 640, 480 );

	$gmap->set(
		latitude   => 40.750275,
		longitude  => -73.993034,
		zoom_level => 4,          # valid values are 0..12, lower value is more zoomed
		cache_dir  => "/tmp",     # optional, but helps speed up future requests
		pan_x      => 0,          # experimental this version
		pan_y      => 0,          # experimental this version
	);
	my $gd = $gmap->generate_gd();

	# or simply

	# my $gd2 = $gmap->generate_gd(40.43305, -74.49757, 4);  # latitude, longitude, zoom_level

	$gd->{error} && print "Error: $gd->{error}\n";
	
	open (FH, ">", "msg.png");
	binmode FH;
	print FH $gd->png;
	close(FH);



=head1 FUNCTIONS

=head2 new()

=head2 set()

=head2 get()

=head2 generate_gd()

Returns a GD object.

=head2 generate_html()

Not in this version.

=head1 AUTHOR

Karl Lohner, C<< <karllohner+googlemaps@thespill.com> >>

=head1 BUGS

Please report any bugs or feature requests to
C<karllohner+googlemaps@gmail.com>

=head1 ACKNOWLEDGEMENTS

Thanks to Joel Webber and his blog article at http://jgwebber.blogspot.com/2005/02/mapping-google.html
as well as to the many people who added comments there, particularly the anonymous comments that detailed
the algorithm used to map latitude/longitude to specific tiles.


=head1 COPYRIGHT & LICENSE

Copyright 2005 Karl Lohner, All Rights Reserved.

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

=cut

