File Coverage

blib/lib/Geo/Coder/Navteq.pm
Criterion Covered Total %
statement 19 21 90.4
branch n/a
condition n/a
subroutine 7 7 100.0
pod n/a
total 26 28 92.8


line stmt bran cond sub pod time code
1             package Geo::Coder::Navteq;
2              
3 2     2   139769 use strict;
  2         6  
  2         239  
4 2     2   13 use warnings;
  2         5  
  2         70  
5              
6 2     2   11 use Carp qw(croak);
  2         8  
  2         168  
7 2     2   2109 use Encode ();
  2         68127  
  2         339  
8 2     2   5156 use LWP::UserAgent;
  2         388291  
  2         194  
9 2     2   29 use URI;
  2         4  
  2         54  
10 2     2   3847 use XML::Simple ();
  0            
  0            
11              
12             our $VERSION = '0.03';
13             $VERSION = eval $VERSION;
14              
15             sub new {
16             my ($class, @params) = @_;
17             my %params = (@params % 2) ? (appkey => @params) : @params;
18              
19             croak q('appkey' is required) unless $params{appkey};
20              
21             my $self = bless \ %params, $class;
22              
23             if ($params{ua}) {
24             $self->ua($params{ua});
25             }
26             else {
27             $self->{ua} = LWP::UserAgent->new(agent => "$class/$VERSION");
28             }
29              
30             if ($self->{debug}) {
31             my $dump_sub = sub { $_[0]->dump(maxlength => 0); return };
32             $self->ua->set_my_handler(request_send => $dump_sub);
33             $self->ua->set_my_handler(response_done => $dump_sub);
34             }
35             elsif ($self->{compress}) {
36             $self->ua->default_header(accept_encoding => 'gzip,deflate');
37             }
38              
39             # Each appkey has this url aautomatically added on registration.
40             $self->{url} ||= 'http://localhost';
41              
42             return $self;
43             }
44              
45             sub response { $_[0]->{response} }
46              
47             sub ua {
48             my ($self, $ua) = @_;
49             if ($ua) {
50             croak q('ua' must be (or derived from) an LWP::UserAgent')
51             unless ref $ua and $ua->isa(q(LWP::UserAgent));
52             $self->{ua} = $ua;
53             }
54             return $self->{ua};
55             }
56              
57             sub geocode {
58             my ($self, @params) = @_;
59             my %params = (@params % 2) ? (location => @params) : @params;
60              
61             $self->_authenticate or return;
62              
63             my $location = $params{location} or return;
64             $location = Encode::decode('utf-8', $location);
65              
66             my $uri = URI->new('http:/map24/webservices1.5');
67             $uri->host($self->_hostname);
68             $uri->query_form(
69             action => 'soap',
70             bdom => $self->_bdom($location),
71             fromAjax => 1,
72             gzip => 1,
73             mid => '***',
74             request_id => ++$self->{request_id},
75             sid => $self->_session_id,
76             writeTypeAttributes => 'false',
77             xsltdir => 'ajax/2.3.0.4700/bdom_wb/',
78             );
79              
80             my $res = $self->{response} = $self->ua->get(
81             $uri, referer => $self->{url},
82             );
83             return unless $res->is_success;
84              
85             my $xml = $res->decoded_content;
86             return unless $xml;
87              
88             my $data = eval { $self->_parser->xml_in(\$xml) };
89             return unless $data;
90              
91             my $body = $data->{'soapenv:Body'};
92              
93             if (my $err = $body->{'soapenv:Fault'}{faultstring}) {
94             if ($err =~ /RequestHeader NOT authenticated/) {
95             $self->_authenticate(1);
96             return &geocode;
97             }
98              
99             return;
100             }
101              
102             my @results = @{
103             $body->{'tns:searchFreeResponse'}{MapSearchResponse}{Alternatives}
104             || []
105             };
106             if (@results) {
107             $#results = 0 unless wantarray;
108              
109             # Convert from decimal minutes to decimal degrees.
110             for my $result (@results) {
111             do { $_ /= 60 if defined $_ } for
112             @{$result->{Coordinate}}{qw(Latitude Longitude)},
113             @{$result->{PropertiesMinor}}{qw(X0 X1 Y0 Y1)},
114             }
115             }
116              
117             return wantarray ? @results : $results[0];
118             }
119              
120             sub _authenticate {
121             my ($self, $force) = @_;
122              
123             return 1 if not $force and $self->{auth_time};
124              
125             # TODO: determine if there is a standard timeout when sessions need
126             # to be reauthed. That would avoid a single doomed geocode request.
127              
128             my $uri = URI->new('http:/map24/webservices1.5');
129             $uri->host($self->_hostname);
130             $uri->query_form(
131             action => 'GetMap24Application',
132             applicationkey => $self->{appkey},
133             cgi => 'Map24AuthenticationService',
134             requestid => ++$self->{request_id},
135             sid => $self->_session_id,
136             );
137              
138             my $res = $self->{response} = $self->ua->get(
139             $uri, referer => $self->{url},
140             );
141             return unless $res->is_success;
142              
143             my $xml = $res->decoded_content;
144             return unless $xml;
145              
146             my $data = eval { $self->_parser->xml_in(\$xml) };
147             return unless $data;
148              
149             return unless $data->{'soapenv:Body'}{'tns:getMap24ApplicationResponse'}
150             ->{GetMap24ApplicationResponse};
151              
152             $self->{auth_time} = time;
153              
154             return 1;
155             }
156              
157             sub _parser {
158             $_[0]->{parser} ||= XML::Simple->new(
159             ContentKey => '-Value',
160             ForceArray => ['item'],
161             GroupTags => {
162             Alternatives => 'item',
163             PropertiesMajor => 'item',
164             PropertiesMinor => 'item',
165             },
166             KeyAttr => ['Key'],
167             NoAttr => 1,
168             );
169             }
170              
171             {
172             my @chars = (0..9, 'a'..'z');
173             sub _hostname {
174             my $rnd = join '', map { $chars[rand 36] } (1..8);
175             return $rnd . '.tl.maptp50.map24.com';
176             }
177             }
178              
179             sub _session_id {
180             return $_[0]->{session_id} ||= 'AJAXSESS_' . time . '123_' . rand;
181             }
182              
183             # The encoding scheme takes a SOAP message and converts it into a binary
184             # representation of the resulting DOM. Only the location and session id will
185             # vary between messages, so the bulk of the message is pre-encoded.
186             sub _bdom {
187             my ($self, $location) = @_;
188             return '.74fsearchFree.7n_basicZ75Ltns.0WsearchFreeZ78vurn.0W'
189             . 'Map24Geocoder51Z7D_.0G.0G.0GZ'
190             . _encode_string($self->_session_id) . 'Z'
191             . _encode_string($location) . 'XgzWgAgBWgCgDWgEgFXgJXgMWgGgNWgHgIX'
192             . 'gLXgaVgOUXgbVgPUUXg0Xg1VD4fUXg8VgQUUUUU';
193             }
194              
195             {
196             my @encode_table = (0..9, 'a'..'z', 'A'..'Z', qw(. _));
197             my %decode_table = do { my $i = 0; map { $_ => $i++ } @encode_table };
198              
199             sub _encode_string {
200             my ($str) = @_;
201              
202             return 0 unless defined $str;
203              
204             $str=~ s{ ([^0-9A-Za-z]) }{
205             my $ord = ord $1;
206             if (4096 > $ord) {
207             join '', '.', @encode_table[$ord >> 6, $ord & 63];
208             }
209             else {
210             join '', '_', @encode_table[
211             $ord >> 24, $ord >> 18 & 63, $ord >> 12 & 63,
212             $ord >> 16 & 63, $ord & 63
213             ];
214             }
215             }egx;
216              
217             my $prefix = _encode_number(length $str);
218              
219             return $encode_table[ $decode_table{ substr($prefix, 0, 1) } & 15]
220             . substr($prefix, 1) . $str;
221             }
222              
223             sub _encode_number {
224             my ($num) = @_;
225             return $encode_table[32] unless $num;
226              
227             my $len = length($num);
228             my $chunks = int(($len - 1) / 3) + 2;
229             my @s = ('D');
230             my $end = 0;
231              
232             for my $chunk (0 .. $chunks - 1) {
233             my $i = $chunk * 3;
234             my @c = (0, 0, 0);
235              
236             for my $j (0 .. 2) {
237             if ($i >= $len) {
238             $c[$j] = 15;
239             $end = 1;
240             }
241             else {
242             $c[$j] = ord(substr $num, $i, 1) - 48;
243             $c[$j] = 0 if $c[$j] < 0 or $c[$j] > 9;
244             }
245              
246             $i++
247             }
248             my $val = $c[0] << 8 | $c[1] << 4 | $c[2];
249             push @s, @encode_table[$val >> 6, $val & 63];
250              
251             last if $end;
252             }
253             unless ($end) {
254             $s[-1] = $encode_table[ $decode_table{$s[-1]} | 15 ];
255             }
256              
257             return join '', @s;
258             }
259             }
260              
261              
262             1;
263              
264             __END__