The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.

NAME

Geo::WebService::Elevation::USGS - Elevation queries against USGS web services.

SYNOPSIS

 use Geo::WebService::Elevation::USGS;
 
 my $eq = Geo::WebService::Elevation::USGS->new();
 print "The elevation of the White House is ",
   $eq->getElevation(38.898748, -77.037684)->{Elevation},
   " feet above sea level.\n";

NOTICE

This module has been converted to use HTTP Post instead of SOAP as a transport, due to the slowness of SOAP::Lite in fixing test errors.

In the transition version, 0.007_01, the 'transport' attribute was made available to select which transport was used. That module also documented that this attribute would be revoked in the next production version unless I got feedback to the contrary.

It is now time for the production release, and I have received no feedback (either asking for the retention of SOAP::Lite or otherwise). So with this release, support for SOAP as a transport is revoked, as is the 'transport' attribute.

DESCRIPTION

This module executes elevation queries against the United States Geological Survey's web server. You provide the latitude and longitude in degrees, with south latitude and west longitude being negative. The return is typically a hash containing the data you want. Query errors are exceptions by default, though the object can be configured to signal an error by an undef response, with the error retrievable from the 'error' attribute.

For documentation on the underlying web service, see http://gisdata.usgs.gov/XMLWebServices/TNM_Elevation_Service.php.

