File Coverage

blib/lib/Geo/Google.pm
Criterion Covered Total %
statement 112 291 38.4
branch 16 100 16.0
condition 2 34 5.8
subroutine 20 26 76.9
pod 5 6 83.3
total 155 457 33.9


line stmt bran cond sub pod time code
1             =head1 NAME
2              
3             Geo::Google - Perform geographical queries using Google Maps
4              
5             =head1 SYNOPSIS
6              
7             use strict;
8             use Data::Dumper;
9             use Geo::Google;
10              
11             #Allen's office
12             my $gonda_addr = '695 Charles E Young Dr S, Los Angeles, Los Angeles, California 90024, United States';
13             #Stan's Donuts
14             my $stans_addr = '10948 Weyburn Ave, Westwood, CA 90024';
15             #Roscoe's House of Chicken and Waffles
16             my $roscoes_addr = "5006 W Pico Blvd, Los Angeles, CA 90019";
17              
18             #Instantiate a new Geo::Google object.
19             my $geo = Geo::Google->new();
20              
21             #Create Geo::Google::Location objects. These contain
22             #latitude/longitude coordinates, along with a few other details
23             #about the locus.
24             my ( $gonda ) = $geo->location( address => $gonda_addr );
25             my ( $stans ) = $geo->location( address => $stans_addr );
26             my ( $roscoes ) = $geo->location( address => $roscoes_addr );
27             print $gonda->latitude, " / ", $gonda->longitude, "\n";
28             print $stans->latitude, " / ", $stans->longitude, "\n";
29             print $roscoes->latitude, " / ", $roscoes->longitude, "\n";
30              
31             #Create a Geo::Google::Path object from $gonda to $roscoes
32             #by way of $stans.
33             my ( $donut_path ) = $geo->path($gonda, $stans, $roscoes);
34              
35             #A path contains a series of Geo::Google::Segment objects with
36             #text labels representing turn-by-turn driving directions between
37             #two or more locations.
38             my @segments = $donut_path->segments();
39              
40             #This is the human-readable directions for the first leg of the
41             #journey.
42             print $segments[0]->text(),"\n";
43              
44             #Geo::Google::Segment objects contain a series of
45             #Geo::Google::Location objects -- one for each time the segment
46             #deviates from a straight line to the end of the segment.
47             my @points = $segments[1]->points;
48             print $points[0]->latitude, " / ", $points[0]->longitude, "\n";
49              
50             #Now how about some coffee nearby?
51             my @coffee = $geo->near($stans,'coffee');
52             #Too many. How about some Coffee Bean & Tea Leaf?
53             @coffee = grep { $_->title =~ /Coffee.*?Bean/i } @coffee;
54              
55             #Still too many. Let's find the closest with a little trig and
56             #a Schwartzian transform
57             my ( $coffee ) = map { $_->[1] }
58             sort { $a->[0] <=> $b->[0] }
59             map { [ sqrt(
60             ($_->longitude - $stans->longitude)**2
61             +
62             ($_->latitude - $stans->latitude)**2
63             ), $_ ] } @coffee;
64              
65             # Export a location as XML for part of a Google Earth KML file
66             my $strStansDonutsXML = $stans->toXML();
67            
68             # Export a location as JSON data to use with Google Maps
69             my $strRoscoesJSON = $roscoes->toJSON();
70              
71             =head1 DESCRIPTION
72              
73             Geo::Google provides access to the map data used by the popular
74             L web application.
75              
76             =head2 WHAT IS PROVIDED
77              
78             =over
79              
80             =item Conversion of a street address to a 2D Cartesian point
81             (latitude/longitude)
82              
83             =item Conversion of a pair of points to a multi-segmented path of
84             driving directions between the two points.
85              
86             =item Querying Google's "Local Search" given a point and one or more
87             query terms.
88              
89             =back
90              
91             =head2 WHAT IS NOT PROVIDED
92              
93             =over
94              
95             =item Documentation of the Google Maps map data XML format
96              
97             =item Documentation of the Google Maps web application API
98              
99             =item Functionality to create your own Google Maps web page.
100              
101             =back
102              
103             =head1 AUTHOR
104              
105             Allen Day Eallenday@ucla.eduE, Michael Trowbridge
106             Emichael.a.trowbridge@gmail.comE
107              
108             =head1 COPYRIGHT AND LICENSE
109              
110             Copyright (c) 2004-2007 Allen Day. All rights
111             reserved. This program is free software; you can redistribute it
112             and/or modify it under the same terms as Perl itself.
113              
114             =head1 BUGS / TODO
115              
116             Report documentation and software bugs to the author, or better yet,
117             send a patch. Known bugs/issues:
118              
119             =over
120              
121             =item Lack of documentation.
122              
123             =item JSON exporting is not exactly identical to the original Google
124             JSON response. Some of the Google Maps-specific data is discarded
125             during parsing, and the perl JSON module does not allow for bare keys
126             while exporting to a JSON string. It should still be functionally
127             interchangeable with a Google JSON reponse.
128              
129             =back
130              
131             =head1 SEE ALSO
132              
133             http://maps.google.com
134             http://www.google.com/apis/maps/
135             http://libgmail.sourceforge.net/googlemaps.html
136              
137             =cut
138              
139             package Geo::Google;
140 1     1   55428 use strict;
  1         2  
  1         52  
