File Coverage

blib/lib/Geo/What3Words.pm
Criterion Covered Total %
statement 78 104 75.0
branch 9 18 50.0
condition 9 26 34.6
subroutine 17 22 77.2
pod 8 9 88.8
total 121 179 67.6


line stmt bran cond sub pod time code
1             # ABSTRACT: turn WGS84 coordinates into three word addresses and vice-versa using what3words.com HTTPS API
2              
3             package Geo::What3Words;
4             $Geo::What3Words::VERSION = '3.0.3';
5 1     1   150228 use strict;
  1         4  
  1         33  
6 1     1   5 use warnings;
  1         3  
  1         35  
7 1     1   1024 use Cpanel::JSON::XS;
  1         6971  
  1         92  
8 1     1   10 use Data::Dumper;
  1         2  
  1         60  
9             $Data::Dumper::Sortkeys = 1;
10 1     1   839 use Encode qw( decode_utf8 );
  1         10962  
  1         104  
11 1     1   10 use HTTP::Tiny;
  1         3  
  1         26  
12 1     1   864 use Net::Ping;
  1         19887  
  1         102  
13 1     1   700 use Net::Ping::External;
  1         2026  
  1         68  
14 1     1   706 use Ref::Util qw( is_hashref is_coderef );
  1         1830  
  1         85  
15 1     1   729 use URI;
  1         4968  
  1         518  