For all methods, the input latitude and longitude are documented at the above web site as being WGS84, which for practical purposes I understand to be equivalent to NAD83. The vertical reference is not documented under the above link, but correspondence with the USGS says that it is derived from the National Elevation Dataset (NED; see http://ned.usgs.gov). This is referred to NAD83 (horizontal) and NAVD88 (vertical). NAVD88 is based on geodetic leveling surveys, not the WGS84/NAD83 ellipsoid, and takes as its zero datum sea level at Father Point/Rimouski, in Quebec, Canada. Alaska is an exception, and is based on NAD27 (horizontal) and NAVD29 (vertical).

Anyone interested in the gory details may find the paper Converting GPS Height into NAVD88 Elevation with the GEOID96 Geoid Height Model by Dennis G. Milbert, Ph.D. and Dru A. Smith, Ph.D helpful. This is available at http://www.ngs.noaa.gov/PUBS_LIB/gislis96.html. This paper states that the difference between ellipsoid and geoid heights ranges between -75 and +100 meters globally, and between -53 and -8 meters in "the conterminous United States."

Caveat: This module relies on the documented behavior of the above-referred-to USGS web service, as well as certain undocumented but observed behaviors and the proper functioning of the USGS' hardware and software and the network connecting you to them. Changes or failures in any of these will probably cause this module not to work.

I have not (I think) gone out of my way to use undocumented behavior, but there are a couple cases where I felt forced into it.

First, the documentation says that if a point is not in a requested source data set, the returned elevation will be -1.79769313486231E+308. In practice, the getAllElevations web service returns 'BAD_EXTENT' in this case, and the getElevation web service returns error 'ERROR: No Elevation value was returned from servers.' This affects the behavior of the is_valid() method, but more importantly it convinced me to try to convert the corresponding getElevation error back into a successful call of the getElevation() method, returning 'BAD_EXTENT' as the elevation.

Second, experimentation shows that, although the source IDs are generally documented as upper-case, in fact the getElevation web service queries are not sensitive to the case of the provided source ID. Because of this, this package normalizes source IDs (by converting them to upper case) before using them in a comparison. This affects the behavior of the elevation() method when the 'source' attribute is an array or hash reference and the source is being used to select results from a getAllElevations query. It also affects the construction of the 'BAD_EXTENT' packet in response to a getElevation web service failure, since the source name is obtained by making a call to getAllElevations() (or the cached results of such a call) and finding the name corresponding to the given source ID.

Methods

The following public methods are provided:

$eq = Geo::WebService::Elevation::USGS->new();

This method instantiates a query object. If any arguments are given, they are passed to the set() method. The instantiated object is returned.

%values = $eq->attributes();

This method returns a list of the names and values of all attributes of the object. If called in scalar context it returns a hash reference.

$rslt = $usgs->elevation($lat, $lon, $valid);

This method queries the data sets defined in the 'source' attribute for the elevation at the given latitude and longitude, returning the results in the given array reference. If called in list context the array itself is returned. The returned array contains hashes identical to that returned by getElevation().

You can also pass a Geo::Point, GPS::Point, or Net::GPSD::Point object in lieu of the $lat and $lon arguments. If you do this, $valid becomes the second argument, rather than the third.

If the 'source' is undef or -1, getElevation() is called to get the 'best' data set representing the given coordinates. The result is an array (or array reference) whose sole element is the hash returned by GetElevation().

If the 'source' is any other scalar, getElevation() is called to get the named data set. The result is an array (or array reference) whose sole element is the hash returned by GetElevation().

If the 'source' is a reference to an empty array or an empty hash, getAllElevations() is called, and its output (or a reference to it) is returned.

If the 'source' is a reference to a non-empty array with at least as many entries as the value of the 'use_all_limit' attribute, and none of the entries begins with a '*' (what the USGS calls 'best available subset' syntax) it is made into a hash by using the contents of the array as keys and 1 as the value for all keys. Then getAllElevations() is called, and the array (or a reference to it) of all source data sets whose Source_ID values appear in the hash are returned. An error will be declared if there are any source IDs specified in the array which are not returned by getAllElevations().

If the 'source' is a reference to a non-empty array and none of the other conditions of the previous paragraph apply, getElevation() is called for each element of the array, and the array of all results (or a reference to it) is returned.

Please note that the use of wildcard source IDs (either the magic '-1' or anything beginning with '*') in an array (or hash, see below) is not supported. Users will find that the current behavior is to error out with an invalid source name if the query is directed to getAllElevations. If the query gets handled by iterating with getElevation(), it succeeds or errors out under the same conditions that the getElevation() method would. But I make no commitment to retain this functionality. Instead, I hope that use of the module will clarify what its behavior should be.

If the 'source' is a reference to a non-empty hash, it is handled pretty much as though the 'source' were a reference to an array containing the sorted keys of the hash.

If the 'source' is a reference to a regular expression, getAllElevations() is called, and items whose Data_ID does not match the regular expression are eliminated from its output. The resultant array (or a reference to it) is returned.

If the 'source' is a reference to code, getAllElevations() is called. For each item in the returned array, the code is called, with the Geo::WebService::Elevation::USGS object as the first argument, and the array item (which, remember, is a hash reference like that returned by getElevation()) as the second argument. If the code returns true the item is included in the output; if the code returns false the item is excluded. The resultant array (or a reference to it) is returned.

For example,

 $ele->set(source => sub {$_[1]{Data_ID} =~ m/CONUS/i});

will retain all items whose Data_ID contains the string 'CONUS', and therefore has the same result as

 $ele->set(source => qr{CONUS}i);

If the optional $valid argument to elevation() is specified, data with invalid elevations are eliminated before the array is returned. Note that this may result in an empty array.

$value = $eq->get($attribute);

This method returns the value of the given attribute. It will croak if the attribute does not exist.

$rslt = $eq->getAllElevations($lat, $lon, $valid);

This method executes the getAllElevations query, which returns the elevation of the given point as recorded in all available data sets. The results are returned as a reference to an array containing hashes representing the individual data sets. Each data set hash is structured like the one returned by getElevation(). If a failure occurs, the method will croak if the 'croak' attribute is true, or return undef otherwise. The arguments are WGS84 latitude and longitude, in degrees, with south latitude and west longitude being negative. The elevations returned are NAVD88 elevations.

You can also pass a Geo::Point, GPS::Point, or Net::GPSD::Point object in lieu of the $lat and $lon arguments.

If the elevation is not available from a given source, that source will still appear in the output, but the Elevation will either be a non-numeric value (e.g. 'BAD_EXTENT', though this is nowhere documented that I can find), or a very large negative number (documented as -1.79769313486231E+308).

If the optional $valid argument to getAllElevations() is specified, data with invalid elevations are eliminated before the array is returned. Note that this may result in an empty array.

$rslt = $eq->getElevation($lat, $lon, $source, $elevation_only);

This method executes the getElevation query, requesting elevation for the given WGS84 latitude and longitude (in degrees, with south latitude and west longitude being negative) from the source data set. The returned elevation is NAVD88. If a failure occurs, the method will croak if the 'croak' attribute is true, or return undef otherwise. Either way, the error if any will be available in the 'error' attribute.

You can also pass a Geo::Point, GPS::Point, or Net::GPSD::Point object in lieu of the $lat and $lon arguments. If you do this, $source becomes the second argument, rather than the third, and $elevation_only becomes the third argument rather than the fourth.

If the $source argument is omitted, undef, or -1, data comes from the 'best' data set for the given position. If the $source argument begins with an asterisk ('*') you get data from the 'best' data set whose name matches the given name. In either case, you get an error (not success with an invalid {Elevation}) if the given position is not covered by any of the selected data sets.

The $elevation_only argument is optional. If provided and true (in the Perl sense) it causes the return on success to be the numeric value of the elevation, rather than the hash reference described below.

Assuming success and an unspecified or false $elevation_only, $rslt will be a reference to a hash containing the data. The USGS documents the following keys:

 Data_Source: name or description of data source
 Data_ID: string identifier of data source
 Elevation: the elevation of the point
 Units: the units of the Elevation

The contents of $rslt will then be something like:

 {
     Data_Source => 'source name'
     Data_ID => 'source ID',
     Elevation => elevation from given source,
     Units => 'units from source',
 }

If the elevation is not available, the Elevation will either be a non-numeric value (e.g. 'BAD_EXTENT', though this is nowhere documented that I can find), or a very large negative number (documented as -1.79769313486231E+308).

Caveat: The USGS web service upon which this code is based throws an error ('ERROR: No Elevation value was returned from servers.') if the requested latitude and longitude do not appear in the specified data set(s). If a specific data source name was specified, this code attempts to trap this error and return a hash with Elevation => 'BAD_EXTENT', the way getAllElevations does, in order to get more desirable behavior from the elevation() method. If the behavior of the USGS web service changes, the change may be visible to users of this software, either as an error, as an unexpected value in the 'Elevation' key, or some other way.

Another caveat: If you have not specified a data source (or if you have specified a 'wildcard' data source such as '-1' or anything beginning with '*'), and no data source covers the point you have specified, an error will occur. This will be thrown as an exception if the 'carp' attribute is true; otherwise undef will be returned and the error will be in the 'error' attribute. This seems inconsistent with the behavior of the previous caveat, but it is also unclear what to do about it.

$boolean = $eq->is_valid($elevation);

This method (which can also be called as a static method or as a subroutine) returns true if the given datum represents a valid elevation, and false otherwise. A valid elevation is a number having a value greater than -1e+300. The input can be either an elevation value or a hash whose {Elevation} key supplies the elevation value.

$eq = $eq->set($attribute => $value ...);

This method sets the value of the given attribute. Multiple attribute/value pairs may be specified. The object itself is returned, to allow call chaining. An attempt to set a non-existent attribute will result in an exception being thrown.

Attributes

carp (boolean)

This boolean attribute determines whether the data acquisition methods carp on encountering an error. If false, they silently return undef. Note, though, that the croak attribute trumps this one.

If retry is set to a number greater than 0, you will get a carp on each failed query, provided croak is false. If croak is true, no retries will be carped.

This attribute was introduced in Geo::WebService::Elevation::USGS version 0.005_01.

The default is 0 (i.e. false).

croak (boolean)

This attribute determines whether the data acquisition methods croak on encountering an error. If false, they return undef on an error.

If retry is set to a number greater than 0, the data acquisition method will not croak until all retries are exhausted.

The default is 1 (i.e. true).

default_ns (string)

This attribute records the XML namespace used by the SOAP query. This must agree with the targetNamespace value given in the USGS' WSDL found at http://gisdata.usgs.gov/XMLWebServices/TNM_Elevation_service.asmx?WSDL.

This attribute should not ordinarily need to be modified, but the desperate user may be able to use it to get him- or herself going again if the USGS changes the WSDL and this module has not been modified to track the change.

The default is 'http://gisdata.usgs.gov/XMLWebServices2/'.

error (string)

This attribute records the error returned by the last query operation, or undef if no error occurred. This attribute can be set by the user, but will be reset by any query operation.

The default (before any queries have occurred) is undef.

places (integer)

If this attribute is set to a non-negative integer, elevation results will be rounded to this number of decimal places by running them through sprintf "%.${places}f".

The default is undef.

proxy (string)

This attribute specifies the actual url to which the SOAP query is posted. It must agree with the soap:address location value given for wsdl:port name "Elevation_ServiceSoap" given in the USGS' WSDL found at http://gisdata.usgs.gov/XMLWebServices/TNM_Elevation_service.asmx?WSDL.

This attribute should not ordinarily need to be modified, but the desperate user may be able to use it to get him- or herself going again if the USGS changes the WSDL and this module has not been modified to track the change.

The default is 'http://gisdata.usgs.gov/XMLWebServices2/Elevation_Service.asmx'.

retry (unsigned integer)

This attribute specifies the number of retries to be done by getAllElevations() and getElevation() when an error is encountered. The first try is not considered a retry, so if you set this to 1 you get a maximum of two queries (the try and the retry).

Retries are done only on actual errors, not on bad extents. They are also subject to the "throttle" setting if any.

The default is 0, i.e. no retries.

retry_hook (code reference)

This attribute specifies a piece of code to be called before retrying. The code will be called before a retry takes place, and will be passed the Geo::WebService::Elevation::USGS object, the number of the retry (from 1), the name of the method being retried ('getAllElevations' or 'getElevation'), and the arguments to that method. If the position was passed as an object, the hook gets the latitude and longitude unpacked from the object. The hook will not be called before the first try, nor after the last retry.

Examples:

 # To sleep 5 seconds between retries:
 $eq->set( retry_hook => sub { sleep 5 } );
 
 # To sleep 1 second before the first retry, 2 seconds
 # before the second, and so on:
 $eq->set( retry_hook => sub { sleep $_[1] } );
 
 # To do nothing between retries:
 $eq->set( retry_hook => sub {} );

The default is the null subroutine, i.e. sub {}.

source

This attribute specifies the ID of the source layer to be queried by the elevation() method. Valid layer IDs are documented at http://gisdata.usgs.gov/XMLWebServices/TNM_Elevation_Service_Methods.php.

A legal value is a scalar, or an ARRAY, CODE, HASH, or Regexp reference. Please see the elevation() method's documentation for how these are used.

The default is '-1', which requests a response from the 'best' data source for the given point.

throttle (non-negative number, or undef)

 Geo::WebService::Elevation::USGS->set( throttle => 5 );

This attribute, if defined and positive, specifies the minimum interval between queries, in seconds. This attribute may be set statically only, and the limit applies to all queries, not just the ones from a given object. If Time::HiRes can be loaded, then sub-second intervals are supported, otherwise not.

This functionality, and its implementation, are experimental, and may be changed or retracted without notice. Heck, I may even go back to $TARGET, though I don't think so.

timeout (integer, or undef)

This attribute specifies the timeout for the SOAP query in seconds.

The default is 30.

trace (boolean)

If true, this attribute requests a SOAP::Lite trace of any queries made. This should only be used for troubleshooting, and the author makes no representation about and has no control over what output you get if you set this true.

The default is undef (i.e. false).

units (string)

This attribute specifies the desired units for the resultant elevations. The USGS documents 'FEET' and 'METERS' as valid values. Experimentation shows that undocumented values (e.g. 'CUBITS') return results in feet, which is documented as the default.

The default is 'FEET'.

use_all_limit (integer)

This attribute is used to optimize the behavior of the elevation() method when the 'source' attribute is an array or hash reference. If the number of elements in the array or hash is greater than or equal to this, elevation() gets its data by calling getAllElevations() and then dropping unwanted data. If the number of elements is less than this number, elevation() iterates over the elements of the array or the sorted keys of the hash, calling getElevation() on each.

Note that setting this to 0 causes getAllElevations() to be used always. Setting this to -1 (or any negative number) is special-cased to cause getElevation() to be used whenever the 'source' array or hash has any entries at all, no matter how many it has.

The default is 5, which was chosen based on timings of the two methods.

Globals

$Geo::WebService::Elevation:USGS::THROTTLE

This interface is deprecated; use the "throttle" static method instead. It will be revoked in release 0.006, and at that time setting $THROTTLE will become a fatal error. The short deprecation cycle is because this interface has never been released in a production release, and because you were warned when it was introduced. Until the next production release, the next query after setting $THROTTLE will cause a warning message to be issued, and its value to be passed to "throttle".

If set to a positive number, this symbol limits the rate at which queries can be issued. The value represents the minimum interval between queries (in seconds), which is enforced by having the query methods sleep() as needed. This minimum is enforced among objects, not just within a single object.

If Time::HiRes can be loaded, its time() and sleep() will be used, and fractional minimum intervals can be specified. If it can not be loaded, the core time() and sleep() will be used. With the core functions, the resolution is a second, and a $THROTTLE value between 0 and 1 will be taken to be 1.

The behavior of this functionality when $THROTTLE is set to a non-numeric value is undefined. In practice, you will probably get an error of some sort when you issue the query, but the author makes no commitments. Same for quasi-numbers such as Inf and NaN.

The default is undef, which means that this functionality is disabled.

ACKNOWLEDGMENTS

The author wishes to acknowledge the following individuals and groups.

The members of the geo-perl mailing list provided valuable suggestions and feedback, and generally helped me thrash through such issues as how the module should work and what it should actually be called.

Michael R. Davis provided prompt and helpful feedback on a testing problem in my first module to rely heavily on Test::More.

BUGS

Bugs can be reported to the author by mail, or through http://rt.cpan.org/.

SEE ALSO

AUTHOR

Thomas R. Wyant, III; wyant at cpan dot org

COPYRIGHT AND LICENSE

Copyright (C) 2008-2010, Thomas R. Wyant, III

This program is free software; you can redistribute it and/or modify it under the same terms as Perl 5.10.0. For more details, see the full text of the licenses in the directory LICENSES.

This program is distributed in the hope that it will be useful, but without any warranty; without even the implied warranty of merchantability or fitness for a particular purpose.