141             our $VERSION = '0.05';
142              
143             #this gets a javascript page containing map XML
144 1     1   6 use constant LQ => 'http://maps.google.com/maps?output=js&v=1&q=%s';
  1         3  
  1         48  
145              
146             #this gets a javascript page containing map XML. special for "nearby" searches
147 1     1   6 use constant NQ => 'http://maps.google.com/maps?output=js&v=1&near=%s&q=%s';
  1         7  
  1         39  
148              
149             #used in polyline codec
150 1     1   4 use constant END_OF_STREAM => 9999;
  1         2  
  1         38  
151              
152             #external libs
153 1     1   1044 use Data::Dumper;
  1         33989  
  1         113  
154 1     1   11 use Digest::MD5 qw( md5_hex );
  1         2  
  1         82  
155 1     1   1081 use HTML::Entities;
  1         12974  
  1         245  
156 1     1   13 use JSON;
  1         2  
  1         9  
157 1     1   2348 use LWP::Simple;
  1         74415  
  1         10  
158 1     1   451 use URI::Escape;
  1         3  
  1         64  
159              
160             #our libs
161 1     1   618 use Geo::Google::Location;
  1         3  
  1         30  
162 1     1   884 use Geo::Google::Path;
  1         2  
  1         27  
163 1     1   493 use Geo::Google::Segment;
  1         1  
  1         4010  
