package Geo::Coder::HostIP; use strict; use Carp; use LWP::UserAgent; sub new { my $c = shift; my $pkg = ref $c || $c; my $obj = {}; bless $obj, $pkg; my $server = shift || ''; $obj->{_server} = $server; $obj->{_ua} = LWP::UserAgent->new; $obj->{_agent} = 'Geo::Coder::HostIP/0.1'; return $obj; } sub Agent { my $obj = shift; my $uaname = shift or return undef; $obj->{_agent} = $uaname; } sub _request { my $obj = shift; $obj->{_ua}->agent($obj->{_agent}); return undef unless $obj->{_server} and $obj->{ip}; my $res = $obj->{_ua}->get("http://$obj->{_server}/get_html.php?". "ip=$obj->{ip}&position=true"); if ($res->is_success) { return $obj->_parse($res->content); } else { croak $res->status_line; } } sub FetchIP { my $obj = shift; my $ip = shift or return undef; $obj->{ip} = $ip; return $obj->_request; } sub FetchName { my $obj = shift; my $name = shift or return undef; my $ip = join ".", unpack('C4', (gethostbyname $obj->{domain})[4]); return $obj->FetchIP($obj->{domain}); } sub FetchRemoteAddr { my $obj = shift; if (exists $ENV{REMOTE_ADDR} && defined $ENV{REMOTE_ADDR}) { return $obj->FetchIP($ENV{REMOTE_ADDR}); } else { return undef; } } sub Lat { my $obj = shift; return exists $obj->{Latitude} ? $obj->{Latitude} : undef; } sub Long { my $obj = shift; return exists $obj->{Longitude} ? $obj->{Longitude} : undef; } sub Latitude { my $obj = shift; return $obj->Lat; } sub Longitude { my $obj = shift; return $obj->Long; } sub City { my $obj = shift; return exists $obj->{City} ? $obj->{City} : undef; } sub Country { my $obj = shift; return exists $obj->{Country} ? $obj->{Country} : undef; } sub CountryCode { my $obj = shift; return exists $obj->{Country_Code} ? $obj->{Country_Code} : undef; } sub State { my $obj = shift; return exists $obj->{State} ? $obj->{State} : undef; } sub Coords { my $obj = shift; if (exists $obj->{Latitude} and exists $obj->{Longitude}) { return wantarray ? ($obj->{Latitude}, $obj->{Longitude}) : "Lat: $obj->{Latitude}, Long: $obj->{Longitude}"; } else { return undef; } } sub GoogleMap { my $obj = shift; my $params = pop; if (@_) { my $ip = shift; $obj->FetchIP($ip); } unless ( exists $obj->{Latitude} and defined $obj->{Latitude} and exists $obj->{Longitude} and defined $obj->{Longitude} and ref $params eq 'HASH' and $params->{apikey}) { return undef; } my $apikey = $params->{apikey}; my $id = $params->{id} || 'googlemap'; my $fname = $params->{func_name} || 'load'; my $width = $params->{width} || 500; my $height = $params->{height} || 300; my $zoom = $params->{zoom} || 13; my $controls = ''; if ($params->{control}) { if ($params->{control} eq 'large') { $controls = 'map.addControl(new GLargeMapControl());'; } elsif ($params->{control} eq 'small') { $controls = 'map.addControl(new GSmallMapControl());'; } else { $controls = 'map.addControl(new GSmallScaleControl());'; } } my $scalec = ''; if ($params->{scalecontrol}) { $scalec = 'map.addControl(new GScaleControl());'; } my $typec = ''; if ($params->{typecontrol}) { $typec = 'map.addControl(new GMapTypeControl());'; } my $overc = ''; if ($params->{overviewcontrol}) { $overc = 'map.addControl(new GOverviewMapControl());'; } my $lat = $obj->{Latitude} + 0; my $long = $obj->{Longitude} + 0; return <<"EOF";
EOF } sub _parse { my $obj = shift; my $res = shift or return undef; for my $key (qw(Country City Longitude Latitude)) { delete $obj->{$key}; } my $content = $res; my @rows = split /[\r\n]+/, $content; chomp @rows; for my $row (@rows) { my ($k, $v) = split /\s*:\s*/, $row, 2; if ($k eq 'Country') { $v =~ /(.*)\s*(\([^\)])\)$/; $obj->{Country} = $1; $obj->{Country_Code} = $2; } elsif ($k eq 'City') { if ($v =~ /(.*),\s+([A-Z]+)/) { $obj->{City} = $1; $obj->{State} = $2; } } elsif ($k eq 'Latitude' or $k eq 'Longitude') { $obj->{$k} = $v + 0; } else { $obj->{$k} ||= $v; } } if (defined $obj->{Latitude} and defined $obj->{Longitude}) { return wantarray ? ($obj->{Latitude}, $obj->{Longitude}) : "Lat: $obj->{Latitude}, Long: $obj->{Longitude}"; } else { return wantarray ? () : 'Coordinates Not Found'; } } 1; __END__ =head1 NAME Geo::Coder::HostIP - Object-Oriented Retrival of IP based geocoding info =head1 SYNOPSIS use strict; use CGI; use Geo::Coder::HostIP; my $cgi = new CGI; my $geo = new Geo::Coder::HostIP; print $cgi->header; my ($lat, $long); if ($cgi->param('ip')) { ($lat, $long) = $geo->FetchIP($cgi->param('ip')); } else { ($lat, $long) = $geo->FetchRemoteAddr; } my $zoom = $cgi->param('zoom') || 13; if (defined $lat && defined $long) { print $geo->GoogleMap({apikey => 'reallylongstringthatgooglegivesyou', control => 'small', scalecontrol => 1, typecontrol => 1, overviewcontrol => 1}); } else { print "I can't find you."; } =head1 DESCRIPTION Geo::Coder::HostIP is an Object-Oriented module intended to make use of the HostIP API documented at It's useful for web stuff, but it's useful for other stuff too, like figuring out if, for instance, the majority of people hitting your spanish language webpage are Spanish or Mexican, for instance, and thus deciding whether to say 'auto' or 'carro'. Whatev. Anyway, you HAVE to have LWP installed for it to work. =head1 Instantiation You create a Geo::Coder::HostIP object, which I'm calling 'geo' because it's a relatively simple thing to call it, but you can call it anything you want. Except 'menlo' because I hate that word and I don't know why. new() can be called void. If you want, you can give it one argument to override the default server of '' with something else. However, that something else had better have the same scripts in the same places with the same arguments wanted, etc. Else it will break. At the time of thos writing, it only works in about 40-50% of cases depending on the IP's locale. =head1 Public Methods Several nice convenience methods are supplied, so you can make the module do the thinking and you can get back to drinking... or whatever. Why is the rum always gone? =over 4 =item new($addr=null) (constructor) As stated above, this creates a new Geo::Coder::HostIP object. That's it. It doesn't do any getting of infos until you tell it to. This is to make it so you have to deliberately choose to navigate this great series of tubes. my $geo = new Geo::Coder::HostIP; =item Agent($) Takes any string and makes that be the UserAgent string used by LWP. $book->Agent('MooseCrawler/1.7'); =item FetchIP($) Takes any string that is an IP address and attempts to retrieve the data from for it. If successful, it returns either a list containing the Latitude and Longitude thus found, or, if called in scalar context, a string that says "Lat: ###, Long: ###" where the ##s denote the values thereof. Latitude and Longitude are returned in decimal format. Don't try to use private network IP addresses. That's just a bit daft. my ($lat, $long) = $geo->FetchIP(''); If it fails, the list context returns the empty list, and scalar returns the string "Coordinates Not Found". Remember that print() implies list mode, not scalar mode, before you just print right off this! Also, this populates the data itcan find into the object, up to Country, Country Code, City, State (US only), Latitude and Longitude. =item FetchName($) Works like FetchIP above, but expects a domain name. Returns the same stuff and populates the properties just the same. Matter of fact, it just gets the IP address and then passes that on to FetchIP and returns whatever it gets. my ($lat, $long) = $geo->FetchName(''); =item FetchRemoteAddr() Works like FetchIP above, but automatically uses the value stored in $ENV{REMOTE_ADDR} if present and defined. =item Lat(), Latitude() After something is fetched using any of the above three methods, returns the latitude numerically. Returns undef if the latitude is not set. Returns 0 if latitude is set to something non-numeric. =item Long(), Longitude() See Lat() above and try and guess really hard. =item City() Returns the City property if set, or undef if not. Doesn't let you set it though. Set the property for that, though that's... Well, how should I know? You might have a reason. =item Country() Gee, guess what this does? =item CountryCode() Again... obvious as hell. =item State() La la la. item Coords() returns the same thing the preceding Fetch*() method would have returned, but doesn't take an argument and doesn't do any fetching. Rather it uses the current values in the hash. item GoogleMap($ip=null, ${apikey, id='googlemap', func_name='load', width=500, height=300, zoom=13, control='small', scalecontrol=0, typecontrol=0, overviewcontrol=0}) Okay, this one's pretty cool... GoogleMap() gives you back HTML code for -- you guessed it. A Google map. It takes an optional IP address, which it will fetch if provided. Note that if you do not provide an IP address, do NOT supply 'undef' as an argument! Just leave it out altogether, otherwise it will depopulate the object. Next (or first and last and only if you leave out th IP address), it takes a hashref of parmetres, which MUST contain a true string keyed by 'apikey' or it will not bother and return undef. Of course, the idea here is that you give it your google maps API key, so that the results actually work on your page. This hashref may also contain several other parametres. Among them are height and width, which will set the height and width of the map in pixels and default to 300 and 500 respectively, id which will set the HTML element id of the map itself and defaults to 'googlemap', and fname which will determine the name to be used for the onLoad JavaScript function in case you want to change it from the default of 'load'. Also there's 'zoom' which sets the default zoom, which defaults to 13, about normal city-level street view. Next there's control, which can be set to 'large', 'small', or any other true value. If 'large' the map will get the large google scale/pan control. If 'small' it will get the smaller version, and if set to some other true value it will get the little scale-only control up in the left upper corner. Then there are the other controls, which are booleans (defaulting to 0, i.e. false). If present and true, 'scalecontrol' will add the little scale meter, 'typecontrol' will give the user the buttons that let them switch to aerial photography view and hybrid type maps, and 'overviewcontrol' will give the little inset map in the lower right corner that shows how the rest of the map fits into its surroundings. You can, of course, parse the HTML resulting and do things to it, or just run a regex on it to make it different. It does include a BODY and /BODY tag, so you may want to. The HTMl thus output has two HTML comments embedded in it, which contain ' {{ADDITIONAL HEAD}} ' and ' {{ADDITIONAL BODY}} ' inside a proper HTML comment tag, for your convenience in this regard. =back =head1 Private Methods =over 4 =item _request() Goes and grabs the data and calls _parse if successful. =item _parse() Parses out the results of _request =back =head1 Properties =over 4 =item properties The object is a hashref of properties once populated. These are set by the _parse() method. You get to these with: $geo->{Property_name} These are case sensitive, capitalised, and potentially contain: Country, Country_Code, City, State, Latitude, Longitude There's also 'ip' which isn't capitalised, and it set whenever you call a Fetch*() method. There are also some private properties, which you shouldn't aughtta muck about with without good reason, including: _server, _agent, and _ua. _server is the address of the server (as may be set in new()), _agent is the UserAgent string (as set in Agent()), and _ua is an LWP object. =back =head1 HOPES AND DREAMS - Make GoogleMap more robust - Add YahooMap and, if they make an API, MapQuestMap - Add GetWeather function. That'd be cool. And maybe some other fun toy methods, like MeasureDistance(ip, ip), and Traceroute(ip) the latter of which might actually do a traceroute on every IP along the way and work out how many miles the packets actually travelled. Altitude() is another thought, and possible to get from public info as long as Latitude and Longitude are populated. =head1 AUTHOR Dodger - =head1 WEBSITE =head1 SEE ALSO perl(1). =cut