=cut
package HTML::GoogleMaps;
use strict;
use Geo::Coder::Google;
our $VERSION = 5;
sub new
{
my ($class, %opts) = @_;
return 0
unless $opts{key};
if ($opts{db})
{
require Geo::Coder::US;
Geo::Coder::US->set_db($opts{db});
}
bless {
%opts,
points => [],
poly_lines => [],
geocoder => Geo::Coder::Google->new(apikey => $opts{key}),
}, $class;
}
sub _text_to_point {
my ($this, $point_text) = @_;
# IE, already a long/lat pair
return [reverse @$point_text] if ref($point_text) eq "ARRAY";
# US street address
if ($this->{db}) {
my ($point) = Geo::Coder::US->geocode($point_text);
if ($point->{lat}) {
return [ $point->{lat}, $point->{long} ];
}
} else {
my $location = $this->{geocoder}->geocode(location => $point_text);
return [
$location->{Point}{coordinates}[1],
$location->{Point}{coordinates}[0],
];
}
# Unknown
return 0;
}
sub _find_center
{
my ($this) = @_;
# Null case
return unless @{$this->{points}};
my $total_lat;
my $total_long;
my $total_abs_long;
foreach my $point (@{$this->{points}})
{
$total_lat += $point->{point}[0];
$total_long += $point->{point}[1];
$total_abs_long += abs($point->{point}[1]);
}
# Latitude is easy, just an average
my $center_lat = $total_lat/@{$this->{points}};
# Longitude, on the other hand, is trickier. If points are
# clustered around the international date line a raw average
# would produce a center around longitude 0 instead of -180.
my $avg_long = $total_long/@{$this->{points}};
my $avg_abs_long = $total_abs_long/@{$this->{points}};
return [ $center_lat, $avg_long ] # All points are on the
if abs($avg_long) == $avg_abs_long; # same hemasphere
if ($avg_abs_long > 90) # Closer to the IDL
{
if ($avg_long < 0 && abs($avg_long) <= 90)
{
$avg_long += 180;
}
elsif (abs($avg_long) <= 90)
{
$avg_long -= 180;
}
}
return [ $center_lat, $avg_long ];
}
sub center
{
my ($this, $point_text) = @_;
my $point = $this->_text_to_point($point_text);
return 0 unless $point;
$this->{center} = $point;
return 1;
}
sub zoom
{
my ($this, $zoom_level) = @_;
$this->{zoom} = 17-$zoom_level;
}
sub v2_zoom
{
my ($this, $zoom_level) = @_;
$this->{zoom} = $zoom_level;
}
sub controls
{
my ($this, @controls) = @_;
my %valid_controls = map { $_ => 1 } qw(large_map_control
small_map_control
small_zoom_control
map_type_control);
return 0 if grep { !$valid_controls{$_} } @controls;
$this->{controls} = [ @controls ];
}
sub dragging
{
my ($this, $dragging) = @_;
$this->{dragging} = $dragging;
}
sub info_window
{
my ($this, $info) = @_;
$this->{info_window} = $info;
}
sub map_type
{
my ($this, $type) = @_;
my %valid_types = (map_type => 'G_NORMAL_MAP',
satellite_type => 'G_SATELLITE_MAP',
normal => 'G_NORMAL_MAP',
satellite => 'G_SATELLITE_MAP',
hybrid => 'G_HYBRID_MAP');
return 0 unless $valid_types{$type};
$this->{type} = $valid_types{$type};
}
sub add_marker
{
my ($this, %opts) = @_;
return 0 if $opts{icon} && $opts{icon} !~ /^[A-J]$/
&& !$this->{icon_hash}{$opts{icon}};
my $point = $this->_text_to_point($opts{point});
return 0 unless $point;
push @{$this->{points}}, { point => $point,
icon => $opts{icon},
html => $opts{html},
format => !$opts{noformat} };
}
sub add_icon
{
my ($this, %opts) = @_;
return 0 unless $opts{image} && $opts{shadow} && $opts{name};
$this->{icon_hash}{$opts{name}} = 1;
push @{$this->{icons}}, \%opts;
}
sub add_polyline
{
my ($this, %opts) = @_;
my @points = map { $this->_text_to_point($_) } @{$opts{points}};
return 0 if grep { !$_ } @points;
push @{$this->{poly_lines}}, { points => \@points,
color => $opts{color} || "\#0000ff",
weight => $opts{weight} || 5,
opacity => $opts{opacity} || .5 };
}
sub render
{
my ($this) = @_;
# Add in all the defaults
$this->{height} ||= 400;
$this->{width} ||= 600;
$this->{dragging} = 1 unless defined $this->{dragging};
$this->{info_window} = 1 unless defined $this->{info_window};
$this->{type} ||= "G_NORMAL_MAP";
$this->{zoom} ||= 13;
$this->{center} ||= $this->_find_center;
my $map = "
{width}px; height: $this->{height}px\">
";
my $text = "
";
return ("", $map, $text);
}
1;