164              
165 1     1 0 6 sub version { return $VERSION }
166              
167             =head1 CONSTRUCTOR
168              
169             =cut
170              
171             =head2 new()
172              
173             Usage : my $geo = Geo::Google->new();
174             Function : constructs and returns a new Geo::Google object
175             Returns : a Geo::Google object
176             Args : n/a
177              
178             =cut
179              
180             sub new {
181 1     1 1 18 return bless {}, __PACKAGE__;
182             }
183              
184             =head1 OBJECT METHODS
185              
186             =cut
187              
188             =head2 error()
189              
190             Usage : my $error = $geo->error();
191             Function : Fetch error messages produced by the Google Maps XML server.
192             Errors can be produced for a number of reasons, e.g. inability
193             of the server to resolve a street address to geographical
194             coordinates.
195             Returns : The most recent error string. Calling this method clears the
196             last error.
197             Args : n/a
198              
199             =cut
200              
201             sub error {
202 0     0 1 0 my ( $self, $msg ) = @_;
203 0 0 0     0 if ( !defined($msg) or ! $self->isa(__PACKAGE__) ) {
204 0         0 my $error = $self->{error};
205 0         0 $self->{error} = undef;
206 0         0 return $error;
207             }
208             else {
209 0         0 $self->{error} = $msg;
210             }
211             }
212              
213             =head2 location()
214              
215             Usage : my $loc = $geo->location( address => $address );
216             Function : creates a new Geo::Google::Location object, given a
217             street address.
218             Returns : a Geo::Google::Location object, or undef on error
219             Args : an anonymous hash:
220             key required? value
221             ------- --------- -----
222             address yes address to search for
223             id no unique identifier for the
224             location. useful if producing
225             XML.
226             icon no image to be used to represent
227             point in Google Maps web
228             application
229             infoStyle no unknown. css-related, perhaps?
230              
231             =cut
232              
233             sub location {
234 1     1 1 5 my ( $self, %arg ) = @_;
235 1         3 my @result = ();
236              
237 1 50 0     7 my $address = $arg{'address'} or ($self->error("must provide an address to location()") and return undef);
238              
239 1         307 my $json = new JSON (skipinvalid => 1, barekey => 1, quotapos => 1, unmapping => 1 );
240 0         0 my $response_json = undef;
241             # I'm using an an array here because I might need to parse several pages if Google suggests a different address
242 0         0 my @pages = ( get( sprintf( LQ, uri_escape($address) ) ) );
243            
244             # See if google returned no results
245 0 0       0 if ( $pages[0] =~ /did\snot\smatch\sany\slocations/i ) {
    0          
246 0 0       0 $self->error( "Google couldn't find any locations matching $address." ) and return undef;
247             }
248             # See if Google was unable to resolve the address, but suggested other addresses
249             # To see this, run a query for 695 Charles E Young Dr S, Westwood, CA 90024
250             elsif ( $pages[0] =~ m#Did you mean:#is ) {
251             # Extract the queries from all the http get queries for alterate addresses
252             # \u003cdiv class=\"ref\"\u003e\u003ca href=\"/maps?v=1\u0026amp;q=695+Charles+E+Young+Drive+East,+Los+Angeles,+Los+Angeles,+California+90024,+United+States\u0026amp;ie=UTF8\u0026amp;hl=en\u0026amp;oi=georefine\u0026amp;ct=clnk\u0026amp;cd=2\" onclick=\"return loadUrl(this.href)\"\u003e
253             # We need it to fit the LQ query 'http://maps.google.com/maps?output=js&v=1&q=%s'
254 0         0 my @queries = $pages[0] =~ m#\\u003cdiv class=\\"ref\\"\\u003e\\u003ca href=\\"/maps\?v=1\\u0026amp;q=(.+?)\\u0026amp;#gsi;
255             # clear the $pages array so we can fill it with the pages from the @urls
256 0         0 @pages = ();
257 0         0 foreach my $suggested_query (@queries) {
258 0         0 push( @pages, get( sprintf( LQ, $suggested_query ) ) );
259             }
260             }
261             # Verify that we actually retrieved pages to parse
262 0 0       0 if ( scalar(@pages) > 0 ) {
263 0         0 foreach my $page (@pages) {
264             # attempt to locate the JSON formatted data block
265 0 0       0 if ($page =~ m#loadVPage\((.+), "\w+"\);}//]]>#is) { $response_json = $json->jsonToObj($1); }
  0         0  
266             else {
267 0 0       0 $self->error( "Unable to locate the JSON format data in google's response.") and return undef;
268             }
269 0 0       0 if ( scalar(@{$response_json->{"overlays"}->{"markers"}}) > 0 ) {
  0         0  
270 0         0 foreach my $marker (@{$response_json->{"overlays"}->{"markers"}}) {
  0         0  
271 0         0 my $loc = $self->_obj2location($marker, %arg);
272 0         0 push @result, $loc;
273             }
274             }
275             else {
276 0 0       0 $self->error("Found the JSON Data block and was able to parse it, but it had no location markers "
277             . "in it. Maybe Google changed their JSON data structure?.") and return undef;
278             }
279             }
280             }
281             else {
282 0 0       0 $self->error("Google couldn't resolve the address $address but suggested alternate addresses. "
283             . "I attempted to download them but failed.") and return undef;
284             }
285 0         0 return @result;
286             }
287              
288             =head2 near()
289              
290             Usage : my @near = $geo->near( $loc, $phrase );
291             Function : searches Google Local for records matching the
292             phrase provided, with the constraint that they are
293             physically nearby the Geo::Google::Location object
294             provided. search phrase is passed verbatim to Google.
295             Returns : a list of Geo::Google::Location objects
296             Args : 1. A Geo::Google::Location object
297             2. A search phrase.
298              
299             =cut
300              
301             sub near {
302 0     0 1 0 my ( $self, $where, $query ) = @_;
303 0         0 my $page = get( sprintf( NQ, join(',', $where->lines ), $query ) );
304            
305 0         0 my $json = new JSON (skipinvalid => 1, barekey => 1,
306             quotapos => 1, unmapping => 1 );
307 0         0 my $response_json = undef;
308              
309             # See if google returned no results
310 0 0       0 if ( $page =~ /did\snot\smatch\sany\slocations/i ) {
    0          
311 0 0       0 $self->error( "Google couldn't find a $query near " . $where->title) and return undef;
312             }
313             # attempt to locate the JSON formatted data block
314             elsif ($page =~ m#loadVPage\((.+), "\w+"\);}//]]>#is) {
315 0         0 my $strJSON = $1;
316 0         0 $response_json = $json->jsonToObj($strJSON);
317             }
318             else {
319 0 0       0 $self->error( "Unable to locate the JSON format data in Google's response.") and return undef;
320             }
321              
322 0 0       0 if ( scalar(@{$response_json->{"overlays"}->{"markers"}}) > 0 ) {
  0         0  
323 0         0 my @result = ();
324 0         0 foreach my $marker (@{$response_json->{"overlays"}->{"markers"}}) {
  0         0  
325 0         0 my $loc = $self->_obj2location($marker);
326 0         0 push @result, $loc;
327             }
328 0         0 return @result;
329             }
330             else {
331 0 0       0 $self->error("Found the JSON Data block and was "
332             . "able to parse it, but it had no location markers"
333             . "in it. Maybe Google changed their "
334             . "JSON data structure?") and return undef;
335             }
336             }
337              
338             =head2 path()
339              
340             Usage : my $path = $geo->path( $from, $OptionalWaypoints, $to );
341             Function : get driving directions between two points
342             Returns : a Geo::Google::Path object
343             Args : 1. a Geo::Google::Location object (from)
344             2. optional Geo::Google::Location waypoints
345             3. a Geo::Google::Location object (final destination)
346              
347             =cut
348              
349             sub path {
350 0     0 1 0 my ( $self, @locations ) = @_;
351 0         0 my $json = new JSON (skipinvalid => 1, barekey => 1,
352             quotapos => 1, unmapping => 1 );
353 0         0 my $response_json = undef;
354              
355 0 0       0 if(scalar(@locations) < 2) {
356 0         0 $self->error("Less than two locations were passed to the path function");
357 0         0 return undef;
358             }
359             #check each @locations element to see if it is a Geo::Google::Location
360 0         0 for (my $i=0; $i<=$#locations; $i++) {
361 0 0       0 if(!$locations[$i]->isa('Geo::Google::Location')) {
362 0         0 $self->error("Location " . ($i+1)
363             . " passed to the path function is not a "
364             . "Geo::Google::Location"
365             . " object, or subclass thereof");
366 0         0 return undef;
367             }
368             }
369              
370             # construct the google search text
371 0         0 my $googlesearch = "from: " . join(', ', $locations[0]->lines);
372 0         0 for (my $i=1; $i<=$#locations; $i++){
373 0         0 $googlesearch .= " to:" . join(', ', $locations[$i]->lines);
374             }
375 0         0 my $page = get( sprintf( LQ, uri_escape( $googlesearch ) ) );
376              
377             # See if google returned no results
378 0 0       0 if ( $page =~ /did\snot\smatch\sany\slocations/i ) {
    0          
379 0 0       0 $self->error( "Google couldn't find one of the locations you provided for your directions query") and return undef;
380             }
381             # See if google didn't recognize an input, but suggested
382             # a correction to the input that it does recognize
383             elsif ( $page =~ m#didyou#s )
384             {
385             # Parse the JSON to unescape the escaped unicode characters in the URLs we need to parse
386 0         0 my ( $strJSON ) = $page =~ m#loadVPage\((.+), "\w+"\);}//]]>#s;
387 0         0 my $suggestion_json = $json->jsonToObj($strJSON);
388             # Did you mean:
389 0         0 my ( $first_suggestion ) = $suggestion_json->{panel} =~ m#(saddr=.+?)" onclick#s;
390             # Get the directions using google's first suggestion
391 0         0 $page = get ( _html_unescape("http://maps.google.com/maps?output=js&$1") );
392              
393             # warn the user using the error method, but don't return undef.
394 0         0 $self->error("Google suggested a different address for your query. Using the google suggestion instead.");
395             }
396             # attept to locate the JSON formatted data block
397 0 0       0 if ($page =~ m#loadVPage\((.+), "\w+"\);}//]]>#s) {
398             # Extract the JSON data structure from the response.
399 0         0 $response_json = $json->jsonToObj( $1 );
400             }
401             else {
402 0 0       0 $self->error( "Unable to locate the JSON format data in Google's response.") and return undef;
403             }
404              
405 0         0 my @points;
406             my @enc_points;
407 0         0 for (my $i = 0; $i<=$#{$response_json->{"overlays"}->{"polylines"}}; $i++) {
  0         0  
408 0         0 $enc_points[$i] = $response_json->{"overlays"}->{"polylines"}->[$i]->{"points"};
409 0         0 $points[$i] = [ _decode($enc_points[$i]) ];
410             }
411              
412             # extract a series of directions from HTML inside the panel
413             # portion of the JSON data response, stuffing them in @html_segs
414 0         0 my @html_segs;
415 0         0 my $stepsfound = 0;
416              
417 0         0 my $panel = $response_json->{'panel'};
418 0         0 $panel =~ s/ / /g;
419              
420 0         0 my @subpaths = $panel =~ m#(\s*)#gs; #ddspt_table \s* #s; #s;
421             #my ( $subpanel ) = $response_json->{'panel'} =~ m#(.+)
#s;
422              
423 0         0 foreach my $subpath ( @subpaths ) {
424 0         0 my @segments = split m#
425 0         0 foreach my $segment ( @segments ) {
426             #skip irrelevant waypoint rows
427 0 0 0     0 if ( $subpath =~ m#ddwpt_table#s && $segment !~ m#ddptlnk#s ) { next }
  0         0  
428              
429 0         0 my ( $id, $pointIndex ) = $segment =~ m#id="(.+?)" polypoint="(.+?)"#s;
430 0         0 my ( $html ) = $segment =~ m#"dirsegtext_\d+_\d+">(.+?)
431 0         0 my ( $distance ) = $segment =~ m#"sxdist".+?>(.+?)<#s;
432 0         0 my ( $time ) = $segment =~ m#"segtime nw pw">(.+?)<#s;
433              
434 0 0       0 if ( ! defined( $id ) ) {
435 0 0       0 if ( $subpath =~ m#waypoint="(.+?)"#s ) {
436 0         0 $id = "waypoint_$1";
437 0         0 $html = $locations[$1]->title();
438 0         0 ($pointIndex) = $segment =~ m#polypoint="(.+?)"#s;
439             }
440             }
441              
442 0 0       0 next unless $id;
443              
444 0 0       0 if ( ! $time ) {
445             #some segments are different (why? what is the pattern?)
446 0         0 my ( $d2, $t2 ) = $segment =~ m#timedist ul.+?>(.+?)\(about&\#160;(.+?)\)
447 0         0 $time = $t2;
448 0   0     0 $distance ||= $d2;
449             }
450              
451             #some segments have no associated point, e.g. when there are long-distance driving segments
452              
453             #some segments have time xor distance (not both)
454 0   0     0 $distance ||= ''; $distance = decode_entities( $distance ); $distance =~ s/\s+/ /g;
  0         0  
  0         0  
455 0   0     0 $time ||= ''; $time = decode_entities( $time ); $time =~ s/\s+/ /g;
  0         0  
  0         0  
456              
457 0         0 push (@html_segs, {
458             distance => $distance,
459             time => $time,
460             pointIndex => $pointIndex,
461             id => $id,
462             html => $html
463             });
464 0         0 $stepsfound++;
465             }
466             }
467              
468 0 0       0 if ($stepsfound == 0) {
469 0 0       0 $self->error("Found the HTML directions from the JSON "
470             . "reponse, but was not able to extract "
471             . "the driving directions from the HTML") and return undef;
472             }
473 0         0 my @segments = ();
474             # Problem: When you create a Geo::Google::Location by
475             # looking it up on Google from an address, it returns coordinates
476             # with millionth of a degree precision. Coordinates that come out
477             # the polyline string only have hundred thousandth of a degree
478             # precision. This means that the correlation algorithm won't find
479             # the start, stop or waypoints in the polyline unless we round
480             # start, stop and waypoint coordinates to the hundred-thousandth
481             # degree precision.
482 0         0 foreach my $location (@locations) {
483 0         0 $location->{'latitude'} = sprintf("%3.5f", $location->{'latitude'} );
484 0         0 $location->{'longitude'} = sprintf("%3.5f", $location->{'longitude'} );
485             }
486              
487             # Correlate the arrays of lats and longs we decoded from the
488             # JSON object with the segments we extracted from the panel
489             # HTML and put the result into an array of
490             # Geo::Google::Location objects
491 0         0 my @points_subset = ( $locations[0] );
492 0         0 push (@segments, Geo::Google::Segment->new(
493             pointIndex => $html_segs[0]{'pointIndex'},
494             id => $html_segs[0]{'id'},
495             html => $html_segs[0]{"html"},
496             distance => $html_segs[0]{'distance'},
497             time => $html_segs[0]{'time'},
498             from => $locations[0],
499             to => $locations[0],
500             points => [@points_subset])
501             );
502 0         0 shift @html_segs;
503 0         0 for (my $i = 0; $i <= $#points; $i++) {
504             # start/points cause us problems because they're often the same
505             # the same pointindex as the first segment of the directions
506             # pulling the first html_seg off the stack now makes the next
507             # control loop easier to maintain.
508 0         0 @points_subset = ();
509              
510 0         0 my $m = 0;
511 0         0 my @pointset = @{$points[$i]};
  0         0  
512 0         0 while ( @pointset ) {
513 0         0 my $lat = shift @pointset;
514 0         0 my $lon = shift @pointset;
515 0         0 $m++;
516 0         0 my %html_seg;
517              
518             # Check to see if the lat and long belong to a start, stop or waypoint
519 0         0 my $pointislocation = -1;
520 0         0 for (my $j=0; $j <= $#locations; $j++) {
521 0 0 0     0 if ( ( $lat == $locations[$j]->latitude() ) && ( $lon == $locations[$j]->longitude() ) ) { $pointislocation = $j; last; }
  0         0  
  0         0  
522             }
523             # If the point that just came off the pointset array is a start, stop or waypoint, use that start/stop/waypoint.
524             # otherwise, create a new point for the lat/long that just came off the pointset array.
525 0         0 my $point;
526 0 0       0 if ( $pointislocation >= 0 ){ $point = $locations[$pointislocation]; }
  0         0  
527 0         0 else { $point = Geo::Google::Location->new( latitude => $lat, longitude => $lon ); }
528              
529 0         0 push @points_subset, $point;
530              
531 0 0       0 if ( $html_segs[1] ) {
    0          
532             # There's a segment after the one we're working on
533             # This tests to see if we need to wrap up the current segment
534 0 0       0 if ( defined( $html_segs[1]{'pointIndex'} ) ) {
535 0 0 0     0 next unless ((($m == $html_segs[1]{'pointIndex'}) && ($#html_segs > 1) ) || (! @pointset) );
      0        
536             }
537 0         0 %html_seg = %{shift @html_segs};
  0         0  
538 0         0 push @segments, Geo::Google::Segment->new(
539             pointIndex => $html_seg{'pointIndex'},
540             id => $html_seg{'id'},
541             html => decode_entities($html_seg{"html"}),
542             distance => $html_seg{'distance'},
543             time => $html_seg{'time'},
544             from => $points_subset[0],
545             to => $point,
546             points => [@points_subset]
547             );
548 0         0 @points_subset = ();
549             } elsif ($html_segs[0]) { # We're working on the last segment
550             # This tests to see if we need to wrap up the last segment
551 0 0       0 next unless (! $pointset[0]);
552 0         0 %html_seg = %{shift @html_segs};
  0         0  
553              
554             # An attempt to get the last point in the last segment
555             # set. Google doesn't include it in their polylines.
556 0         0 push @points_subset, $locations[$i+1];
557 0         0 push @segments, Geo::Google::Segment->new(
558             pointIndex => $html_seg{'pointIndex'},
559             id => $html_seg{'id'},
560             html => decode_entities($html_seg{"html"}),
561             distance => $html_seg{'distance'},
562             time => $html_seg{'time'},
563             from => $points_subset[0],
564             to => $locations[$i+1],
565             points => [@points_subset]
566             );
567 0         0 @points_subset = ();
568             } else { # we accidentally closed out the last segment early
569 0         0 push @{ $segments[$#segments]->{points} }, $point;
  0         0  
570             }
571             }
572             }
573             # Dirty: add the final waypoint
574 0         0 push (@segments, Geo::Google::Segment->new(
575             pointIndex => $html_segs[0]{'pointIndex'},
576             id => $html_segs[0]{'id'},
577             html => $html_segs[0]{"html"},
578             distance => $html_segs[0]{'distance'},
579             time => $html_segs[0]{'time'},
580             from => $locations[$#locations],
581             to => $locations[$#locations],
582             points => [ ($locations[$#locations]) ])
583             );
584             # Extract the total information using a regex on the panel hash. At the end of the "printheader", we're looking for:
585             # 9.4 mi – about 17 mins
586             # Replace XML numeric character references with spaces to make the next regex less dependent upon Google's precise formatting choices
587 0         0 $response_json->{"printheader"} =~ s/&#\d+;/ /g;
588 0 0       0 if ( $response_json->{"printheader"} =~ m#(\d+\.?\d*)\s*(mi|km|m)\s*about\s*(.+?)
$#s ){ 589 0         0 return Geo::Google::Path->new( 590             segments => \@segments, 591             distance => $1 . " " . $2, 592             time => $3, 593             polyline => [ @enc_points ], 594             locations => [ @locations ], 595             panel => $response_json->{"panel"}, 596             levels => $response_json->{"overlays"}->{"polylines"}->[0]->{"levels"} ); 597             } else { 598 0 0       0 $self->error("Could not extract the total route distance and time from google's directions") and return undef; 599             } 600               601             #$Data::Dumper::Maxdepth=6; 602             #warn Dumper($path); 603             604             # 605             # Head southwest from Venice Blvd 606             # Make a U-turn at Venice Blvd 607             # 608             } 609               610             =head1 INTERNAL FUNCTIONS AND METHODS 611               612             =cut 613               614             =head2 _decode_word() 615               616             Usage : my $float = _decode_word($encoded_quintet_word); 617             Function : turn a quintet word into a float for the _decode() function 618             Returns : a float 619             Args : one data word made of ASCII characters carrying 620             a five-bit number per character from an encoded 621             Google polyline string 622               623             =cut 624               625             sub _decode_word { 626 6     6   10 my $quintets = shift; 627 6         22 my @quintets = split '', $quintets; 628 6         9 my $num_chars = scalar(@quintets); 629 6         7 my $i = 0; 630 6         7 my $final_number = 0; 631 6         7 my $ordinal_offset = 63; 632             633 6         14 while ($i < $num_chars ) { 634 23 100       63 if ( ord($quintets[$i]) < 95 ) { $ordinal_offset = 63; }   6         8   635 17         19 else { $ordinal_offset = 95; } 636 23         24 my $quintet = ord( $quintets[$i] ) - $ordinal_offset; 637 23         27 $final_number |= $quintet << ( $i * 5 ); 638 23         42 $i++; 639             } 640 6 100       15 if ($final_number % 2 > 0) { $final_number *= -1; $final_number --; }   4         6     4         4   641 6         25 return $final_number / 2E5; 642             } 643               644             =head2 _decode() 645               646             Usage : my @points = _decode($encoded_points); 647             Function : decode a polyline into its composite lat/lon pairs 648             Returns : an array of floats (lat1, long1, lat2, long2 ... ) 649             Args : an encoded google polyline string 650               651             =cut 652               653             sub _decode { 654             # Each letter in the polyline is a quintet (five bits in a row). 655             # A grouping of quintets that makes up a number we'll use 656             # to calculate lat and long will be called a "word". 657 1     1   2 my $quintets = shift; 658 1 50       5 return undef unless defined $quintets; 659 1         5 my @quintets = split '', $quintets; 660 1         2 my @locations = (); 661 1         2 my $word = ""; 662               663             # Extract the first lat and long. 664             # The initial latitude word is the first five quintets. 665 1         4 for (my $i=0; $i<=4; $i++) { $word .= $quintets[$i]; }   5         9   666 1         3 push ( @locations, _decode_word($word) ); 667 1         2 my $lastlat = 0; 668               669             # The initial longitude is the next five quintets. 670 1         2 $word = ""; 671 1         5 for (my $i=5; $i<10; $i++) { $word .= $quintets[$i]; }   5         11   672 1         4 push ( @locations, _decode_word($word) ); 673 1         3 my $lastlong = 1; 674               675             # The remaining quintets form words that represent 676             # delta coordinates from the last coordinate. The only 677             # way to identify them is that they are at least one 678             # character long and end in a ASCII character between 679             # ordinal 63 and ordinal 95. Latitude first, then 680             # longitude. 681 1         2 $word = ""; 682 1         1 my $i = 10; 683 1         5 while ($i <= $#quintets) { 684 3         5 $word .= $quintets[$i]; 685 3 100 66     19 if ( (length($word) >= 1) && ( ord($quintets[$i]) <= 95 ) ) { 686 2 100       6 if ( $lastlat > $lastlong ) { 687 1         4 push @locations, _decode_word($word) + $locations[$lastlong]; 688 1         2 $lastlong = $#locations; 689             } 690             else { 691 1         3 push @locations, _decode_word($word) + $locations[$lastlat]; 692 1         2 $lastlat = $#locations; 693             } 694 2         4 $word = ""; 695             } 696 3         7 $i++; 697             } 698             # Prettify results 699 1         3 return map {sprintf("%3.5f",$_)} @locations;   4         26   700             } 701               702             =head2 _encode() 703               704             Usage : my $encoded_points = _encode(@points); 705             Function : encode lat/lon pairs into a polyline string 706             Returns : a string 707             Args : an array of coordinates [38.47823, -118.48571, 38.47845, -118.48582, ...] 708               709             =cut 710               711             sub _encode { 712 1     1   6 my @points = @_; 713 1         2 my $polyline; 714 1         6 for (my $i = 0; $i <= $#points; $i++) { 715             # potential pitfall: pass the correct floating point precision 716             # to the _encode_word() function or 34.06694 - 34.06698 will give you 717             # -3.999999999999999057E-5 which doesn't encode properly. -4E-5 encodes properly. 718 4 100       8 if ( $i > 1 ) { # All points after the first lat/long pair are delta coordinates 719 2         14 $polyline .= _encode_word( sprintf("%3.5f", $points[$i] - $points[$i-2] ) ); 720             } 721             else { 722 2         14 $polyline .= _encode_word( sprintf("%3.5f", $points[$i] ) ); 723             } 724             } 725 1         9 return $polyline; 726             } 727               728             =head2 _encode_word() 729               730             Usage : my $encoded_quintet_word = _encode_word($signed_floating_point_coordinate); 731             Function : turn a signed float (either a full coordinate 732             or a delta) for the _encode() function 733             Returns : a string containing one encoded coordinate that 734             will be added to a polyline string 735             Args : one data word made of ASCII characters carrying 736             a five-bit number per character from an encoded 737             Google polyline string 738               739             =cut 740               741             sub _encode_word { 742 6     6   11 my $coordinate = shift; 743             # Convert the floating point coordinate into a doubled signed integer. -38.45671 turns into -7691342 744             # This looks quirky cos when I used int(-0.00015 * 2E5) I got -29 (should have been -30). Suspect this is a perl 5.8.8 bug (MAT). 745 6         51 my $signed_int = int( sprintf("%8.0f", $coordinate * 2E5) ); 746             # If the signed integer is negative, add one then lose the sign. -7691342 turns into 7691341 747 6         6 my $unsigned_int; 748 6 100       14 if ($signed_int < 0) { $unsigned_int = -($signed_int + 1); }   4         8   749 2         3 else { $unsigned_int = $signed_int; } 750             751             # Quintets get created in reverse order (least signficant quintet first, most significant quintet last) 752 6         7 my $ordinal_offset; 753             my $quintet; 754             755             # This do...while structure allows me to properly encode the coordinate 0 756 6         5 do { 757 23 100       35 if ( $unsigned_int < 32 ) { $ordinal_offset = 63; } #last quintet   6         7   758 17         19 else { $ordinal_offset = 95; } 759 23         26 my $quintet_mask = ( $unsigned_int >> 5 ) << 5; 760 23         34 $quintet .= chr( ( $unsigned_int ^ $quintet_mask ) + $ordinal_offset ); 761 23         47 $unsigned_int = $unsigned_int >> 5; 762             } while ( $unsigned_int > 0 ); 763 6         35 return $quintet; 764             } 765               766             =head2 _html_unescape() 767               768             Usage : my $clean = _html_unescape($dirty); 769             Function : does HTML unescape of & > < " special characters 770             Returns : an unescaped HTML string 771             Args : an HTML string. 772               773             =cut 774               775             sub _html_unescape { 776 0     0     my ( $raw ) = shift; 777               778 0           while ( $raw =~ m!&(amp|gt|lt|quot);!) { 779 0           $raw =~ s!&!&!g; 780 0           $raw =~ s!>!>!g; 781 0           $raw =~ s!<! 782 0           $raw =~ s!"!"!g; 783             } 784 0           return $raw; 785             } 786               787             =head2 _obj2location() 788               789             Usage : my $loc = _obj2location($obj); 790             Function : converts a perl object generated from a Google Maps 791             JSON response to a Geo::Google::Location object 792             Returns : a Geo::Google::Location object 793             Args : a member of the $obj->{overlays}->{markers}->[] 794             anonymous array that you get when you read google's 795             JSON response and parse it using JSON::jsonToObj() 796               797             =cut 798               799             sub _obj2location { 800 0     0     my ( $self, $marker, %arg ) = @_; 801               802 0           my @lines; 803             my $title; 804 0           my $description; 805             # Check to make sure that the info window contents are HTML 806             # and that google hasn't changed the format since I wrote this 807 0 0         if ( $marker->{"infoWindow"}->{"type"} eq "html" ) { 808 0 0         if ($marker->{"laddr"} =~ /\((.+)\)\s\@\-?\d+\.\d+,\-?\d+\.\d+$/s){ 809 0           $title = $1; 810             } 811             else { 812 0           $title = $marker->{"laddr"}; 813             } 814               815 0           $description = decode_entities($marker->{"infoWindow"}->{"basics"}); 816             # replace

,
,
and
with newlines 817 0           $description =~ s/<\/p>|/\n/gi; 818             # remove all remaining markup tags 819 0           $description =~ s/<.+>//g; 820             } 821             else { 822             # this is a non-fatal nuisance error, only lat/long are 823             # absolutely essential products of this function 824 0           $title = "Could not extract a title or description from " 825             . "google's response. Have they changed their format since " 826             . "this function was written?"; 827             } 828               829 0           my $loc = Geo::Google::Location->new( 830             title => $title, 831             latitude => $marker->{"lat"}, 832             longitude => $marker->{"lng"}, 833 0   0       lines => [ @{ $marker->{"addressLines"} } ],       0               0         834             id => $marker->{"id"} 835             || $arg{'id'} 836             || md5_hex( localtime() ), 837             infostyle => $arg{'icon'} 838             || 'http://maps.google.com/mapfiles/marker.png', 839             icon => "http://maps.google.com" . $marker->{"image"} 840             || $arg{'infoStyle'} 841             || 'http://maps.google.com/mapfiles/arrow.png' 842             ); 843 0           return $loc; 844               845 0           qq( 846             847             848             849             850             <b>Starbucks</b> Coffee: Santa Monica 851            
852             2525 Wilshire Blvd 853             Santa Monica, CA 90403 854            
855             (310) 264-0669 856             1.2 mi SW 857             858             859             http://www.hellosantamonica.com/YP/c_COFFEESTORES.Cfm 860             hellosantamonica.com 861             Santa Monica California Yellow Pages. COFFEE STORES <b>...</b>Santa Monica California Yel... 862             863             864             /local?q=Starbucks+Coffee:+Santa+Monica&near=Santa+Monica,+CA+90403&latlng=34047451,-118462143,1897416402105863377 865            
866            
867             ); 868             } 869               870             =head2 _JSONrenderSkeleton() 871               872             Usage : my $perlvariable = _JSONrenderSkeleton(); 873             Function : creates the skeleton of a perl data structure used by 874             the Geo::Google::Location and Geo::Google::Path for 875             rendering to Google Maps JSON format 876             Returns : a mildly complex multi-level anonymous hash/array 877             perl data structure that corresponds to the Google 878             Maps JSON data structure 879             Args : none 880               881             =cut 882               883             sub _JSONrenderSkeleton{ 884             # This data structure is based on a sample query 885             # performed on 27 Dec 06 by Michael Trowbridge 886             return { 887 0     0     'urlViewport' => 0, 888             'ei' => '', 889             'form' => { 890             'l' => { 891             'q' => '', 892             'near' => '' 893             }, 894             'q' => { 895             'q' => '' 896             }, 897             'd' => { 898             'saddr' => '', 899             'daddr' => '', 900             'dfaddr' => '' 901             }, 902             'selected' => '' 903             }, 904             'overlays' => { 905             'polylines' => [], 906             'markers' => [], 907             'polygons' => [] 908             }, 909             'printheader' => '', 910             'modules' => [ 911             undef 912             ], 913             'viewport' => { 914             'mapType' => '', 915             'span' => { 916             'lat' => '', 917             'lng' => '' 918             }, 919             'center' => { 920             'lat' => '', 921             'lng' => '' 922             } 923             }, 924             'panelResizeState' => 'not resizeable', 925             'ssMap' => { 926             '' => '' 927             }, 928             'vartitle' => '', 929             'url' => '/maps?v=1&q=URI_ESCAPED_QUERY_GOES_HERE&ie=UTF8', 930             'title' => '' 931             }; 932             } 933               934             1; 935               936             #http://brevity.org/toys/google/google-draw-pl.txt 937               938             __END__