File Coverage

blib/lib/Geo/WebService/Elevation/USGS.pm
Criterion Covered Total %
statement 134 172 77.9
branch 41 88 46.5
condition 15 38 39.4
subroutine 28 33 84.8
pod 6 6 100.0
total 224 337 66.4


line stmt bran cond sub pod time code
1             =head1 NAME
2              
3             Geo::WebService::Elevation::USGS - Elevation queries against USGS web services.
4              
5             =head1 SYNOPSIS
6              
7             use Geo::WebService::Elevation::USGS;
8            
9             my $eq = Geo::WebService::Elevation::USGS->new();
10             print "The elevation of the White House is ",
11             $eq->elevation( 38.898748, -77.037684 )->{Elevation},
12             " feet above sea level.\n";
13              
14             =head1 NOTICE
15              
16             Some time while I was not looking the USGS changed the web address and
17             the API for the point elevation service yet again.
18              
19             I have dealt with this by resurrecting the C attribute and
20             defaulting it to a true value. This attribute affects the hash returned
21             by the C method. See that method's documentation
22             for the gory details.
23              
24             You are encouraged to set this attribute to a false value as soon as you
25             can, since my long-term plan is to discourage a true value, and then
26             ultimately deprecate and remove it.
27              
28             =head1 DESCRIPTION
29              
30             This module executes elevation queries against the United States
31             Geological Survey's Elevation Data Point Service. You provide the
32             latitude and longitude in degrees, with south latitude and west
33             longitude being negative. The return is typically a hash containing the
34             data you want. Query errors are exceptions by default, though the object
35             can be configured to signal an error by an undef response, with the
36             error retrievable from the 'error' attribute.
37              
38             For documentation on the underlying web service, see
39             L,
40             particularly L.
41              
42             For all methods, the input latitude and longitude are documented at the
43             above web site as being WGS84, which for practical purposes I understand
44             to be equivalent to NAD83. The vertical reference is not documented
45             under the above link, but correspondence with the USGS says that it is
46             derived from the National Elevation Dataset (NED; see
47             L).
48             This is referred to NAD83 (horizontal) and NAVD88 (vertical). NAVD88 is
49             based on geodetic leveling surveys, B
50             and takes as its zero datum sea level at Father Point/Rimouski, in
51             Quebec, Canada. Alaska is an exception, and is based on NAD27
52             (horizontal) and NAVD29 (vertical).
53              
54             Anyone interested in the gory details may find the paper I
55             GPS Height into NAVD88 Elevation with the GEOID96 Geoid Height Model> by
56             Dennis G. Milbert, Ph.D. and Dru A. Smith, Ph.D helpful. This is
57             available at L. This
58             paper states that the difference between ellipsoid and geoid heights
59             ranges between -75 and +100 meters globally, and between -53 and -8
60             meters in "the conterminous United States."
61              
62             =head2 Methods
63              
64             The following public methods are provided:
65              
66             =cut
67              
68             package Geo::WebService::Elevation::USGS;
69              
70 2     2   624663 use 5.008;
  2         8  
71              
72 2     2   11 use strict;
  2         3  
  2         50  
73 2     2   7 use warnings;
  2         4  
  2         136  
74              
75 2     2   11 use Carp;
  2         4  
  2         171  
76 2     2   1156 use HTTP::Request::Common;
  2         34585  
  2         139  
77 2     2   753 use JSON;
  2         13770  
  2         14  
78 2     2   1588 use LWP::UserAgent;
  2         55601  
  2         104  
79 2     2   17 use Scalar::Util 1.10 qw{ blessed looks_like_number };
  2         42  
  2         149  
80              
81             our $VERSION = '0.201';
82              
83             # use constant USGS_URL => 'https://ned.usgs.gov/epqs/pqs.php';
84             # use constant USGS_URL => 'https://nationalmap.gov/epqs/pqs.php';
85 2     2   11 use constant USGS_URL => 'https://epqs.nationalmap.gov/v1/json';
  2         3  
  2         120  
