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
421
|
|
|
|
|
|
|
#my ( $subpanel ) = $response_json->{'panel'} =~ m##s; |
422
|
|
|
|
|
|
|
|
423
|
0
|
|
|
|
|
0
|
foreach my $subpath ( @subpaths ) { |
424
|
0
|
|
|
|
|
0
|
my @segments = split m# | \s*
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+">(.+?) | #s;
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;(.+?)\) | #s;
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 ){