Geo-Google

 view release on metacpan or  search on metacpan

lib/Geo/Google.pm  view on Meta::CPAN

    $self->{error} = $msg;
  }
}

=head2 location()

 Usage    : my $loc = $geo->location( address => $address );
 Function : creates a new Geo::Google::Location object, given a
            street address.
 Returns  : a Geo::Google::Location object, or undef on error
 Args     : an anonymous hash:
            key       required?   value
            -------   ---------   -----
            address   yes         address to search for
            id        no          unique identifier for the
                                  location.  useful if producing
                                  XML.
            icon      no          image to be used to represent
                                  point in Google Maps web
                                  application
            infoStyle no          unknown.  css-related, perhaps?

=cut

sub location {
  my ( $self, %arg ) = @_;
  my @result = ();

  my $address   = $arg{'address'} or ($self->error("must provide an address to location()") and return undef);

  my $json = new JSON (skipinvalid => 1, barekey => 1, quotapos => 1, unmapping => 1 );
  my $response_json = undef;
  # I'm using an an array here because I might need to parse several pages if Google suggests a different address
  my @pages = ( get( sprintf( LQ, uri_escape($address) ) ) );
  
  # See if google returned no results
  if ( $pages[0] =~ /did\snot\smatch\sany\slocations/i ) {
    $self->error( "Google couldn't find any locations matching $address." ) and return undef;
  }
  # See if Google was unable to resolve the address, but suggested other addresses
  # To see this, run a query for 695 Charles E Young Dr S, Westwood, CA 90024
  elsif ( $pages[0] =~ m#Did you mean:#is ) {
    # Extract the queries from all the http get queries for alterate addresses
    # \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\" on...
    # We need it to fit the LQ query 'http://maps.google.com/maps?output=js&v=1&q=%s'
    my @queries = $pages[0] =~ m#\\u003cdiv class=\\"ref\\"\\u003e\\u003ca href=\\"/maps\?v=1\\u0026amp;q=(.+?)\\u0026amp;#gsi;
    # clear the $pages array so we can fill it with the pages from the @urls
    @pages = ();
    foreach my $suggested_query (@queries) {
      push( @pages, get( sprintf( LQ, $suggested_query ) ) );
    }
  }
  # Verify that we actually retrieved pages to parse
  if ( scalar(@pages) > 0 ) {
    foreach my $page (@pages) {
      # attempt to locate the JSON formatted data block
      if ($page =~ m#loadVPage\((.+), "\w+"\);}//]]>#is) { $response_json = $json->jsonToObj($1); }
      else {
	$self->error( "Unable to locate the JSON format data in google's response.") and return undef;
      }
      if ( scalar(@{$response_json->{"overlays"}->{"markers"}}) > 0 ) {	
        foreach my $marker (@{$response_json->{"overlays"}->{"markers"}}) {
	  my $loc = $self->_obj2location($marker, %arg);
	  push @result, $loc;
        }		
      }
      else {
	$self->error("Found the JSON Data block and was able to parse it, but it had no location markers "
	  . "in it.  Maybe Google changed their JSON data structure?.") and return undef;
      }
    }
  }
  else {
    $self->error("Google couldn't resolve the address $address but suggested alternate addresses.  "
      . "I attempted to download them but failed.") and return undef;
  }
  return @result;
}

=head2 near()

 Usage    : my @near = $geo->near( $loc, $phrase );
 Function : searches Google Local for records matching the
            phrase provided, with the constraint that they are
            physically nearby the Geo::Google::Location object
            provided.  search phrase is passed verbatim to Google.
 Returns  : a list of Geo::Google::Location objects
 Args     : 1. A Geo::Google::Location object
            2. A search phrase.

=cut

sub near {
  my ( $self, $where, $query ) = @_;
  my $page = get( sprintf( NQ, join(',', $where->lines ), $query ) );
  
  my $json = new JSON (skipinvalid => 1, barekey => 1, 
			quotapos => 1, unmapping => 1 );
  my $response_json = undef;

  # See if google returned no results
  if ( $page =~ /did\snot\smatch\sany\slocations/i ) {
    $self->error( "Google couldn't find a $query near " . $where->title) and return undef;
  }
  # attempt to locate the JSON formatted data block
  elsif ($page =~ m#loadVPage\((.+), "\w+"\);}//]]>#is) {
    my $strJSON = $1;
    $response_json = $json->jsonToObj($strJSON);
  }
  else {
    $self->error( "Unable to locate the JSON format data in Google's response.") and return undef;
  }

  if ( scalar(@{$response_json->{"overlays"}->{"markers"}}) > 0 ) {
    my @result = ();
    foreach my $marker (@{$response_json->{"overlays"}->{"markers"}}) {
      my $loc = $self->_obj2location($marker);
      push @result, $loc;
    }		
    return @result;
  }
  else {
    $self->error("Found the JSON Data block and was "
      . "able to parse it, but it had no location markers"
      . "in it.  Maybe Google changed their "
      . "JSON data structure?") and return undef;
  }
}

=head2 path()

 Usage    : my $path = $geo->path( $from, $OptionalWaypoints, $to );
 Function : get driving directions between two points
 Returns  : a Geo::Google::Path object
 Args     : 1. a Geo::Google::Location object (from)
	    2. optional Geo::Google::Location waypoints
            3. a Geo::Google::Location object (final destination)

=cut

sub path {
  my ( $self, @locations ) = @_;
  my $json = new JSON (skipinvalid => 1, barekey => 1, 
			quotapos => 1, unmapping => 1 );
  my $response_json = undef;

  if(scalar(@locations) < 2) {
    $self->error("Less than two locations were passed to the path function");
    return undef;
  }
  #check each @locations element to see if it is a Geo::Google::Location
  for (my $i=0; $i<=$#locations; $i++) {
	if(!$locations[$i]->isa('Geo::Google::Location')) {
	    $self->error("Location " . ($i+1)
			. " passed to the path function is not a "
			. "Geo::Google::Location"
			. " object, or subclass thereof");
	    return undef;
	}
  }

  # construct the google search text
  my $googlesearch = "from: " . join(', ', $locations[0]->lines);
  for (my $i=1; $i<=$#locations; $i++){
	$googlesearch .= " to:" . join(', ', $locations[$i]->lines);
  }
  my $page = get( sprintf( LQ, uri_escape( $googlesearch ) ) );

  # See if google returned no results
  if ( $page =~ /did\snot\smatch\sany\slocations/i ) {
    $self->error( "Google couldn't find one of the locations you provided for your directions query") and return undef;
  }
  # See if google didn't recognize an input, but suggested
  # a correction to the input that it does recognize
  elsif ( $page =~ m#didyou#s )
  {
    # Parse the JSON to unescape the escaped unicode characters in the URLs we need to parse
    my ( $strJSON ) = $page =~ m#loadVPage\((.+), "\w+"\);}//]]>#s;
    my $suggestion_json = $json->jsonToObj($strJSON);
    # Did you mean:</span><div class="ref"><a href="/maps?v=1&amp;ie=UTF8&amp;hl=en&amp;ct=clnk&amp;cd=1&amp;saddr=695+Charles+E+Young+Dr+S,+Los+Angeles,+Los+Angeles,+California+90024,+United+States&amp;daddr=10948+Weyburn+Ave,+Los+Angeles,+CA+90024+...
    my ( $first_suggestion ) = $suggestion_json->{panel} =~ m#(saddr=.+?)" onclick#s;
    # Get the directions using google's first suggestion
    $page = get ( _html_unescape("http://maps.google.com/maps?output=js&$1") );

    # warn the user using the error method, but don't return undef.
    $self->error("Google suggested a different address for your query.  Using the google suggestion instead.");
  }
  # attept to locate the JSON formatted data block
  if ($page =~ m#loadVPage\((.+), "\w+"\);}//]]>#s) {
    # Extract the JSON data structure from the response.
    $response_json = $json->jsonToObj( $1 );
  }
  else {
    $self->error( "Unable to locate the JSON format data in Google's response.") and return undef;
  }

  my @points;
  my @enc_points;
  for (my $i = 0; $i<=$#{$response_json->{"overlays"}->{"polylines"}}; $i++) {
    $enc_points[$i] = $response_json->{"overlays"}->{"polylines"}->[$i]->{"points"};
    $points[$i] = [ _decode($enc_points[$i]) ];
  }

  # extract a series of directions from HTML inside the panel 
  # portion of the JSON data response, stuffing them in @html_segs
  my @html_segs;
  my $stepsfound = 0;

  my $panel = $response_json->{'panel'};
  $panel =~ s/&#160;/ /g;

  my @subpaths = $panel =~ m#(<table class="(ddrsteps(?: pw)?|ddwpt_table|dirsegment)".+?</table>\s*</div>)#gs; #ddspt_table
  #my ( $subpanel ) = $response_json->{'panel'} =~ m#<table class="ddrsteps pw">(.+)</table>#s;

  foreach my $subpath ( @subpaths ) {
    my @segments = split m#</tr>\s*<tr#s, $subpath;
    foreach my $segment ( @segments ) {
      #skip irrelevant waypoint rows
      if ( $subpath =~ m#ddwpt_table#s && $segment !~ m#ddptlnk#s ) { next }

      my ( $id, $pointIndex ) = $segment =~ m#id="(.+?)" polypoint="(.+?)"#s;
      my ( $html )       = $segment =~ m#"dirsegtext_\d+_\d+">(.+?)</td>#s;
      my ( $distance )   = $segment =~ m#"sxdist".+?>(.+?)<#s;
      my ( $time )       = $segment =~ m#"segtime nw pw">(.+?)<#s;

      if ( ! defined( $id ) ) {
        if ( $subpath =~ m#waypoint="(.+?)"#s ) {
          $id = "waypoint_$1";
	  $html = $locations[$1]->title();
          ($pointIndex)  = $segment =~ m#polypoint="(.+?)"#s;
        }
      }

      next unless $id;

      if ( ! $time ) {
        #some segments are different (why? what is the pattern?)
        my ( $d2, $t2 ) = $segment =~ m#timedist ul.+?>(.+?)\(about&\#160;(.+?)\)</td>#s;
        $time = $t2;
        $distance ||= $d2;
      }

      #some segments have no associated point, e.g. when there are long-distance driving segments

      #some segments have time xor distance (not both)
      $distance   ||= ''; $distance = decode_entities( $distance ); $distance =~ s/\s+/ /g;
      $time       ||= ''; $time     = decode_entities( $time     ); $time =~ s/\s+/ /g;

      push (@html_segs, {
        distance   => $distance,
        time       => $time,
        pointIndex => $pointIndex,
        id         => $id,
        html       => $html
      });
      $stepsfound++;
    }
  }

  if ($stepsfound == 0) {

lib/Geo/Google.pm  view on Meta::CPAN

        }
        %html_seg = %{shift @html_segs};
        push @segments, Geo::Google::Segment->new(
          pointIndex => $html_seg{'pointIndex'},
          id         => $html_seg{'id'},
          html       => decode_entities($html_seg{"html"}),
          distance   => $html_seg{'distance'},
          time       => $html_seg{'time'},
          from       => $points_subset[0],
          to         => $point,
          points     => [@points_subset]
        );
        @points_subset = ();
      } elsif ($html_segs[0]) { # We're working on the last segment
	# This tests to see if we need to wrap up the last segment
         next unless (! $pointset[0]);
         %html_seg = %{shift @html_segs};

	 # An attempt to get the last point in the last segment
	 # set.  Google doesn't include it in their polylines.
	 push @points_subset, $locations[$i+1];
         push @segments, Geo::Google::Segment->new(
            pointIndex => $html_seg{'pointIndex'},
            id         => $html_seg{'id'},
            html       => decode_entities($html_seg{"html"}),
            distance   => $html_seg{'distance'},
            time       => $html_seg{'time'},
            from       => $points_subset[0],
            to         => $locations[$i+1],
            points     => [@points_subset]
          );
          @points_subset = ();
      } else { # we accidentally closed out the last segment early
          push @{ $segments[$#segments]->{points} }, $point;
      }
    }
  }
  # Dirty:  add the final waypoint
  push (@segments, Geo::Google::Segment->new(
          pointIndex => $html_segs[0]{'pointIndex'},
          id         => $html_segs[0]{'id'},
          html       => $html_segs[0]{"html"},
          distance   => $html_segs[0]{'distance'},
          time       => $html_segs[0]{'time'},
          from       => $locations[$#locations],
          to         => $locations[$#locations],
          points     => [ ($locations[$#locations]) ])
	);
  # Extract the total information using a regex on the panel hash.  At the end of the "printheader", we're looking for:
  # <td class="value">9.4&#160;mi &#8211; about 17 mins</td></tr></table>
  # Replace XML numeric character references with spaces to make the next regex less dependent upon Google's precise formatting choices
  $response_json->{"printheader"} =~ s/&#\d+;/ /g;
  if ( $response_json->{"printheader"} =~ m#(\d+\.?\d*)\s*(mi|km|m)\s*about\s*(.+?)</td></tr></table>$#s ){
    return Geo::Google::Path->new(
      segments  => \@segments,
      distance  => $1 . " " . $2,
      time      => $3,
      polyline  => [ @enc_points ],
      locations => [ @locations ],
      panel     => $response_json->{"panel"},
      levels    => $response_json->{"overlays"}->{"polylines"}->[0]->{"levels"} );
  } else {
      $self->error("Could not extract the total route distance and time from google's directions") and return undef;
  }

#$Data::Dumper::Maxdepth=6;
#warn Dumper($path);
 
#<segments distance="0.6&#160;mi" meters="865" seconds="56" time="56 secs">
#  <segment distance="0.4&#160;mi" id="seg0" meters="593" pointIndex="0" seconds="38" time="38 secs">Head <b>southwest</b> from <b>Venice Blvd</b></segment>
#  <segment distance="0.2&#160;mi" id="seg1" meters="272" pointIndex="6" seconds="18" time="18 secs">Make a <b>U-turn</b> at <b>Venice Blvd</b></segment>
#</segments>
}

=head1 INTERNAL FUNCTIONS AND METHODS

=cut

=head2 _decode_word()

 Usage    : my $float = _decode_word($encoded_quintet_word);
 Function : turn a quintet word into a float for the _decode() function
 Returns  : a float
 Args     : one data word made of ASCII characters carrying
            a five-bit number per character from an encoded 
	    Google polyline string

=cut

sub _decode_word {
  my $quintets = shift;
  my @quintets = split '', $quintets;
  my $num_chars = scalar(@quintets);
  my $i = 0;
  my $final_number = 0;
  my $ordinal_offset = 63;
  
  while ($i < $num_chars ) {
    if ( ord($quintets[$i]) < 95 ) { $ordinal_offset = 63; }
    else { 		             $ordinal_offset = 95; }
    my $quintet = ord( $quintets[$i] ) - $ordinal_offset;
    $final_number |= $quintet << ( $i * 5 );
    $i++;
  }
  if ($final_number % 2 > 0) { $final_number *= -1; $final_number --; }
  return $final_number / 2E5;
}

=head2 _decode()

 Usage    : my @points = _decode($encoded_points);
 Function : decode a polyline into its composite lat/lon pairs
 Returns  : an array of floats (lat1, long1, lat2, long2 ... )
 Args     : an encoded google polyline string

=cut

sub _decode {
  # Each letter in the polyline is a quintet (five bits in a row).
  # A grouping of quintets that makes up a number we'll use
  # to calculate lat and long will be called a "word".

lib/Geo/Google.pm  view on Meta::CPAN

 Returns  : a string containing one encoded coordinate that
	    will be added to a polyline string
 Args     : one data word made of ASCII characters carrying
            a five-bit number per character from an encoded 
	    Google polyline string

=cut

sub _encode_word {
  my $coordinate = shift;
  # Convert the floating point coordinate into a doubled signed integer.  -38.45671 turns into -7691342
  # 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).
  my $signed_int = int( sprintf("%8.0f", $coordinate * 2E5) );
  # If the signed integer is negative, add one then lose the sign.  -7691342 turns into 7691341
  my $unsigned_int;
  if ($signed_int < 0) { $unsigned_int = -($signed_int + 1); }
  else		       { $unsigned_int = $signed_int;	     }
  
  # Quintets get created in reverse order (least signficant quintet first, most significant quintet last)
  my $ordinal_offset;
  my $quintet;
  
  # This do...while structure allows me to properly encode the coordinate 0
  do {
    if ( $unsigned_int < 32 ) { $ordinal_offset = 63; } #last quintet
    else { 		        $ordinal_offset = 95; }
    my $quintet_mask = ( $unsigned_int >> 5 ) << 5;
    $quintet .= chr( ( $unsigned_int ^ $quintet_mask ) + $ordinal_offset );
    $unsigned_int = $unsigned_int >> 5;
  } while ( $unsigned_int > 0 );
  return $quintet;
}

=head2 _html_unescape()

 Usage    : my $clean = _html_unescape($dirty);
 Function : does HTML unescape of & > < " special characters
 Returns  : an unescaped HTML string
 Args     : an HTML string.

=cut

sub _html_unescape {
  my ( $raw ) = shift;

  while ( $raw =~ m!&(amp|gt|lt|quot);!) {
    $raw =~ s!&amp;!&!g;
    $raw =~ s!&gt;!>!g;
    $raw =~ s!&lt;!<!g;
    $raw =~ s!&quot;!"!g;
  }
  return $raw;
}

=head2 _obj2location()

 Usage    : my $loc = _obj2location($obj);
 Function : converts a perl object generated from a Google Maps 
		JSON response to a Geo::Google::Location object
 Returns  : a Geo::Google::Location object
 Args     : a member of the $obj->{overlays}->{markers}->[] 
		anonymous array that you get when you read google's 
		JSON response and parse it using JSON::jsonToObj()

=cut

sub _obj2location {
  my ( $self, $marker, %arg ) = @_;

  my @lines;
  my $title;
  my $description;
  # Check to make sure that the info window contents are HTML
  # and that google hasn't changed the format since I wrote this
  if ( $marker->{"infoWindow"}->{"type"} eq "html" ) {
    if ($marker->{"laddr"} =~ /\((.+)\)\s\@\-?\d+\.\d+,\-?\d+\.\d+$/s){
      $title = $1;
    }
    else {
      $title = $marker->{"laddr"};
    }

    $description = decode_entities($marker->{"infoWindow"}->{"basics"});
    # replace </P>, <BR>, <BR/> and <BR /> with newlines
    $description =~ s/<\/p>|<br\s?\/?>/\n/gi;
    # remove all remaining markup tags
    $description =~ s/<.+>//g;
  }
  else {
    # this is a non-fatal nuisance error, only lat/long are 
    # absolutely essential products of this function
    $title = "Could not extract a title or description from "
	. "google's response.  Have they changed their format since "
	. "this function was written?";
  }  

  my $loc = Geo::Google::Location->new(
    title     => $title,
    latitude  => $marker->{"lat"},
    longitude => $marker->{"lng"},
    lines     => [ @{ $marker->{"addressLines"} } ],
    id        => $marker->{"id"}
                 || $arg{'id'}
                 || md5_hex( localtime() ),
    infostyle => $arg{'icon'}
                 || 'http://maps.google.com/mapfiles/marker.png',
    icon      => "http://maps.google.com" . $marker->{"image"}
                 || $arg{'infoStyle'}
                 || 'http://maps.google.com/mapfiles/arrow.png'
  );
  return $loc;

qq(
    <location id="H" infoStyle="/maps?file=li&amp;hl=en">
      <point lat="34.036003" lng="-118.477652"/>
      <icon class="local" image="/mapfiles/markerH.png"/>
      <info>
        <title xml:space="preserve"><b>Starbucks</b> Coffee: Santa Monica</title>
        <address>
          <line>2525 Wilshire Blvd</line>
          <line>Santa Monica, CA 90403</line>
        </address>
        <phone>(310) 264-0669</phone>
        <distance>1.2 mi SW</distance>
        <references count="5">
          <reference>
            <url>http://www.hellosantamonica.com/YP/c_COFFEESTORES.Cfm</url>
            <domain>hellosantamonica.com</domain>
            <title xml:space="preserve">Santa Monica California Yellow Pages. COFFEE STORES <b>...</b></title><shorttitle xml:space="preserve">Santa Monica California Yel...</shorttitle>
          </reference>
        </references>
        <url>/local?q=Starbucks+Coffee:+Santa+Monica&amp;near=Santa+Monica,+CA+90403&amp;latlng=34047451,-118462143,1897416402105863377</url>
      </info>
    </location>
);
}

=head2 _JSONrenderSkeleton()

 Usage    : my $perlvariable = _JSONrenderSkeleton();
 Function : creates the skeleton of a perl data structure used by 
		the Geo::Google::Location and Geo::Google::Path for 
		rendering to Google Maps JSON format
 Returns  : a mildly complex multi-level anonymous hash/array 
		perl data structure that corresponds to the Google 
		Maps JSON data structure
 Args     : none

=cut

sub _JSONrenderSkeleton{
	# This data structure is based on a sample query
	# performed on 27 Dec 06 by Michael Trowbridge
	return {
          'urlViewport' => 0,
          'ei' => '',
          'form' => {
                      'l' => {
                               'q' => '',
                               'near' => ''
                             },
                      'q' => {
                               'q' => ''
                             },
                      'd' => {
                               'saddr' => '',
                               'daddr' => '',
                               'dfaddr' => ''
                             },
                      'selected' => ''
                    },
          'overlays' => {
                          'polylines' => [],
                          'markers' => [],
                          'polygons' => []
                        },
          'printheader' => '',
          'modules' => [
                         undef
                       ],
          'viewport' => {
                          'mapType' => '',
                          'span' => {
                                      'lat' => '',
                                      'lng' => ''
                                    },
                          'center' => {
                                        'lat' => '',
                                        'lng' => ''
                                      }
                        },
          'panelResizeState' => 'not resizeable',
          'ssMap' => {
                       '' => ''
                     },
          'vartitle' => '',
          'url' => '/maps?v=1&q=URI_ESCAPED_QUERY_GOES_HERE&ie=UTF8',
          'title' => ''
        };
}

1;

#http://brevity.org/toys/google/google-draw-pl.txt

__END__



( run in 0.525 second using v1.01-cache-2.11-cpan-39bf76dae61 )