16             # DO NOT TRY TO USE URI::XS IT JUST LEADS TO PROBLEMS
17              
18             my $JSONXS = Cpanel::JSON::XS->new->allow_nonref(1);
19              
20              
21              
22              
23             sub new {
24 4     4 1 4895 my ($class, %params) = @_;
25              
26 4         13 my $self = {};
27 4   50     34 $self->{api_endpoint} = $params{api_endpoint} || 'https://api.what3words.com/v3/';
28 4   100     30 $self->{key} = $params{key} || die "API key not set";
29 3         9 $self->{language} = $params{language};
30 3         10 $self->{logging} = $params{logging};
31              
32             ## _ua is used for testing. But could also be used to
33             ## set proxies or such
34 3   33     14 $self->{ua} = $params{ua} || HTTP::Tiny->new;
35              
36 3   50     11 my $version = $Geo::What3Words::VERSION || '';
37 3         22 $self->{ua}->agent("Perl Geo::What3Words $version");
38              
39 3         70 return bless($self, $class);
40             }
41              
42              
43             sub ping {
44 0     0 1 0 my $self = shift;
45              
46             ## http://example.com/some/path => example.com
47             ## also works with IP addresses
48 0         0 my $host = URI->new($self->{api_endpoint})->host;
49              
50 0         0 $self->_log("pinging $host...");
51              
52 0         0 my $netping = Net::Ping->new('external');
53 0         0 my $res = $netping->ping($host);
54              
55 0 0       0 $self->_log($res ? 'available' : 'unavailable');
56              
57 0         0 return $res;
58             }
59              
60              
61             sub words2pos {
62 0     0 1 0 my ($self, @params) = @_;
63              
64 0         0 my $res = $self->words_to_position(@params);
65 0 0 0     0 if ($res && is_hashref($res) && exists($res->{coordinates})) {
      0        
66 0         0 return $res->{coordinates}->{lat} . ',' . $res->{coordinates}->{lng};
67             }
68 0         0 return;
69             }
70              
71              
72              
73             sub pos2words {
74 1     1 1 8 my ($self, @params) = @_;
75 1         5 my $res = $self->position_to_words(@params);
76 1 0 33     8 if ($res && is_hashref($res) && exists($res->{words})) {
      33        
77 0         0 return $res->{words};
78             }
79 1         8 return;
80             }
81              
82              
83             sub valid_words_format {
84 9     9 1 28 my $self = shift;
85 9         18 my $words = shift;
86              
87             ## Translating the PHP regular expression w3w uses in their
88             ## documentation
89             ## http://perldoc.perl.org/perlunicode.html#Unicode-Character-Properties
90             ## http://php.net/manual/en/reference.pcre.pattern.differences.php
91 9 100       24 return 0 unless $words;
92 1 100   1   10 return 1 if ($words =~ m/^(\p{Lower}+)\.(\p{Lower}+)\.(\p{Lower}+)$/);
  1         17  
  1         22  
  7         93  
93 4         17 return 0;
94             }
95              
96              
97             sub words_to_position {
98 0     0 1 0 my $self = shift;
99 0         0 my $words = shift;
100              
101 0         0 return $self->_query_remote_api('convert-to-coordinates', {words => $words});
102              
103             }
104              
105              
106             sub position_to_words {
107 1     1 1 2 my $self = shift;
108 1         2 my $position = shift;
109 1   33     10 my $language = shift || $self->{language};
110              
111             # https://developer.what3words.com/public-api/docs#convert-to-3wa
112 1         8 return $self->_query_remote_api(
113             'convert-to-3wa',
114             { coordinates => $position,
115             language => $language
116             }
117             );
118             }
119              
120              
121             sub get_languages {
122 0     0 1 0 my $self = shift;
123 0         0 my $position = shift;
124 0         0 return $self->_query_remote_api('available-languages');
125             }
126              
127             sub oneword_available {
128 0     0 0 0 warn 'deprecated method: oneword_available';
129 0         0 return;
130             }
131              
132             sub _query_remote_api {
133 1     1   2 my $self = shift;
134 1         2 my $method_name = shift;
135 1   50     4 my $rh_params = shift || {};
136              
137             my $rh_fields = {
138             #a => 1,
139             key => $self->{key},
140 1         5 format => 'json',
141             %$rh_params
142             };
143              
144 1         6 foreach my $key (keys %$rh_fields) {
145 4 100       13 delete $rh_fields->{$key} if (!defined($rh_fields->{$key}));
146             }
147              
148 1         13 my $uri = URI->new($self->{api_endpoint} . $method_name);
149 1         10244 $uri->query_form($rh_fields);
150 1         265 my $url = $uri->as_string;
151              
152 1         12 $self->_log("GET $url");
153 1         34 my $response = $self->{ua}->get($url);
154              
155 1 50       160437 if (!$response->{success}) {
156 1         31 warn "got failed response from $url: " . $response->{status};
157 1         102 $self->_log("got failed response from $url: " . $response->{status});
158 1         36 return;
159             }
160              
161 0         0 my $json = $response->{content};
162 0         0 $json = decode_utf8($json);
163 0         0 $self->_log($json);
164              
165 0         0 return $JSONXS->decode($json);
166             }
167              
168             sub _log {
169 2     2   6 my $self = shift;
170 2         4 my $message = shift;
171 2 50       13 return unless $self->{logging};
172              
173 2 50       13 if (is_coderef($self->{logging})) {
174 2         5 my $lc = $self->{logging};
175 2         15 &$lc("Geo::What3Words -- " . $message);
176             } else {
177 0         0 print "Geo::What3Words -- " . $message . "\n";
178             }
179 2         1216 return;
180             }
181              
182              
183             1;
184              
185             __END__
186              
187             =pod
188              
189             =encoding UTF-8
190              
191             =head1 NAME
192              
193             Geo::What3Words - turn WGS84 coordinates into three word addresses and vice-versa using what3words.com HTTPS API
194              
195             =head1 VERSION
196              
197             version 3.0.3
198              
199             =head1 SYNOPSIS
200              
201             my $w3w = Geo::What3Words->new();
202              
203             $w3w->pos2words('51.484463,-0.195405');
204             # returns 'three.example.words'
205              
206             $w3w->pos2words('51.484463,-0.195405', 'ru');
207             # returns 'три.пример.слова'
208              
209             $w3w->words2pos('three.example.words');
210             # returns '51.484463,-0.195405' (latitude,longitude)
211              
212             =head1 DESCRIPTION
213              
214             what3words (http://what3words.com/) divides the world into 57 trillion squares
215             of 3 metres x 3 metres. Each square has been given a 3 word address comprised
216             of 3 words from the dictionary.
217              
218             This module calls API version 3 (https://docs.what3words.com/public-api/)
219             to convert coordinates into 3 word addresses (forward) and 3
220             words into coordinates (reverse).
221              
222             Versions 1 and 2 are deprecated and are no longer supported.
223              
224             You need to sign up at http://what3words.com/login and then register for
225             an API key at https://developer.what3words.com
226              
227             =head1 METHODS
228              
229             =head2 new
230              
231             Creates a new instance. The api key is required.
232              
233             my $w3w = Geo::What3Words->new( key => 'your-api-key' );
234             my $w3w = Geo::What3Words->new( key => 'your-api-key', language => 'ru' );
235              
236             For debugging you can either set logging or provide a callback.
237              
238             my $w3w = Geo::What3Words->new( key => 'your-api-key', logging => 1 );
239             # will print debugging output to STDOUT
240              
241             my $callback = sub { my $msg = shift; $my_log4perl_logger->info($msg) };
242             my $w3w = Geo::What3Words->new( key => 'your-api-key', logging => $callback );
243             # will log with log4perl.
244              
245             =head2 ping
246              
247             Check if the remote server is available. This is helpful for debugging or
248             testing, but too slow to run for every conversion.
249              
250             $w3w->ping();
251              
252             =head2 words2pos
253              
254             Tiny wrapper around words_to_position.
255              
256             $w3w->words2pos('three.example.words');
257             # returns '51.484463,-0.195405' (latitude,longitude)
258              
259             $w3w->words2pos('does.not.exist');
260             # returns undef
261              
262             =head2 pos2words
263              
264             Tiny wrapper around position_to_words.
265              
266             $w3w->pos2words('51.484463,-0.195405'); # latitude,longitude
267             # returns 'three.example.words'
268              
269             $w3w->pos2words('51.484463,-0.195405', 'ru');
270             # returns 'три.пример.слова'
271              
272             $w3w->pos2words('invalid,coords');
273             # returns undef
274              
275             =head2 valid_words_format
276              
277             Returns 1 if the string looks like three words, 0 otherwise. Does
278             not call the remote API.
279              
280             $w3w->valid_words_format('one.two.three');
281             # returns 1
282              
283             =head2 words_to_position
284              
285             Returns a more verbose response than words2pos.
286              
287             $w3w->words_to_position('prom.cape.pump');
288             # {
289             # 'coordinates' => {
290             # 'lat' => '51.484463',
291             # 'lng' => '-0.195405'
292             # },
293             # 'country' => 'GB',
294             # 'language' => 'en',
295             # 'map' => 'https://w3w.co/prom.cape.pump',
296             # 'nearestPlace' => 'Kensington, London',
297             # 'square' => {
298             # 'northeast' => {
299             # 'lat' => '51.484476',
300             # 'lng' => '-0.195383'
301             # },
302             # 'southwest' => {
303             # 'lat' => '51.484449',
304             # 'lng' => '-0.195426'
305             # }
306             # },
307             # 'words' => 'prom.cape.pump'
308             # };
309              
310             =head2 position_to_words
311              
312             Returns a more verbose response than pos2words.
313              
314             $w3w->position_to_words('51.484463,-0.195405')
315              
316             # {
317             # 'coordinates' => {
318             # 'lat' => '51.484463',
319             # 'lng' => '-0.195405'
320             # },
321             # 'country' => 'GB',
322             # 'language' => 'en',
323             # 'map' => 'https://w3w.co/prom.cape.pump',
324             # 'nearestPlace' => 'Kensington, London',
325             # 'square' => {
326             # 'northeast' => {
327             # 'lat' => '51.484476',
328             # 'lng' => '-0.195383'
329             # },
330             # 'southwest' => {
331             # 'lat' => '51.484449',
332             # 'lng' => '-0.195426'
333             # }
334             # },
335             # 'words' => 'prom.cape.pump'
336             # };
337              
338             =head2 get_languages
339              
340             Retuns a list of language codes and names.
341              
342             $w3w->get_languages();
343             # {
344             # 'languages' => [
345             # {
346             # 'name' => 'German',
347             # 'nativeName' => 'Deutsch',
348             # 'code' => 'de'
349             # },
350             # {
351             # 'name' => 'English',
352             # 'nativeName' => 'English',
353             # 'code' => 'en'
354             # },
355             # {
356             # 'name' => "Spanish",
357             # 'nativeName' => "Español",
358             # 'code' => 'es'
359             # },
360             # ...
361              
362             =head1 INSTALLATION
363              
364             The test suite will use pre-recorded API responses. If you suspect something
365             changed in the API you can force the test suite to use live requests with
366             your API key
367              
368             PERLLIB=./lib W3W_RECORD_REQUESTS=1 W3W_API_KEY=<your key> perl t/base.t
369              
370             =head1 AUTHOR
371              
372             mtmail <mtmail-cpan@gmx.net>
373              
374             =head1 COPYRIGHT AND LICENSE
375              
376             This software is copyright (c) 2021 by OpenCage GmbH.
377              
378             This is free software; you can redistribute it and/or modify it under
379             the same terms as the Perl 5 programming language system itself.
380              
381             =cut