86              
87 2     2   10 use constant ARRAY_REF => ref [];
  2         4  
  2         103  
88 2     2   9 use constant CODE_REF => ref sub {};
  2         4  
  2         84  
89 2     2   8 use constant HASH_REF => ref {};
  2         4  
  2         114  
90 2     2   11 use constant REGEXP_REF => ref qr{};
  2         3  
  2         5538  
91              
92             my $using_time_hires;
93             {
94             my $mark;
95             if ( eval {
96             require Time::HiRes;
97             Time::HiRes->can( 'time' ) && Time::HiRes->can( 'sleep' );
98             } ) {
99             *_time = \&Time::HiRes::time;
100             *_sleep = \&Time::HiRes::sleep;
101             $using_time_hires = 1;
102             } else {
103             *_time = sub { return time };
104             *_sleep = sub { return sleep $_[0] };
105             }
106              
107             $mark = _time();
108             sub _pause {
109             ## my ( $self ) = @_; # Invocant unused
110 4     4   19 my $now = _time();
111 4         14 while ( $now < $mark ) {
112 0         0 _sleep( $mark - $now );
113 0         0 $now = _time();
114             }
115             # We use __PACKAGE__ rather than $self because the attribute is
116             # static, and it needs to be static because it needs to apply to
117             # everything coming from this user, not just everything coming
118             # from the invoking object.
119 4         30 $mark = $now + __PACKAGE__->get( 'throttle' );
120 4         9 return;
121             }
122             }
123              
124             =head3 $eq = Geo::WebService::Elevation::USGS->new();
125              
126             This method instantiates a query object. If any arguments are given,
127             they are passed to the set() method. The instantiated object is
128             returned.
129              
130             =cut
131              
132             sub new {
133 3     3 1 1400 my ($class, @args) = @_;
134 3 50       10 ref $class and $class = ref $class;
135 3 100       105 $class or croak "No class name specified";
136 2         3 shift;
137             my $self = {
138             carp => 0,
139             compatible => 1,
140             croak => 1,
141             error => undef,
142             places => undef,
143             retry => 0,
144       0     retry_hook => sub {},
145             timeout => 30,
146             trace => undef,
147             units => 'FEET',
148 2   50     23 usgs_url => $ENV{GEO_WEBSERVICE_ELEVATION_USGS_URL} || USGS_URL,
149             };
150 2         5 bless $self, $class;
151 2 100       9 @args and $self->set(@args);
152 2         6 return $self;
153             }
154              
155             my %mutator = (
156             croak => \&_set_literal,
157             carp => \&_set_literal,
158             compatible => \&_set_literal,
159             error => \&_set_literal,
160             places => \&_set_integer_or_undef,
161             retry => \&_set_unsigned_integer,
162             retry_hook => \&_set_hook,
163             throttle => \&_set_throttle,
164             timeout => \&_set_integer_or_undef,
165             trace => \&_set_literal,
166             units => \&_set_literal,
167             usgs_url => \&_set_literal,
168             );
169              
170             my %access_type = (
171             throttle => \&_only_static_attr,
172             );
173              
174             foreach my $name ( keys %mutator ) {
175             exists $access_type{$name}
176             or $access_type{$name} = \&_no_static_attr;
177             }
178              
179             =head3 %values = $eq->attributes();
180              
181             This method returns a list of the names and values of all attributes of
182             the object. If called in scalar context it returns a hash reference.
183              
184             =cut
185              
186             sub attributes {
187 3     3 1 760 my $self = shift;
188 3         4 my %attr;
189 3         13 foreach (keys %mutator) {
190 36         63 $attr{$_} = $self->{$_};
191             }
192 3 100       25 return wantarray ? %attr : \%attr;
193             }
194              
195             =head3 $rslt = $usgs->elevation($lat, $lon, $valid);
196              
197             This method queries the data base for the elevation at the given
198             latitude and longitude, returning the results as a hash reference.
199              
200             If the C attribute is true, this hash will contain the
201             following keys:
202              
203             =over
204              
205             =item {Data_Source} => A text description of the data source (always 'USGS Elevation Point Query Service');
206              
207             =item {Elevation} => The elevation in the given units;
208              
209             =item {Units} => The units of the elevation (C<'Feet'> or C<'Meters'>);
210              
211             =item {x} => The C<$lon> argument;
212              
213             =item {y} => The C<$lat> argument.
214              
215             =back
216              
217             If the C attribute is false, the hash will contain the
218             values documented at L. B
219             that the elevation comes back in key C<{value}>. For my own sanity key
220             C<{Elevation}> is added to this hash; it contains the value of
221             C<{value}>, rounded to C if that attribute is set.
222              
223             You can also pass a C, C, or C
224             object in lieu of the C<$lat> and C<$lon> arguments. If you do this,
225             C<$valid> becomes the second argument, rather than the third.
226              
227             If the optional C<$valid> argument is specified as a true value B
228             the returned data are invalid, nothing is returned. The source does not
229             seem to produce data recognizable as invalid, so you will probably not
230             see this.
231              
232             =cut
233              
234             sub elevation {
235 4     4 1 958078 my ( $self, $lat, $lon, $valid ) = _latlon( @_ );
236 4         27 my $retry_limit = $self->get( 'retry' );
237 4         10 my $retry = 0;
238              
239 4         489 while ( $retry++ <= $retry_limit ) {
240              
241 4         11 $self->{error} = undef;
242              
243 4         18 $self->_pause();
244              
245 4         5 my $rslt;
246             eval {
247             $rslt = $self->_request(
248             x => $lon,
249             y => $lat,
250             units => $self->{units},
251 4         17 );
252 4         25 1;
253 4 50       9 } or do {
254 0         0 $self->_error( $@ );
255 0         0 next;
256             };
257              
258 4 50       16 $rslt
259             or next;
260              
261 4 50 33     104 not $valid
262             or is_valid( $rslt )
263             or next;
264              
265 4         42 return $rslt;
266              
267             } continue {
268              
269 0 0       0 if ( $retry <= $retry_limit ) {
270 0         0 ( my $sub = ( caller( 0 ) )[3] ) =~ s/ .* :: //smx;
271 0         0 $self->get( 'retry_hook' )->( $self, $retry, $sub, $lat,
272             $lon );
273             }
274              
275             }
276              
277 0 0       0 $self->{croak} and croak $self->{error};
278 0         0 return;
279              
280             }
281              
282             =head3 $value = $eq->get($attribute);
283              
284             This method returns the value of the given attribute. It will croak if
285             the attribute does not exist.
286              
287             =cut
288              
289             sub get {
290 37     37 1 4324 my ($self, $name) = @_;
291 37 100       410 $access_type{$name}
292             or croak "No such attribute as '$name'";
293 35         77 my $holder = $access_type{$name}->( $self, $name );
294 35         213 return $holder->{$name};
295             }
296              
297             =head3 $rslt = $eq->getAllElevations($lat, $lon, $valid);
298              
299             This method was removed in version 0.116_01. Please use the
300             C method instead. See the L above for
301             details.
302              
303             =head3 $rslt = $eq->getElevation($lat, $lon, $source, $elevation_only);
304              
305             This method was removed in version 0.116_01. Please use the
306             C method instead. See the L above for
307             details.
308              
309             =cut
310              
311             =head3 $boolean = $eq->is_valid($elevation);
312              
313             This method (which can also be called as a static method or as a
314             subroutine) returns true if the given datum represents a valid
315             elevation, and false otherwise. A valid elevation is a number having a
316             value greater than -1e+300. The input can be either an elevation value
317             or a hash whose {Elevation} key supplies the elevation value.
318              
319             B that as of June 11 2024 I am unable to find any documentation to
320             support this method. Therefore use of this method is discouraged, and it
321             will deprecated and removed when I drop support for the C
322             attribute.
323              
324             =cut
325              
326             sub is_valid {
327 6     6 1 3896 my $ele = pop;
328 6         16 my $ref = ref $ele;
329 6 100       24 if ( HASH_REF eq $ref ) {
    100          
330 1         4 $ele = $ele->{Elevation};
331             } elsif ($ref) {
332 1         139 croak "$ref reference not understood";
333             }
334 5   100     75 return defined( $ele ) && looks_like_number($ele) && $ele > -1e+300;
335             }
336              
337             =head3 $eq = $eq->set($attribute => $value ...);
338              
339             This method sets the value of the given attribute. Multiple
340             attribute/value pairs may be specified. The object itself is returned,
341             to allow call chaining. An attempt to set a non-existent attribute will
342             result in an exception being thrown.
343              
344             =cut
345              
346             {
347              
348             # Changes in these values require re-instantiating the transport
349             # object. Or at least, they may do, under the following assumptions:
350             # HTTP_Post: timeout.
351             my %clean_transport_object = map { $_ => 1 } qw{ timeout };
352              
353             sub set { ## no critic (ProhibitAmbiguousNames)
354 11     11 1 6047 my ($self, @args) = @_;
355 11         19 my $clean;
356 11         30 while (@args) {
357 12         30 my ( $name, $val ) = splice @args, 0, 2;
358 12 100       347 $access_type{$name}
359             or croak "No such attribute as '$name'";
360 10 50       23 exists $mutator{$name}
361             or croak "Attribute '$name' is read-only";
362 10         22 _deprecate( attribute => $name );
363 10         18 my $holder = $access_type{$name}->( $self, $name );
364 10         24 $mutator{$name}->( $holder, $name, $val );
365 9   33     51 $clean ||= $clean_transport_object{$name};
366             }
367 8 50       14 $clean and delete $self->{_transport_object};
368 8         16 return $self;
369             }
370              
371             }
372              
373             sub _set_hook {
374 0     0   0 my ( $self, $name, $val ) = @_;
375 0 0       0 CODE_REF eq ref $val
376             or croak "Attribute $name must be a code reference";
377 0         0 return( $self->{$name} = $val );
378             }
379              
380             sub _set_integer_or_undef {
381 5     5   16 my ($self, $name, $val) = @_;
382 5 100 100     230 (defined $val && $val !~ m/ \A \d+ \z /smx)
383             and croak "Attribute $name must be an unsigned integer or undef";
384 4         13 return ($self->{$name} = $val);
385             }
386              
387             sub _set_literal {
388 5     5   12 return $_[0]{$_[1]} = $_[2];
389             }
390              
391             sub _set_throttle {
392 0     0   0 my ( $self, $name, $val ) = @_;
393 0 0       0 if ( defined $val ) {
394 0 0 0     0 looks_like_number( $val )
395             and $val >= 0
396             or croak "The $name attribute must be undef or a ",
397             'non-negative number';
398 0 0 0     0 $using_time_hires
      0        
399             or $val >= 1
400             or $val == 0
401             or $val = 1;
402             } else {
403 0         0 $val = 0;
404             }
405 0         0 return( $self->{$name} = $val );
406             }
407              
408             sub _set_unsigned_integer {
409 0     0   0 my ($self, $name, $val) = @_;
410 0 0 0     0 ( !defined $val || $val !~ m/ \A \d+ \z /smx )
411             and croak "Attribute $name must be an unsigned integer";
412 0         0 return ($self->{$name} = $val + 0);
413             }
414              
415             ########################################################################
416             #
417             # Private methods
418             #
419             # The author reserves the right to change these without notice.
420              
421             {
422             # NOTE to me: The deprecation of everything but 'compatible' is on
423             # hold until 'compatible' gets to 2. Then everything goes to 3
424             # together.
425             my %dep = (
426             attribute => {
427             dflt => sub { return },
428             item => {
429             compatible => 0,
430             default_ns => 3,
431             proxy => 3,
432             source => 3,
433             use_all_limit => 3,
434             },
435             },
436             subroutine => {
437             dflt => sub {
438             ( my $name = ( caller( 2 ) )[3] ) =~ s/ .* :: //smx;
439             return $name;
440             },
441             item => {
442             getElevation => 3,
443             getAllElevations => 3,
444             },
445             },
446             );
447              
448             sub _deprecate {
449 10     10   51 my ( $group, $item ) = @_;
450 10 50       25 my $info = $dep{$group}
451             or confess "Programming error - Deprecation group '$group' unknown";
452             defined $item
453 10 50 33     26 or defined( $item = $info->{dflt}->() )
454             or croak "Programming error - No item default for group '$group'";
455 10 50       29 $info->{item}{$item}
456             or return;
457 0         0 my $msg = ucfirst "$group $item is deprecated";
458 0 0       0 $info->{item}{$item} > 2
459             and croak "Fatal - $msg";
460 0 0       0 warnings::enabled( 'deprecated' )
461             or return;
462 0         0 carp "Warning - $msg";
463             $info->{item}{$item} == 1
464 0 0       0 and $info->{item}{$item} = 0;
465 0         0 return;
466             }
467             }
468              
469             # $ele->_error($text);
470             #
471             # Set the error attribute, and croak if the croak attribute is
472             # true. If croak is false, just return, carping if the carp
473             # attribute is true.
474              
475             sub _error {
476 0     0   0 my ($self, @args) = @_;
477 0         0 $self->{error} = join '', @args;
478             ## $self->{croak} and croak $self->{error};
479 0 0       0 $self->{croak} and return;
480 0 0       0 $self->{carp} and carp $self->{error};
481 0         0 return;
482             }
483              
484             # _instance( $object, $class )
485             # and print "\$object isa $class\n";
486             #
487             # Return true if $object is an instance of class $class, and false
488             # otherwise. Unlike UNIVERSAL::isa, this is false if the first
489             # object is not a reference.
490              
491             sub _instance {
492 12     12   25 my ( $object, $class ) = @_;
493 12 100       39 blessed( $object ) or return;
494 3         26 return $object->isa( $class );
495             }
496              
497             # my ($self, $lat, $lon, @_) = _latlon(@_);
498             #
499             # Strip the object reference, latitude, and longitude off the
500             # argument list. If the first argument is a Geo::Point,
501             # GPS::Point, or Net::GPSD::Point object the latitude and
502             # longitude come from it. Otherwise the first argument is assumed
503             # to be latitude, and the second to be longitude.
504              
505             {
506              
507             my %known = (
508             'Geo::Point' => sub {$_[0]->latlong('wgs84')},
509             'GPS::Point' => sub {$_[0]->latlon()},
510             'Net::GPSD::Point' => sub {$_[0]->latlon()},
511             );
512              
513             sub _latlon {
514 4     4   15 my ($self, $obj, @args) = @_;
515 4         20 foreach my $class (keys %known) {
516 12 100       56 if (_instance( $obj, $class ) ) {
517 1         6 return ($self, $known{$class}->($obj), @args);
518             }
519             }
520 3         11 return ($self, $obj, @args);
521             }
522             }
523              
524             {
525             my %static = ( # Static attribute values.
526             throttle => 0,
527             );
528              
529             # $self->_no_static_attr( $name );
530             #
531             # Croaks if the invocant is not a reference. The message assumes
532             # the method was called trying to access an attribute, whose name
533             # is $name.
534              
535             sub _no_static_attr {
536 41     41   72 my ( $self, $name ) = @_;
537 41 50       83 ref $self
538             or croak "Attribute $name may not be accessed statically";
539 41         70 return $self;
540             }
541              
542             # $self->_only_static_attr( $name );
543             #
544             # Croaks if the invocant is a reference. The message assumes the
545             # method was called trying to access an attribute, whose name is
546             # $name.
547              
548             sub _only_static_attr {
549 4     4   12 my ( $self, $name ) = @_;
550 4 50       12 ref $self
551             and croak "Attribute $name may only be accessed statically";
552 4         10 return \%static;
553             }
554              
555             }
556              
557             # $rslt = $self->_request( %args );
558             #
559             # This private method requests data from the USGS' web service.
560             # The %args are the arguments for the request:
561             # {x} => longitude (West is negative)
562             # {y} => latitude (South is negative)
563             # {units} => desired units ('Meters' or 'Feet')
564             # The return is a reference to a hash containing the parsed JSON
565             # returned from the NAD server.
566              
567             sub _request {
568 4     4   23 my ( $self, %arg ) = @_;
569              
570             # The allow_nonref() is for the benefit of {_hack_result}.
571 4   66     44 my $json = $self->{_json} ||= JSON->new()->allow_nonref();
572              
573             my $ua = $self->{_transport_object} ||=
574 4   66     21 LWP::UserAgent->new( timeout => $self->{timeout} );
575              
576             defined $arg{units}
577 4 50       248 or $arg{units} = 'Feet';
578 4 100       27 $arg{units} = $arg{units} =~ m/ \A meters \z /smxi
579             ? 'Meters'
580             : 'Feet';
581              
582 4         13 my $uri = URI->new( $self->get( 'usgs_url' ) );
583 4         503 $uri->query_form( \%arg );
584 4         698 my $rqst = HTTP::Request::Common::GET( $uri );
585              
586             $self->{trace}
587 4 50       456 and print STDERR $rqst->as_string();
588              
589 4 50       31 my $rslt = exists $self->{_hack_result} ? do {
590 0         0 my $data = delete $self->{_hack_result};
591 0 0       0 CODE_REF eq ref $data ? $data->( $self, %arg ) : $data;
592             } : $ua->request( $rqst );
593              
594 4 50       10775678 if ( $self->{trace} ) {
595 0 0       0 if ( my $redir = $rslt->request() ) {
596 0         0 print STDERR $redir->as_string();
597             }
598 0         0 print STDERR $rslt->as_string();
599             }
600              
601             $rslt->is_success()
602 4 50       24 or croak $rslt->status_line();
603              
604             {
605 4         53 local $@ = undef;
  4         15  
606 4 50       12 eval {
607 4         34 $rslt = $json->decode( $rslt->decoded_content() );
608 4         1771 ref $rslt;
609             } or return $self->_error( $rslt->decoded_content() );
610             }
611              
612 4 50       27 if ( $self->get( 'compatible' ) ) {
613             $rslt = {
614             x => $rslt->{location}{x},
615             y => $rslt->{location}{y},
616             Data_Source => 'USGS Elevation Point Query Service',
617             Elevation => $rslt->{value},
618             Units => $arg{units},
619 4         47 };
620             } else {
621 0         0 $rslt->{Elevation} = $rslt->{value};
622             }
623              
624             =begin comment
625              
626             foreach my $key (
627             qw{ USGS_Elevation_Point_Query_Service Elevation_Query }
628             ) {
629             HASH_REF eq ref $rslt
630             and exists $rslt->{$key}
631             or return $self->_error(
632             "Elevation result is missing element {$key}" );
633             $rslt = $rslt->{$key};
634             }
635              
636             unless ( ref $rslt ) {
637             $rslt =~ s/ (?
638             return $self->_error( $rslt );
639             }
640              
641             =end comment
642              
643             =cut
644              
645 4         9 my $places;
646             defined $rslt->{Elevation}
647             and defined( $places = $self->get( 'places' ) )
648 4 50 33     28 and $rslt->{Elevation} = sprintf '%.*f', $places, $rslt->{Elevation};
649              
650 4         52 return $rslt;
651             }
652              
653             1;
654              
655             __END__