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";

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.

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 an error on the attempt to convert 'BAD_EXTENT' to a number. This affects the behavior of the is_valid() method, but more importantly it convinced me to try to convert the getElevation web service 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(). The USGS code handling this request seems to be badly behaved if the requested data set does not cover the given position, but this package attempts to deal with this by turning their error into a successful return with elevation 'BAD_EXTENT'.

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);

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.

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).

Caveat: The USGS includes source 'SRTM.C_US_1' twice in the data provided to this method. They are not duplicates because for latitude 40 longitude -90 one returns an elevation (600.39...) and the other returns BAD_EXTENT.

$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. 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.

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 a type conversion error ("... System.InvalidCastException: Conversion from string "BAD_EXTENT" to type 'Double' is not valid. ...") if an explicit source was requested and the latitude and longitude are not in that source. This code attempts to trap this error and return a hash with Elevation => 'BAD_EXTENT', the way getAllElevations does, on the presumption that this is what the USGS code was trying to do. 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

croak (boolean)

This attribute determines whether the data acquisition methods croak on encountering an error.

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'.

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.

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.

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

Copyright 2008 by Thomas R. Wyant, III. All rights reserved.

LICENSE

This module is free software; you can use it, redistribute it and/or modify it under the same terms as Perl itself. Please see http://perldoc.perl.org/index-licence.html for the current licenses.