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 9 9 100.0
total 122 179 68.1


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.4';
5 1     1   229061 use strict;
  1         2  
  1         70  
6 1     1   6 use warnings;
  1         2  
  1         54  
7 1     1   1245 use Cpanel::JSON::XS;
  1         8277  
  1         86  
8 1     1   8 use Data::Dumper;
  1         2  
  1         72  
9             $Data::Dumper::Sortkeys = 1;
10 1     1   611 use Encode qw( decode_utf8 );
  1         21249  
  1         145  
11 1     1   10 use HTTP::Tiny;
  1         2  
  1         32  
12 1     1   1275 use Net::Ping;
  1         24265  
  1         110  
13 1     1   759 use Net::Ping::External;
  1         2583  
  1         76  
14 1     1   604 use Ref::Util qw( is_hashref is_coderef );
  1         3010  
  1         93  
15 1     1   928 use URI;
  1         7656  
  1         45  
16 1     1   9 use utf8;
  1         2  
  1         9  
17             # DO NOT TRY TO USE URI::XS IT JUST LEADS TO PROBLEMS
18              
19             my $JSONXS = Cpanel::JSON::XS->new->allow_nonref(1);
20              
21              
22              
23              
24             sub new {
25 4     4 1 218903 my ($class, %params) = @_;
26              
27 4         10 my $self = {};
28 4   50     32 $self->{api_endpoint} = $params{api_endpoint} || 'https://api.what3words.com/v3/';
29 4   100     30 $self->{key} = $params{key} || die "API key not set";
30 3         11 $self->{language} = $params{language};
31 3         17 $self->{logging} = $params{logging};
32              
33             ## _ua is used for testing. But could also be used to
34             ## set proxies or such
35 3   33     15 $self->{ua} = $params{ua} || HTTP::Tiny->new;
36              
37 3   50     10 my $version = $Geo::What3Words::VERSION || '';
38 3         23 $self->{ua}->agent("Perl Geo::What3Words $version");
39              
40 3         45 return bless($self, $class);
41             }
42              
43              
44             sub ping {
45 0     0 1 0 my $self = shift;
46              
47             ## http://example.com/some/path => example.com
48             ## also works with IP addresses
49 0         0 my $host = URI->new($self->{api_endpoint})->host;
50              
51 0         0 $self->_log("pinging $host...");
52              
53 0         0 my $netping = Net::Ping->new('external');
54 0         0 my $res = $netping->ping($host);
55              
56 0 0       0 $self->_log($res ? 'available' : 'unavailable');
57              
58 0         0 return $res;
59             }
60              
61              
62             sub words2pos {
63 0     0 1 0 my ($self, @params) = @_;
64              
65 0         0 my $res = $self->words_to_position(@params);
66 0 0 0     0 if ($res && is_hashref($res) && exists($res->{coordinates})) {
      0        
67 0         0 return $res->{coordinates}->{lat} . ',' . $res->{coordinates}->{lng};
68             }
69 0         0 return;
70             }
71              
72              
73              
74             sub pos2words {
75 1     1 1 9 my ($self, @params) = @_;
76 1         6 my $res = $self->position_to_words(@params);
77 1 0 33     6 if ($res && is_hashref($res) && exists($res->{words})) {
      33        
78 0         0 return $res->{words};
79             }
80 1         6 return;
81             }
82              
83              
84             sub valid_words_format {
85 9     9 1 57 my $self = shift;
86 9         19 my $words = shift;
87              
88             ## Translating the PHP regular expression w3w uses in their
89             ## documentation
90             ## http://perldoc.perl.org/perlunicode.html#Unicode-Character-Properties
91             ## http://php.net/manual/en/reference.pcre.pattern.differences.php
92 9 100       48 return 0 unless $words;
93 7 100       123 return 1 if ($words =~ m/^(\p{Lower}+)\.(\p{Lower}+)\.(\p{Lower}+)$/);
94 4         21 return 0;
95             }
96              
97              
98             sub words_to_position {
99 0     0 1 0 my $self = shift;
100 0         0 my $words = shift;
101              
102 0         0 return $self->_query_remote_api('convert-to-coordinates', {words => $words});
103              
104             }
105              
106              
107             sub position_to_words {
108 1     1 1 4 my $self = shift;
109 1         2 my $position = shift;
110 1   33     11 my $language = shift || $self->{language};
111              
112             # https://developer.what3words.com/public-api/docs#convert-to-3wa
113 1         8 return $self->_query_remote_api(
114             'convert-to-3wa',
115             { coordinates => $position,
116             language => $language
117             }
118             );
119             }
120              
121              
122             sub get_languages {
123 0     0 1 0 my $self = shift;
124 0         0 my $position = shift;
125 0         0 return $self->_query_remote_api('available-languages');
126             }
127              
128              
129             sub oneword_available {
130 0     0 1 0 warn 'deprecated method: oneword_available';
131 0         0 return;
132             }
133              
134             sub _query_remote_api {
135 1     1   3 my $self = shift;
136 1         3 my $method_name = shift;
137 1   50     4 my $rh_params = shift || {};
138              
139             my $rh_fields = {
140             #a => 1,
141             key => $self->{key},
142 1         7 format => 'json',
143             %$rh_params
144             };
145              
146 1         4 foreach my $key (keys %$rh_fields) {
147 4 100       13 delete $rh_fields->{$key} if (!defined($rh_fields->{$key}));
148             }
149              
150 1         13 my $uri = URI->new($self->{api_endpoint} . $method_name);
151 1         13873 $uri->query_form($rh_fields);
152 1         364 my $url = $uri->as_string;
153              
154 1         13 $self->_log("GET $url");
155 1         48 my $response = $self->{ua}->get($url);
156              
157 1 50       436507 if (!$response->{success}) {
158 1         24 warn "got failed response from $url: " . $response->{status};
159 1         118 $self->_log("got failed response from $url: " . $response->{status});
160 1         28 return;
161             }
162              
163 0         0 my $json = $response->{content};
164 0         0 $json = decode_utf8($json);
165 0         0 $self->_log($json);
166              
167 0         0 return $JSONXS->decode($json);
168             }
169              
170             sub _log {
171 2     2   4 my $self = shift;
172 2         5 my $message = shift;
173 2 50       11 return unless $self->{logging};
174              
175 2 50       9 if (is_coderef($self->{logging})) {
176 2         4 my $lc = $self->{logging};
177 2         13 &$lc("Geo::What3Words -- " . $message);
178             } else {
179 0         0 print "Geo::What3Words -- " . $message . "\n";
180             }
181 2         1707 return;
182             }
183              
184              
185             1;
186              
187             __END__
188              
189             =pod
190              
191             =encoding UTF-8
192              
193             =head1 NAME
194              
195             Geo::What3Words - turn WGS84 coordinates into three word addresses and vice-versa using what3words.com HTTPS API
196              
197             =head1 VERSION
198              
199             version 3.0.4
200              
201             =head1 SYNOPSIS
202              
203             my $w3w = Geo::What3Words->new( key => 'your-api-key' );
204              
205             $w3w->pos2words('51.484463,-0.195405');
206             # returns 'prom.cape.pump'
207              
208             $w3w->pos2words('51.484463,-0.195405', 'ru');
209             # returns 'питомец.шутить.намеренно'
210              
211             $w3w->words2pos('prom.cape.pump');
212             # returns '51.484463,-0.195405' (latitude,longitude)
213              
214             =head1 DESCRIPTION
215              
216             what3words (L<https://what3words.com/>) divides the world into 57 trillion
217             squares of 3 metres x 3 metres. Each square has been given a 3 word address
218             comprised of 3 words from the dictionary.
219              
220             This module calls API version 3 (L<https://docs.what3words.com/public-api/>)
221             to convert coordinates into 3 word addresses (forward) and 3
222             words into coordinates (reverse).
223              
224             Versions 1 and 2 are deprecated and are no longer supported.
225              
226             You need to sign up at L<https://what3words.com/login> and then register for
227             an API key at L<https://developer.what3words.com>
228              
229             =head1 METHODS
230              
231             =head2 new
232              
233             Creates a new instance. The C<key> parameter is required.
234              
235             my $w3w = Geo::What3Words->new( key => 'your-api-key' );
236             my $w3w = Geo::What3Words->new( key => 'your-api-key', language => 'ru' );
237              
238             Options:
239              
240             =over 4
241              
242             =item key (required)
243              
244             Your what3words API key.
245              
246             =item language
247              
248             Default language for 3 word addresses (e.g. C<'ru'>, C<'de'>). Can be
249             overridden per request.
250              
251             =item api_endpoint
252              
253             Override the API URL. Defaults to C<https://api.what3words.com/v3/>.
254              
255             =item ua
256              
257             Provide your own L<HTTP::Tiny> instance, e.g. for proxy configuration
258             or testing.
259              
260             =item logging
261              
262             For debugging you can either set logging to a true value or provide a
263             callback.
264              
265             my $w3w = Geo::What3Words->new( key => 'your-api-key', logging => 1 );
266             # will print debugging output to STDOUT
267              
268             my $callback = sub { my $msg = shift; $my_log4perl_logger->info($msg) };
269             my $w3w = Geo::What3Words->new( key => 'your-api-key', logging => $callback );
270             # will log with log4perl
271              
272             =back
273              
274             =head2 ping
275              
276             Check if the remote server is available. This is helpful for debugging or
277             testing, but too slow to run for every conversion.
278              
279             $w3w->ping();
280              
281             =head2 words2pos
282              
283             Convenience wrapper around C<words_to_position>. Takes a 3 word address
284             string, returns a string C<'latitude,longitude'> or C<undef> on failure.
285              
286             $w3w->words2pos('prom.cape.pump');
287             # returns '51.484463,-0.195405'
288              
289             $w3w->words2pos('does.not.exist');
290             # returns undef
291              
292             =head2 pos2words
293              
294             Convenience wrapper around C<position_to_words>. Takes a string
295             C<'latitude,longitude'> and an optional language code. Returns a 3 word
296             address string or C<undef> on failure.
297              
298             $w3w->pos2words('51.484463,-0.195405');
299             # returns 'prom.cape.pump'
300              
301             $w3w->pos2words('51.484463,-0.195405', 'ru');
302             # returns 'питомец.шутить.намеренно'
303              
304             $w3w->pos2words('invalid,coords');
305             # returns undef
306              
307             =head2 valid_words_format
308              
309             Returns 1 if the string looks like three dot-separated words, 0 otherwise.
310             Supports Unicode (e.g. Cyrillic, Turkish). Does not call the remote API.
311              
312             $w3w->valid_words_format('prom.cape.pump');
313             # returns 1
314              
315             $w3w->valid_words_format('диета.новшество.компаньон');
316             # returns 1
317              
318             $w3w->valid_words_format('Not.Valid.Format');
319             # returns 0 (uppercase letters)
320              
321             =head2 words_to_position
322              
323             Takes a 3 word address string and returns a hashref with coordinates,
324             country, language, map link, nearest place, and bounding square.
325             Returns C<undef> on failure.
326              
327             $w3w->words_to_position('prom.cape.pump');
328             # {
329             # 'coordinates' => {
330             # 'lat' => '51.484463',
331             # 'lng' => '-0.195405'
332             # },
333             # 'country' => 'GB',
334             # 'language' => 'en',
335             # 'map' => 'https://w3w.co/prom.cape.pump',
336             # 'nearestPlace' => 'Kensington, London',
337             # 'square' => {
338             # 'northeast' => {
339             # 'lat' => '51.484476',
340             # 'lng' => '-0.195383'
341             # },
342             # 'southwest' => {
343             # 'lat' => '51.484449',
344             # 'lng' => '-0.195426'
345             # }
346             # },
347             # 'words' => 'prom.cape.pump'
348             # };
349              
350             =head2 position_to_words
351              
352             Takes a string C<'latitude,longitude'> and an optional language code.
353             Returns a hashref with coordinates, country, language, map link, nearest
354             place, and bounding square. Returns C<undef> on failure.
355              
356             $w3w->position_to_words('51.484463,-0.195405');
357             $w3w->position_to_words('51.484463,-0.195405', 'ru');
358              
359             # {
360             # 'coordinates' => {
361             # 'lat' => '51.484463',
362             # 'lng' => '-0.195405'
363             # },
364             # 'country' => 'GB',
365             # 'language' => 'en',
366             # 'map' => 'https://w3w.co/prom.cape.pump',
367             # 'nearestPlace' => 'Kensington, London',
368             # 'square' => {
369             # 'northeast' => {
370             # 'lat' => '51.484476',
371             # 'lng' => '-0.195383'
372             # },
373             # 'southwest' => {
374             # 'lat' => '51.484449',
375             # 'lng' => '-0.195426'
376             # }
377             # },
378             # 'words' => 'prom.cape.pump'
379             # };
380              
381             =head2 get_languages
382              
383             Returns a hashref containing a list of supported language codes and names.
384              
385             $w3w->get_languages();
386             # {
387             # 'languages' => [
388             # {
389             # 'name' => 'German',
390             # 'nativeName' => 'Deutsch',
391             # 'code' => 'de'
392             # },
393             # {
394             # 'name' => 'English',
395             # 'nativeName' => 'English',
396             # 'code' => 'en'
397             # },
398             # {
399             # 'name' => "Spanish",
400             # 'nativeName' => "Español",
401             # 'code' => 'es'
402             # },
403             # ...
404              
405             =head2 oneword_available
406              
407             Deprecated. Calling this method will emit a warning and return C<undef>.
408              
409             =head1 ERROR HANDLING
410              
411             On HTTP errors or invalid input the convenience methods (C<pos2words>,
412             C<words2pos>) return C<undef>. The verbose methods (C<position_to_words>,
413             C<words_to_position>, C<get_languages>) also return C<undef> on failure.
414              
415             In all cases a warning is emitted via C<warn> with the HTTP status code.
416             You can catch these with C<$SIG{__WARN__}> or L<Test::Warn> in tests.
417              
418             =head1 TESTING
419              
420             The test suite uses pre-recorded API responses. If you suspect something
421             changed in the API you can force the test suite to use live requests with
422             your API key:
423              
424             W3W_API_KEY=<your key> prove -l t/base.t
425              
426             =head1 SEE ALSO
427              
428             L<https://what3words.com/> - what3words website
429              
430             L<https://developer.what3words.com> - API documentation and key registration
431              
432             L<https://developer.what3words.com/public-api/docs> - API v3 reference
433              
434             =head1 AUTHOR
435              
436             mtmail <mtmail-cpan@gmx.net>
437              
438             =head1 COPYRIGHT AND LICENSE
439              
440             This software is copyright (c) 2026 by OpenCage GmbH.
441              
442             This is free software; you can redistribute it and/or modify it under
443             the same terms as the Perl 5 programming language system itself.
444              
445             =cut