#!/usr/bin/perl -w


#
# Coordinate.pm:  Perl module for:
#
# 1) Creating and manipulating Coordinate objects,
# 2) Translating coordinates between UTM and Latitude/Longitude,
# 3) Translating coordinates between ~231 different datums,
# 4) Formatting coordinates into decimal degrees, degrees/minutes,
#    and degrees/minutes/seconds.
#
# Copyright (C) 2000-2003  Curt Mills, WE7U
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# 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.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
#


#
# $Id: Coordinate.pm,v 1.4 2003/01/06 22:42:15 we7u Exp $
#
#
# Reference ellipsoids derived from Peter H. Dana's website- 
# New:  http://www.Colorado.EDU/geography/gcraft/notes/datum/elist.html
#
# Old:  http://www.utexas.edu/depts/grg/gcraft/notes/datum/elist.html
# Department of Geography, University of Texas at Austin
# Internet: pdana@mail.utexas.edu
# 3/22/95
#
#
# Reference datums derived from Peter H. Dana's website-
# http://www.Colorado.EDU/geography/gcraft/notes/datum/datum.html#GeoDat
#
#
# UTM<->LAT/LON translations were originally written in C++ by
# Chuck Gantz- chuck.gantz@globalstar.com.  Permission received
# by Chuck Gantz via e-mail to release it under the GPL license.
# Re-coded into Perl by Curt Mills.
#
# Datum translations were originally written in C by
# John F. Waers - jfwaers@csn.net.  His code was released to
# the public domain.  Re-coded into Perl by Curt Mills.
#
#
# TODO:
# -----
# Please note that I didn't pay a lot of attention to
# keeping the "double" notation in the form of higher
# precision floating point routines.  This means that
# the Perl5 code won't be as accurate as the original
# C-code.  It doesn't matter for my purposes.  If
# anyone converts to Math::BigFloat for higher precision,
# please send me the changes.  As it is I did a
# quick check and found a difference of only 1.4 meters
# between my Perl results and the results from a web-based
# datum-shift calculator on the 'net.  -- Curt.
#
# Should probably add return values for each method.  Check
# for error conditions and return 0 if problem.
#
# Add method to tell distance in meters of datum shift (verbose flag?)
#
# Get rid of print statements unless verbose or debug mode.
#
# POD documentation.
#


#------------------------------------------------------------------------------------------------


package CoordinateFormat;


$VERSION = do { my @r=(q$Revision: 1.4 $=~/\d+/g); sprintf "%d."."%02d"x$#r,@r };


#
# Conversions between decimal degrees, degrees/decimal minutes, and
# degrees/minutes/seconds.  These are methods which all work on
# "CoordinateFormat" objects.
#
# These routines probably need a bit of work yet.  It'd be nice
# to be able to specify field widths.
#


sub new
{
  my $class = shift;			# What class are we constructing?
  my $self      = { };			# Allocate new memory
  bless $self, $class;			# Mark it of the right type
  $self->_init(@_);			# Call _init with remaining args
  return $self;
}


sub _init
{
  my $self = shift;
  $self->{RAW} = shift if @_;		# Input string
  $self->{FORMATTED} = shift if @_;	# Output string, formatted
}


sub raw					# Set'er/Get'er for input string
{
  my $self = shift;
  $self->{RAW} = shift if @_;
  return $self->{RAW};
}


sub formatted				# Set'er/Get'er for formatted string
{
  my $self = shift;
  $self->{FORMATTED} = shift if @_;
  return $self->{FORMATTED};
}


#
# Returns the version number (RCS version tag) of this module.
#
sub version
{
  return $VERSION;
}





#
# Degrees and Decimal Minutes
#
# Fills in the FORMATTED variable in the object and also returns
# the string.  If unsuccessful, returns an empty string.
#
sub degrees_minutes
{
  my $self = shift;				# Get the object to work on
  my $input = $self->raw();

  my ($degrees, $minutes, $seconds);

  if ( $input =~ /\d+\s+\d+\s+\d+\.*\d*/ )	# Input is in "DD MM SS.SS" format
  {
    $degrees = $minutes = $seconds = $input;
    $degrees =~ s/(\d+)\s+\d+\s+\d+\.*\d*/$1/;
    $minutes =~ s/\d+\s+(\d+)\s+\d+\.*\d*/$1/;
    $seconds =~ s/\d+\s+\d+\s+(\d+\.*\d*)/$1/;
    $minutes = $minutes + ($seconds / 60.0);
    $self->formatted( sprintf( "%02d %02.8f", $degrees, abs($minutes) ) );
    return( $self->formatted() );
  }
  elsif ( $input =~ /\d+\s+\d+\.*\d*/ )		# Input is in "DD MM.MM" format
  {
    $self->formatted( $input );			# No translation necessary
    return( $self->formatted() );
  }
  elsif ( $input =~ /\d+\.*\d*/ )		# Input is in "DD.DDDD" format
  {
    $degrees = int ($input);
    $minutes = ($input - $degrees) * 60.0;
    $self->formatted( sprintf( "%02d %02.8f", $degrees, abs($minutes) ) );
    return( $self->formatted() );
  }
  else
  {
    print "degrees_minutes() method: Input format not recognized: $input\n";
    $self->formatted( "" );
    return( "" );				# Should I die here instead?
  }
}





#
# Degrees/Minutes/Seconds
#
# Fills in the FORMATTED variable in the object and also returns
# the string.  If unsuccessful, returns an empty string.
#
sub degrees_minutes_seconds
{
  my $self = shift;				# Get the object to work on
  my $input = $self->raw();

  my ($degrees, $minutes, $seconds);

  if ( $input =~ /\d+\s+\d+\s+\d+\.*\d*/ )	# Input is in "DD MM SS.SS" format
  {
    $self->formatted( $input );			# No translation necessary
    return( $self->formatted() );
  }
  elsif ( $input =~ /\d+\s+\d+\.*\d*/ )		# Input is in "DD MM.MM" format
  {
    $degrees = $minutes = $input;
    $degrees =~ s/(\d+)\s+\d+\.*\d*/$1/;
    $minutes =~ s/\d+\s+(\d+\.*\d*)/$1/;
    $seconds = $minutes;
    $minutes = int ($minutes);
    $seconds = ($seconds - $minutes) * 60.0;
    $self->formatted( sprintf( "%02d %02d %02.8f", $degrees, abs($minutes), abs($seconds) ) );
    return( $self->formatted() );
  }
  elsif ( $input =~ /\d+\.*\d*/ )		# Input is in "DD.DDDD" format
  {
    $degrees = int ($input);
    $minutes = int ( ($input - $degrees) * 60.0 );
    $seconds = ( ( ($input - $degrees) * 60.0) - $minutes) * 60.0;
    $self->formatted( sprintf( "%02d %02d %02.8f", $degrees, abs($minutes), abs($seconds) ) );
    return( $self->formatted() );
  }
  else
  {
    print "degrees_minutes_seconds() method: Input format not recognized: $input\n";
    $self->formatted( "" );
    return( "" );				# Should I die here instead?
  }
}





#
# Decimal Degrees
#
# Fills in the FORMATTED variable in the object and also returns
# the string.  If unsuccessful, returns an empty string.
#
sub decimal_degrees
{
  my $self = shift;				# Get the object to work on
  my $input = $self->raw();

  my ($degrees, $minutes, $seconds);

  if ( $input =~ /\d+\s+\d+\s+\d+\.*\d*/ )	# Input is in "DD MM SS.SS" format
  {
    $degrees = $minutes = $seconds = $input;
    $degrees =~ s/(\d+)\s+\d+\s+\d+\.*\d*/$1/;
    $minutes =~ s/\d+\s+(\d+)\s+\d+\.*\d*/$1/;
    $seconds =~ s/\d+\s+\d+\s+(\d+\.*\d*)/$1/;
    $self->formatted( sprintf( "%02.8f", $degrees + ($minutes/60.0) + ($seconds/(60.0 * 60.0) ) ) );
    return( $self->formatted() );
  }
  elsif ( $input =~ /\d+\s+\d+\.*\d*/ )		# Input is in "DD MM.MM" format
  {
    $degrees = $minutes = $input;
    $degrees =~ s/(\d+)\s+\d+\.*\d*/$1/;
    $minutes =~ s/\d+\s+(\d+\.*\d*)/$1/;
    $self->formatted( sprintf( "%02.8f", $degrees + ($minutes / 60.0) ) );
    return( $self->formatted() );
  }
  elsif ( $input =~ /\d+\.*\d*/ )		# Input is in "DD.DDDD" format
  {
    $self->formatted( $input );			# No translation necessary
    return( $self->formatted() );
  }
  else
  {
    print "decimal_degrees() method: Input format not recognized: $input\n";
    $self->formatted( "" );
    return( "" );				# Should I die here instead?
  }
}


#------------------------------------------------------------------------------------------------


package Ellipsoid;


$VERSION = do { my @r=(q$Revision: 1.4 $=~/\d+/g); sprintf "%d."."%02d"x$#r,@r };


#
# Ellipsoid: Equatorial_Radius is the ellipsoid semimajor axis (a)
#	     Polar_Radius is the ellipsoid semiminor axis (b).
#	     First_Ecc_Squared is the first eccentricity squared, defined as:  2f - (f^2)
#            Inverse_flattening (1/f) is the inverse of ellipsoid flattening 'f'
#	     Flattening (f) is defined as:  (a-b)/a.
#
# This object is used to create the Ellipsoid_Table object below,
# containing all of the parameters to describe the various
# ellipsoids that we need for datum and UTM<->Lat/Lon translations.
#
# Equatorial_Radius and First_Ecc_Squared are used by the UTM<->Lat/Lon code.
# Equatorial_Radius and Inverse_flattening are used by the datum_shift code.
#


sub new
{
  my $class = shift;		# What class are we constructing?
  my $self      = { };		# Allocate new memory
  bless $self, $class;		# Mark it of the right type
  $self->_init(@_);		# Call _init with remaining args
  return $self;
}


#
# Notice that we compute first_ecc_squared, flattening, and polar_radius
# from the other parameters.
#
sub _init
{
  my $self = shift;
  my $f;
  my $b;
  my $inverse_f;

  $self->{NAME} = shift if @_;					# Name of ellipsoid
  $a = $self->{EQUATORIAL_RADIUS} = shift if @_;		# Semi-major axis, meters
  $inverse_f = $self->{INVERSE_FLATTENING} = shift if @_;	# Inverse of the ellipsoid flattening 'f'

  if ($inverse_f)
  {
    $f = 1.0 / $inverse_f;
    $self->{FLATTENING} = $f;					# Store the flattening value as well
    $self->{FIRST_ECC_SQUARED}  = (2 * $f) - ($f**2);		# Compute First Eccentricity Squared from 'f'

    if ($a)
    {
      # f = (a-b)/a
      # f = a/a - b/a
      # f = 1 - b/a
      # b/a + f = 1
      # b/a = 1 - f
      # b = (1-f)a
      #
      $b = (1.0 - $f) * $a;
      $self->{POLAR_RADIUS} = $b;
    }
  }
}


#
# Returns the version number (RCS version tag) of this module.
#
sub version
{
  return $VERSION;
}


sub name
{
  my $self = shift;
  $self->{NAME} = shift if @_;
  return $self->{NAME};
}


sub equatorial_radius
{
  my $self = shift;
  $self->{EQUATORIAL_RADIUS} = shift if @_;
  return $self->{EQUATORIAL_RADIUS};
}


#
# Note that you can override the computed POLAR_RADIUS value
# by using this method to store a new value in the hash.
#
sub polar_radius
{
  my $self = shift;
  $self->{POLAR_RADIUS} = shift if @_;
  return $self->{POLAR_RADIUS};
}


#
# Note that you can override the computed FIRST_ECC_SQUARED value
# by using this method to store a new value in the hash.
#
sub first_ecc_squared
{
  my $self = shift;
  $self->{FIRST_ECC_SQUARED} = shift if @_;
  return $self->{FIRST_ECC_SQUARED};
}


#
# Note that you can override the computed FLATTENING value
# by using this method to store a new value in the hash.
#
sub flattening
{
  my $self = shift;
  $self->{FLATTENING} = shift if @_;
  return $self->{FLATTENING};
}


sub inverse_flattening
{
  my $self = shift;
  $self->{INVERSE_FLATTENING} = shift if @_;
  return $self->{INVERSE_FLATTENING};
}


#-----------------------------------------------------------------------------------------------


package EllipsoidTable;


$VERSION = do { my @r=(q$Revision: 1.4 $=~/\d+/g); sprintf "%d."."%02d"x$#r,@r };


#
# I could have created this table as a hash of anonymous arrays, which might have been
# faster/more efficient.  Oh well.
#
# All of the methods here are class methods.  There isn't even a way to make an
# EllipsoidTable object given here.  This class is merely a large collection
# of Ellipsoid objects, collected in the %_ellipsoid hash.
#


my %_ellipsoid;		# Class data


#
# Returns the version number (RCS version tag) of this module.
#
sub version
{
  return $VERSION;
}


# Note that this method is really useless.  The name is the same as the
# hash key used in the %_ellipsoid hash.  Redundant.
#
sub name
{
  shift;	# Object reference (don't need it)
  my $ellipsoid_name = shift;

  return $_ellipsoid{$ellipsoid_name}->name();
}


sub equatorial_radius	# (a)
{
  shift;	# Object reference (don't need it)
  my $ellipsoid_name = shift;

  return $_ellipsoid{$ellipsoid_name}->equatorial_radius();
}


sub polar_radius	# (b)
{
  shift;	# Object reference (don't need it)
  my $ellipsoid_name = shift;

  return $_ellipsoid{$ellipsoid_name}->polar_radius();
}


sub first_ecc_squared	# (ecc)
{
  shift;	# Object reference (don't need it)
  my $ellipsoid_name = shift;

  return $_ellipsoid{$ellipsoid_name}->first_ecc_squared();
}


sub flattening		# (f)
{
  shift;	# Object reference (don't need it)
  my $ellipsoid_name = shift;

  return $_ellipsoid{$ellipsoid_name}->flattening();
}


sub inverse_flattening	# (1/f)
{
  shift;	# Object reference (don't need it)
  my $ellipsoid_name = shift;

  return $_ellipsoid{$ellipsoid_name}->inverse_flattening();
}


#
# This method allows printing out each defined Ellipsoid.
#
sub enumerate
{
  my $self = shift;

  print "\nEllipsoid\t\tEquatorial Radius (a)\tPolar Radius (b)\tFirst Eccentricity^2\tFlattening (f)\t\t1/f\n";
  print   "---------\t\t---------------------\t----------------\t--------------------\t--------------\t\t---\n";

  foreach my $key (sort keys %_ellipsoid)
  {
    printf("%23s,\t%s,\t%s,\t%s,\t%s,\t%s\n",
	$key,
	$self->equatorial_radius($key),
	$self->polar_radius($key),
	$self->first_ecc_squared($key),
	$self->flattening($key),
	$self->inverse_flattening($key) );
  }
  return(1);
}





#
# Auto-run code
#
# Fill in the hash containing Ellipsoid objects (fill in the Class data)
#
# Name, Equatorial_Radius (a), Inverse_Flattening (1/f).
#
# Name:				Reference Ellipsoid Name
# Equatorial_Radius:		(a) = Semi-Major Axis		(WGS-84 value = 6378137.0 meters)
# Polar_radius:			(b) = Semi-Minor Axis		(WGS-84 value = 6356752.3142 meters)
# Flattening:			f = (a-b)/a			(WGS-84 value = 0.00335281066475)
# First_Eccentricity_Squared:	ecc = 2f - (f^2)		(WGS-84 value = 0.00669437999013)
# Inverse_flattening:		Reciprocal Flattening (1/f)	(WGS-84 value = 298.257223563)
#
# We can calculate Polar_Radius (b), Flattening (f), and First_Eccentricity_Squared (ecc)
# from the values for Equatorial_Radius (a) and Inverse_flattening (1/f), so the table
# includes only the latter two parameters.
#


#print "Creating ellipsoid data\n";
#                                                                                   Equatorial
#            Name                                          Name                     Radius       Inverse_flattening
$_ellipsoid{"Airy"}			= Ellipsoid->new( "Airy",		    6377563.396, 299.324964600 );
$_ellipsoid{"Modified Airy"}          	= Ellipsoid->new( "Modified Airy",	    6377340.189, 299.324964600 );
$_ellipsoid{"Australian National"}	= Ellipsoid->new( "Australian National",    6378160.000, 298.250000000 );
$_ellipsoid{"Bessel 1841"}		= Ellipsoid->new( "Bessel 1841",	    6377397.155, 299.152812800 );
$_ellipsoid{"Bessel 1841 (Namibia)"}	= Ellipsoid->new( "Bessel 1841 (Namibia)",  6377483.865, 299.152812800 );
$_ellipsoid{"Clarke 1866"}		= Ellipsoid->new( "Clarke 1866",	    6378206.400, 294.978698200 );
$_ellipsoid{"Clarke 1880"}		= Ellipsoid->new( "Clarke 1880",	    6378249.145, 293.465000000 );
$_ellipsoid{"Everest (India 1830)"}	= Ellipsoid->new( "Everest (India 1830)",   6377276.345, 300.801700000 );
$_ellipsoid{"Everest (Sabah Sarawak)"}	= Ellipsoid->new( "Everest (Sabah Sarawak)",6377298.556, 300.801700000 ); 
$_ellipsoid{"Everest (India 1956)"}	= Ellipsoid->new( "Everest (India 1956)",   6377301.243, 300.801700000 );
$_ellipsoid{"Everest (Malaysia 1969)"}	= Ellipsoid->new( "Everest (Malaysia 1969)",6377295.664, 300.801700000 );
$_ellipsoid{"Everest (Malay. & Sing)"}	= Ellipsoid->new( "Everest (Malay. & Sing)",6377304.063, 300.801700000 );
$_ellipsoid{"Everest 1948"}		= Ellipsoid->new( "Everest 1948",           6377304.063, 300.801700000 );
$_ellipsoid{"Everest (Pakistan)"}	= Ellipsoid->new( "Everest (Pakistan)",	    6377309.613, 300.801700000 );
$_ellipsoid{"Fischer 1960 (Mercury)"}	= Ellipsoid->new( "Fischer 1960 (Mercury)", 6378166.000, 298.300000000 );
$_ellipsoid{"Fischer 1968"}		= Ellipsoid->new( "Fischer 1968",	    6378150.000, 298.300000000 );
$_ellipsoid{"Modified Fischer 1960"}	= Ellipsoid->new( "Modified Fischer 1960",  6378155.000, 298.300000000 );
$_ellipsoid{"GRS 1967"}			= Ellipsoid->new( "GRS 1967",		    6378160.000, 298.247167427 );
$_ellipsoid{"GRS 1980"}			= Ellipsoid->new( "GRS 1980",		    6378137.000, 298.257222101 );
$_ellipsoid{"Helmert 1906"}		= Ellipsoid->new( "Helmert 1906",	    6378200.000, 298.300000000 );
$_ellipsoid{"Hough 1960"}		= Ellipsoid->new( "Hough 1960",		    6378270.000, 297.000000000 );
$_ellipsoid{"Indonesian 1974"}		= Ellipsoid->new( "Indonesian 1974",	    6378160.000, 298.247000000 );
$_ellipsoid{"International 1924"}	= Ellipsoid->new( "International 1924",	    6378388.000, 297.000000000 );
$_ellipsoid{"Krassovsky 1940"}		= Ellipsoid->new( "Krassovsky 1940",	    6378245.000, 298.300000000 );
$_ellipsoid{"South American 1969"}	= Ellipsoid->new( "South American 1969",    6378160.000, 298.250000000 );
$_ellipsoid{"WGS 60"}			= Ellipsoid->new( "WGS 60",		    6378165.000, 298.300000000 );
$_ellipsoid{"WGS 66"}			= Ellipsoid->new( "WGS 66",		    6378145.000, 298.250000000 );
$_ellipsoid{"WGS 72"}			= Ellipsoid->new( "WGS 72",		    6378135.000, 298.260000000 );
$_ellipsoid{"WGS 84"}			= Ellipsoid->new( "WGS 84",		    6378137.000, 298.257223563 );


#------------------------------------------------------------------------------------------------


package Datum;


$VERSION = do { my @r=(q$Revision: 1.4 $=~/\d+/g); sprintf "%d."."%02d"x$#r,@r };


#
# These are the objects and methods used to create the DatumTable in the next package.
#


sub new
{
  my $class = shift;		# What class are we constructing?
  my $self      = { };		# Allocate new memory
  bless $self, $class;		# Mark it of the right type
  $self->_init(@_);		# Call _init with remaining args
  return $self;
}


sub _init
{
  my $self = shift;
  $self->{NAME} = shift if @_;
  $self->{ELLIPSOID} = shift if @_;
  $self->{DX} = shift if @_;
  $self->{DY} = shift if @_;
  $self->{DZ} = shift if @_;
}


#
# Returns the version number (RCS version tag) of this module.
#
sub version
{
  return $VERSION;
}


#
# This is mostly a useless method.  We store the name here, but it
# is also present as the hash key in the %_datum hash.  Redundant.
#
sub name			# Name of datum
{
  my $self = shift;
  $self->{NAME} = shift if @_;
  return $self->{NAME};
}


sub ellipsoid			# Name of ellipsoid used in datum
{
  my $self = shift;
  $self->{ELLIPSOID} = shift if @_;
  return $self->{ELLIPSOID};
}


sub dx				# X-offset from WGS84 ellipsoid center
{
  my $self = shift;
  $self->{DX} = shift if @_;
  return $self->{DX};
}


sub dy				# Y-offset from WGS84 ellipsoid center
{
  my $self = shift;
  $self->{DY} = shift if @_;
  return $self->{DY};
}


sub dz				# Z-offset from WGS84 ellipsoid center
{
  my $self = shift;
  $self->{DZ} = shift if @_;
  return $self->{DZ};
}


#------------------------------------------------------------------------------------------------


package DatumTable;


$VERSION = do { my @r=(q$Revision: 1.4 $=~/\d+/g); sprintf "%d."."%02d"x$#r,@r };


#
# I could have created this table as a hash of anonymous arrays, which might have been
# faster/more efficient.  Oh well.
#
# All of the methods here are class methods.  There isn't even a way to make a
# DatumTable object given here.  This class is merely a large collection
# of Datum objects, collected in the %_datum hash.
#

my %_datum;			# Class data


#
# Returns the version number (RCS version tag) of this module.
#
sub version
{
  return $VERSION;
}


sub name				# Name of datum
{
  shift;				# Object reference (don't need it)
  my $datum_name = shift;

  return $_datum{$datum_name}->name();
}


sub ellipsoid				# Name of ellipsoid used in datum
{
  shift;				# Object reference (don't need it)
  my $datum_name = shift;

  return $_datum{$datum_name}->ellipsoid();
}


sub dx					# X-offset from WGS84 ellipsoid center
{
  shift;				# Object reference (don't need it)
  my $datum_name = shift;

  return $_datum{$datum_name}->dx();
}


sub dy					# Y-offset from WGS84 ellipsoid center
{
  shift;				# Object reference (don't need it)
  my $datum_name = shift;

  return $_datum{$datum_name}->dy();
}


sub dz					# Z-offset from WGS84 ellipsoid center
{
  shift;				# Object reference (don't need it)
  my $datum_name = shift;

  return $_datum{$datum_name}->dz();
}


#
# This method allows printing out each defined Datum.
#
sub enumerate
{
  my $self = shift;

  print "\nDatum\t\t\t\t\t\t\t\tEllipsoid\tDx\tDy\tDz\n";
  print   "-----\t\t\t\t\t\t\t\t---------\t--\t--\t--\n";

  foreach my $key (sort keys %_datum)
  {
    printf("%s\n\t\t\t\t\t\t%23s,\t%s,\t%s,\t%s\n",
	$key,
	$self->ellipsoid($key),
	$self->dx($key),
	$self->dy($key),
	$self->dz($key) );
  }
  return(1);
}





#
# Auto-run code
#
# Fill in the Class data.
#
# This code fills in the %_datum hash which has entries consisting of Datum objects.
#
# From the original C code:
#
# "Dx, Dy, Dz: ellipsoid center with respect to WGS 84 ellipsoid center
#  x axis is the prime meridian
#  y axis is 90 degrees east longitude
#  z axis is the axis of rotation of the ellipsoid"
#
# Most of the current data is from Peter Dana's website.
# This increased the number of datums to around 231.  -- Curt.
#
#
# NOTE:  Consider adding a field for "region of use".
#
#
#	 Name	Name	Ellipsoid	Dx	Dy	Dz
$_datum{"Adindan (Burkina Faso)"}
	= Datum->new( "Adindan (Burkina Faso)",
			"Clarke 1880", -118, -14, 218 );

$_datum{"Adindan (Cameroon)"}
	= Datum->new( "Adindan (Cameroon)",
			"Clarke 1880", -134, -2, 210 );

$_datum{"Adindan (Ethiopia)"}
	= Datum->new( "Adindan (Ethiopia)",
			"Clarke 1880", -165, -11, 206 );

$_datum{"Adindan (Mali)"}
	= Datum->new( "Adindan (Mali)",
			"Clarke 1880", -123, -20, 220 );

$_datum{"Adindan (MEAN for Ethiopia/Sudan)"}
	= Datum->new( "Adindan (MEAN for Ethiopia/Sudan)",
			"Clarke 1880", -166, -15, 204 );

$_datum{"Adindan (Senegal)"}
	= Datum->new( "Adindan (Senegal)",
			"Clarke 1880", -128, -18, 224 );

$_datum{"Adindan (Sudan)"}
	= Datum->new( "Adindan (Sudan)",
			"Clarke 1880", -161, -14, 205 );

$_datum{"Afgooye"}
	= Datum->new( "Afgooye",
			"Krassovsky 1940", -43, -163, 45 );

$_datum{"Ain el Abd 1970 (Bahrain)"}
	= Datum->new( "Ain el Abd 1970 (Bahrain)",
			"International 1924", -150, -250, -1 );

$_datum{"Ain el Abd 1970 (Saudi Arabia)"}
	= Datum->new( "Ain el Abd 1970 (Saudi Arabia)",
			"International 1924", -143, -236, 7 );

$_datum{"American Samoa 1962"}
	= Datum->new( "American Samoa 1962",
			"Clarke 1866", -115, 118, 426 );

$_datum{"Anna 1 Astro 1965"}
	= Datum->new( "Anna 1 Astro 1965",
			"Australian National", -491, -22, 435 );

$_datum{"Antigua Island Astro 1943"}
	= Datum->new( "Antigua Island Astro 1943",
			"Clarke 1880", -270, 13, 62 );

$_datum{"Arc 1950 (Botswana)"}
	= Datum->new( "Arc 1950 (Botswana)",
			"Clarke 1880", -138, -105, -289 );

$_datum{"Arc 1950 (Burundi)"}
	= Datum->new( "Arc 1950 (Burundi)",
			"Clarke 1880", -153, -5, -292 );

$_datum{"Arc 1950 (Lesotho)"}
	= Datum->new( "Arc 1950 (Lesotho)",
			"Clarke 1880", -125, -108, -295 );

$_datum{"Arc 1950 (Malawi)"}
	= Datum->new( "Arc 1950 (Malawi)",
			"Clarke 1880", -161, -73, -317 );

$_datum{"Arc 1950 (MEAN)"}
	= Datum->new( "Arc 1950 (MEAN)",
			"Clarke 1880", -143, -90, -294 );

$_datum{"Arc 1950 (Swaziland)"}
	= Datum->new( "Arc 1950",
			"Clarke 1880", -134, -105, -295 );

$_datum{"Arc 1950 (Zaire)"}
	= Datum->new( "Arc 1950",
			"Clarke 1880", -169, -19, -278 );

$_datum{"Arc 1950 (Zambia)"}
	= Datum->new( "Arc 1950",
			"Clarke 1880", -147, -74, -283 );

$_datum{"Arc 1950 (Zimbabwe)"}
	= Datum->new( "Arc 1950",
			"Clarke 1880", -142, -96, -293 );

$_datum{"Arc 1960 (MEAN)"}
	= Datum->new( "Arc 1960 (MEAN)",
			"Clarke 1880", -160, -6, -302 );

$_datum{"Arc 1960 (Kenya)"}
	= Datum->new( "Arc 1960 (Kenya)",
			"Clarke 1880", -157, -2, -299 );

$_datum{"Arc 1960 (Tanzania)"}
	= Datum->new( "Arc 1960 (Tanzania)",
			"Clarke 1880", -175, -23, -303 );

$_datum{"Ascension Island 1958"}
	= Datum->new( "Ascension Island 1958",
			"International 1924", -205, 107, 53 );

$_datum{"Astro B4 Sorol Atoll"}
	= Datum->new( "Astro B4 Sorol Atoll",
			"International 1924", 114, -116, -333 );

$_datum{"Astro Beacon E 1945"}
	= Datum->new( "Astro Beacon E 1945",
			"International 1924", 145, 75, -272 );

$_datum{"Astro DOS 71/4"}
	= Datum->new( "Astro DOS 71/4",
			"International 1924", -320, 550, -494 );

$_datum{"Astro Tern Island (FRIG) 1961"}
	= Datum->new( "Astro Tern Island (FRIG) 1961",
			"International 1924", 114, -116, -333 );

$_datum{"Astronomical Station 1952"}
	= Datum->new( "Astronomical Station 1952",
			"International 1924", 124, -234, -25 );

$_datum{"Australian Geodetic 1966"}
	= Datum->new( "Australian Geodetic 1966",
			"Australian National", -133, -48, 148 );

$_datum{"Australian Geodetic 1984"}
	= Datum->new( "Australian Geodetic 1984",
			"Australian National", -134, -48, 149 );

$_datum{"Ayabelle Lighthouse"}
	= Datum->new( "Ayabelle Lighthouse",
			"Clarke 1880", -79, -129, 145 );

$_datum{"Bellevue (IGN)"}
	= Datum->new( "Bellevue (IGN)",
			"International 1924", -127, -769, 472 );

$_datum{"Bermuda 1957"}
	= Datum->new( "Bermuda 1957",
			"Clarke 1866", -73, 213, 296 );

$_datum{"Bogota Observatory"}
	= Datum->new( "Bogota Observatory",
			"International 1924", 307, 304, -318 );

$_datum{"Bukit Rimpah"}
	= Datum->new( "Bukit Rimpah",
			"Bessel 1841", -384, 664, -48 );

$_datum{"Camp Area Astro"}
	= Datum->new( "Camp Area Astro",
			"International 1924", -104, -129, 239 );

$_datum{"Campo Inchauspe"}
	= Datum->new( "Campo Inchauspe",
			"International 1924", -148, 136, 90 );

$_datum{"Canton Astro 1966"}
	= Datum->new( "Canton Astro 1966",
			"International 1924", 298, -304, -375 );

$_datum{"Cape"}
	= Datum->new( "Cape",
			"Clarke 1880", -136, -108, -292 );

$_datum{"Cape Canaveral"}
	= Datum->new( "Cape Canaveral",
			"Clarke 1866", -2, 151, 181 );

$_datum{"Carthage"}
	= Datum->new( "Carthage",
			"Clarke 1880", -263, 6, 431 );

$_datum{"CH-1903"}
	= Datum->new( "CH-1903",
			"Bessel 1841", 674, 15, 405);

$_datum{"Chatham Island Astro 1971"}
	= Datum->new( "Chatham Island Astro 1971",
			"International 1924", 175, -38, 113);

$_datum{"Chua Astro"}
	= Datum->new( "Chua Astro",
			"International 1924", -134, 229, -29 );

$_datum{"Corrego Alegre"}
	= Datum->new( "Corrego Alegre",
			"International 1924", -206, 172, -6 );

$_datum{"Dabola"}
	= Datum->new( "Dabola",
			"Clarke 1880", -83, 37, 124 );

$_datum{"Deception Island"}
	= Datum->new( "Deception Island",
			"Clarke 1880", 260, 12, -147 );

$_datum{"Djakarta (Batavia)"}
	= Datum->new( "Djakarta (Batavia)",
			"Bessel 1841", -377, 681, -50 );

$_datum{"DOS 1968"}
	= Datum->new( "DOS 1968",
			"International 1924", 230, -199, -752 );

$_datum{"Easter Island 1967"}
	= Datum->new( "Easter Island 1967",
			"International 1924", 211, 147, 111 );

$_datum{"Estonia 1937"}
	= Datum->new( "Estonia 1937",
			"Bessel 1841", 374, 150, 588 );

$_datum{"European 1950 (Cyprus)"}
	= Datum->new( "European 1950 (Cyprus)",
			"International 1924", -104, -101, -140 );

$_datum{"European 1950 (Egypt)"}
	= Datum->new( "European 1950 (Egypt)",
			"International 1924", -130, -117, -151 );

$_datum{"European 1950 (England/Channel Is/Scotland/Shetland Is)"}
	= Datum->new( "European 1950 (England/Channel Is/Scotland/Shetland Is)",
			"International 1924", -86, -96, -120 );

$_datum{"European 1950 (England/Ireland/Scotland/Shetland Is)"}
	= Datum->new( "European 1950 (England/Ireland/Scotland/Shetland Is)",
			"International 1924", -86, -96, -120 );

$_datum{"European 1950 (Finland/Norway)"}
	= Datum->new( "European 1950 (Finland/Norway)",
			"International 1924", -87, -95, -120 );

$_datum{"European 1950 (Greece)"}
	= Datum->new( "European 1950 (Greece)",
			"International 1924", -84, -95, -130 );

$_datum{"European 1950 (Iran)"}
	= Datum->new( "European 1950 (Iran)",
			"International 1924", -117, -132, -164 );

$_datum{"European 1950 (Italy:Sardinia)"}
	= Datum->new( "European 1950 (Italy:Sardinia)",
			"International 1924", -97, -103, -120 );

$_datum{"European 1950 (Italy:Sicily)"}
	= Datum->new( "European 1950 (Italy:Sicily)",
			"International 1924", -97, -88, -135 );

$_datum{"European 1950 (Malta)"}
	= Datum->new( "European 1950 (Malta)",
			"International 1924", -107, -88, -149 );

$_datum{"European 1950 (MEAN for Austria/Belgium/Denmark/Finland/France/W Germany/Gibraltar/Greece/Italy/Luxembourg/Netherlands/Norway/Portugal/Spain/Sweden/Switzerland)"}
	= Datum->new( "European 1950 (MEAN for Austria/Belgium/Denmark/Finland/France/W Germany/Gibraltar/Greece/Italy/Luxembourg/Netherlands/Norway/Portugal/Spain/Sweden/Switzerland)",
			"International 1924", -87, -98, -121 );

$_datum{"European 1950 (MEAN for Austria/Denmark/France/W Germany/Netherlands/Switzerland)"}
	= Datum->new( "European 1950 (MEAN for Austria/Denmark/France/W Germany/Netherlands/Switzerland)",
			"International 1924", -87, -96, -120 );

$_datum{"European 1950 (MEAN for Iraq/Israel/Jordan/Lebanon/Kuwait/Saudi Arabia/Syria)"}
	= Datum->new( "European 1950 (MEAN for Iraq/Israel/Jordan/Lebanon/Kuwait/Saudi Arabia/Syria)",
			"International 1924", -103, -106, -141 );

$_datum{"European 1950 (Portugal/Spain)"}
	= Datum->new( "European 1950 (Portugal/Spain)",
			"International 1924", -84, -107, -120 );

$_datum{"European 1950 (Tunisia)"}
	= Datum->new( "European 1950 (Tunisia)",
			"International 1924", -112, -77, -145 );

$_datum{"European 1979 (MEAN for Austria/Finland/Netherlands/Norway/Spain/Sweden/Switzerland)"}
	= Datum->new( "European 1979 (MEAN for Austria/Finland/Netherlands/Norway/Spain/Sweden/Switzerland)",
			"International 1924",  -86, -98, -119 );

$_datum{"Finland Hayford"}
	= Datum->new( "Finland Hayford",
			"International 1924", -78, -231, -97 );

$_datum{"Fort Thomas"}
	= Datum->new( "Fort Thomas",
			"Clarke 1880", -7, 215, 225 );

$_datum{"Gandajika Base 1970"}
	= Datum->new( "Gandajika Base 1970",
			"International 1924", -133, -321, 50 );

$_datum{"Geodetic Datum 1949"}
	= Datum->new( "Geodetic Datum 1949",
			"International 1924", 84, -22, 209 );

$_datum{"Graciosa Base SW 1948"}
	= Datum->new( "Graciosa Base SW 1948",
			"International 1924", -104, 167, -38 );

$_datum{"Guam 1963"}
	= Datum->new( "Guam 1963",
			"Clarke 1866", -100, -248, 259 );

$_datum{"Gunung Segara"}
	= Datum->new( "Gunung Segara",
			"Bessel 1841", -403, 684, 41 );

$_datum{"GUX 1 Astro"}
	= Datum->new( "GUX 1 Astro",
			"International 1924", 252, -209, -751 );

$_datum{"Herat North"}
	= Datum->new( "Herat North",
			"International 1924", -333, -222, 114 );

$_datum{"Hermannskogel Datum (Namibia)"}
	= Datum->new( "Hermannskogel Datum (Namibia)",
			"Bessel 1841", 653, -212, 449 );

$_datum{"Hjorsey 1955"}
	= Datum->new( "Hjorsey 1955",
			"International 1924", -73, 46, -86 );

$_datum{"Hong Kong 1963"}
	= Datum->new( "Hong Kong 1963",
			"International 1924", -156, -271, -189 );

$_datum{"Hu-Tzu-Shan"}
	= Datum->new( "Hu-Tzu-Shan",
			"International 1924", -637, -549, -203 );

$_datum{"Indian (Bangladesh)"}
	= Datum->new( "Indian (Bangladesh)",
			"Everest (India 1830)", 282, 726, 254 );

$_datum{"Indian (India/Nepal)"}
	= Datum->new( "Indian (India/Nepal)",
			"Everest (India 1956)", 295, 736, 257 );

$_datum{"Indian (Pakistan)"}
	= Datum->new( "Indian (Pakistan)",
			"Everest (Pakistan)", 283, 682, 231 );

$_datum{"Indian 1954 (Thailand)"}
	= Datum->new( "Indian 1954 (Thailand)",
			"Everest (India 1830)", 217, 823, 299 );

$_datum{"Indian 1960 (Vietnam:Con Son Is)"}
	= Datum->new( "Indian 1960 (Vietnam:Con Son Is)",
			"Everest (India 1830)", 182, 915, 344 );

$_datum{"Indian 1960 (Vietnam:Near 160N)"}
	= Datum->new( "Indian 1960 (Vietnam:Near 160N)",
			"Everest (India 1830)", 198, 881, 317 );

$_datum{"Indian 1975 (Thailand)"}
	= Datum->new( "Indian 1975 (Thailand)",
			"Everest (India 1830)", 210, 814, 289 );

$_datum{"Indonesian 1974"}
	= Datum->new( "Indonesian 1974)",
			"Indonesian 1974)", -24, -15, 5 );

$_datum{"Ireland 1965"}
	= Datum->new( "Ireland 1965",
			"Modified Airy", 506, -122, 611 );

$_datum{"ISTS 061 Astro 1968"}
	= Datum->new( "ISTS 061 Astro 1968",
			"International 1924", -794, 119, -298 );

$_datum{"ISTS 073 Astro 1969"}
	= Datum->new( "ISTS 073 Astro 1969",
			"International 1924", 208, -435, -229 );

$_datum{"Johnston Island"}
	= Datum->new( "Johnston Island",
			"International 1924", 189, -79, -202 );

$_datum{"Kandawala"}
	= Datum->new( "Kandawala",
			"Everest (India 1830)", -97, 787, 86 );

$_datum{"Kerguelen Island 1949"}
	= Datum->new( "Kerguelen Island 1949",
			"International 1924", 145, -187, 103 );

$_datum{"Kertau 1948"}
	= Datum->new( "Kertau 1948",
			"Everest (Malay. & Sing)", -11, 851, 5 );

$_datum{"Kusaie Astro 1951"}
	= Datum->new( "Kusaie Astro 1951",
			"International 1924", 647, 1777, -1124 );

$_datum{"Korean Geodetic System"}
	= Datum->new( "Korean Geodetic System",
			"GRS 1980", 0, 0, 0 );

$_datum{"L. C. 5 Astro 1961"}
	= Datum->new( "L. C. 5 Astro 1961",
			"Clarke 1866", 42, 124, 147 );

$_datum{"Leigon"}
	= Datum->new( "Leigon",
			"Clarke 1880", -130, 29, 364 );

$_datum{"Liberia 1964"}
	= Datum->new( "Liberia 1964",
			"Clarke 1880", -90, 40, 88 );

$_datum{"Luzon (Philippines:Except Mindanao)"}
	= Datum->new( "Luzon (Philippines:Except Mindanao)",
			"Clarke 1866", -133, -77, -51 );

$_datum{"Luzon (Philippines:Mindanao)"}
	= Datum->new( "Luzon (Philippines:Mindanao)",
			"Clarke 1866", -133, -79, -72 );

$_datum{"M\'Poraloko"}
	= Datum->new( "M\'Poraloko",
			"Clarke 1880", -74, -130, 42 );

$_datum{"Mahe 1971"}
	= Datum->new( "Mahe 1971",
			"Clarke 1880", 41, -220, -134 );

$_datum{"Marco Astro"}
	= Datum->new( "Marco Astro",
			"International 1924", -289, -124, 60 );

$_datum{"Massawa"}
	= Datum->new( "Massawa",
			"Bessel 1841",  639, 405, 60 );

$_datum{"Merchich"}
	= Datum->new( "Merchich",
			"Clarke 1880", 31, 146, 47 );

$_datum{"Midway Astro 1961"}
	= Datum->new( "Midway Astro 1961",
			"International 1924", 912, -58, 1227 );

$_datum{"Minna (Cameroon)"}
	= Datum->new( "Minna (Cameroon)",
			"Clarke 1880", -81, -84, 115 );

$_datum{"Minna (Nigeria)"}
	= Datum->new( "Minna (Nigeria)",
			"Clarke 1880", -92, -93, 122 );

$_datum{"Montserrat Island Astro 1958"}
	= Datum->new( "Montserrat Island Astro 1958",
			"Clarke 1880", 174, 359, 365 );

$_datum{"Nahrwan (Oman:Masirah Is)"}
	= Datum->new( "Nahrwan (Oman:Masirah Is)",
			"Clarke 1880", -247, -148, 369 );

$_datum{"Nahrwan (Saudi Arabia)"}
	= Datum->new( "Nahrwan (Saudi Arabia)",
			"Clarke 1880", -243, -192, 477 );

$_datum{"Nahrwan (United Arab Emirates)"}
	= Datum->new( "Nahrwan (United Arab Emirates)",
			"Clarke 1880", -249, -156, 381 );

$_datum{"Naparima BWI"}
	= Datum->new( "Naparima BWI",
			"International 1924", -10, 375, 165 );

$_datum{"NAD27 Alaska:Except Aleutian Is"}
	= Datum->new( "NAD27 Alaska:Except Aleutian Is",
			"Clarke 1866", -5, 135, 172 );

$_datum{"NAD27 Alaska:Aleutian Is E of 180W"}
	= Datum->new( "NAD27 Alaska:Aleutian Is E of 180W",
			"Clarke 1866", -2, 152, 149 );

$_datum{"NAD27 Alaska:Aleutian Is W of 180W"}
	= Datum->new( "NAD27 Alaska:Aleutian Is W of 180W",
			"Clarke 1866", 2, 204, 105 );

$_datum{"NAD27 Bahamas:Except San Salvador Is"}
	= Datum->new( "NAD27 Bahamas:Except San Salvador Is",
			"Clarke 1866", -4, 154, 178 );

$_datum{"NAD27 Bahamas:San Salvador Is"}
	= Datum->new( "NAD27 Bahamas:San Salvador Is",
			"Clarke 1866", 1, 140, 165 );

$_datum{"NAD27 Canada (Alberta/B.C.)"}
	= Datum->new( "NAD27 Canada (Alberta/B.C.)",
			"Clarke 1866", -7, 162, 188 );

$_datum{"NAD27 Canada (Manitoba/Ontario)"}
	= Datum->new( "NAD27 Canada (Manitoba/Ontario)",
			"Clarke 1866", -9, 157, 184 );

$_datum{"NAD27 Canada MEAN"}
	= Datum->new( "NAD27 Canada MEAN",
			"Clarke 1866", -10, 158, 189 );

$_datum{"NAD27 Canada (New Brunswick/Newfoundland/Nova Scotia/Quebec)"}
	= Datum->new( "NAD27 Canada (New Brunswick/Newfoundland/Nova Scotia/Quebec)",
			"Clarke 1866", -22, 160, 190 );

$_datum{"NAD27 Canada (Northwest Territories/Saskatchewan)"}
	= Datum->new( "NAD27 Canada (Northwest Territories/Saskatchewan)",
			"Clarke 1866", 4, 159, 188 );

$_datum{"NAD27 Canada (Yukon)"}
	= Datum->new( "NAD27 Canada (Yukon)",
			"Clarke 1866", -7, 139, 181 );

$_datum{"NAD27 Canal Zone"}
	= Datum->new( "NAD27 Canal Zone",
			"Clarke 1866", 0, 125, 201 );

$_datum{"NAD27 Caribbean"}
	= Datum->new( "NAD27 Caribbean",
			"Clarke 1866", -7, 152, 178 );

$_datum{"NAD27 MEAN:Antigua/Barbados/Barbuda/Caicos Islands/Cuba/Dominican Republic/Grand Cayman/Jamaica/Turks Islands"}
	= Datum->new( "NAD27 MEAN:Antigua/Barbados/Barbuda/Caicos Islands/Cuba/Dominican Republic/Grand Cayman/Jamaica/Turks Islands",
			"Clarke 1866", -3, 142, 183 );

$_datum{"NAD27 MEAN:Belize/Costa Rica/El Salvador/Guatemala/Honduras/Nicaragua"}
	= Datum->new( "NAD27 MEAN:Belize/Costa Rica/El Salvador/Guatemala/Honduras/Nicaragua",
			"Clarke 1866", 0, 125, 194 );

$_datum{"NAD27 CONUS MEAN"}
	= Datum->new( "NAD27 CONUS MEAN",
			"Clarke 1866", -8, 160, 176 );

  # Added for compatibility reasons.  Same as above.
$_datum{"NAD27 CONUS"}
	= Datum->new( "NAD27 CONUS",
			"Clarke 1866", -8, 160, 176 );

$_datum{"NAD27 CONUS MEAN:E of Mississippi/Louisiana/Missouri/Minnesota"}
	= Datum->new( "NAD27 CONUS MEAN:E of Mississippi/Louisiana/Missouri/Minnesota",
			"Clarke 1866", -9, 161, 179 );

$_datum{"NAD27 CONUS MEAN:W of Mississippi/Except Louisiana/Minnesota/Missouri"}
	= Datum->new( "NAD27 CONUS W of Mississippi/Except Louisiana/Minnesota/Missouri",
			"Clarke 1866", -8, 159, 175 );

$_datum{"NAD27 Cuba"}
	= Datum->new( "NAD27 Cuba",
			"Clarke 1866", -9, 152, 178 );

$_datum{"NAD27 Greenland (Hayes Peninsula)"}
	= Datum->new( "NAD27 Greenland (Hayes Peninsula)",
			"Clarke 1866", 11, 114, 195 );

$_datum{"NAD27 Mexico"}
	= Datum->new( "NAD27 Mexico",
			"Clarke 1866", -12, 130, 190 );

$_datum{"NAD27 San Salvador"}
	= Datum->new( "NAD27 San Salvador",
			"Clarke 1866", 1, 140, 165 );

$_datum{"NAD83 Alaska (Except Aleutian Is)"}
	= Datum->new( "NAD83 Alaska (Except Aleutian Is)",
			"GRS 1980", 0, 0, 0 );

$_datum{"NAD83 Aleutian Is"}
	= Datum->new( "NAD83 Aleutian Is",
			"GRS 1980", -2, 0, 4 );

$_datum{"NAD83 Canada"}
	= Datum->new( "NAD83 Canada",
			"GRS 1980", 0, 0, 0 );

$_datum{"NAD83 CONUS"}
	= Datum->new( "NAD83 CONUS",
			"GRS 1980", 0, 0, 0 );

$_datum{"NAD83 Hawaii"}
	= Datum->new( "NAD83 Hawaii",
			"GRS 1980", 1, 1, -1 );

$_datum{"NAD83 Mexico/Central America"}
	= Datum->new( "NAD83 Mexico/Central America",
			"GRS 1980", 0, 0, 0 );

$_datum{"North Sahara"}
	= Datum->new( "North Sahara",
			"Clarke 1880", -186, -93, 310 );

$_datum{"Nahrwn Masirah Ilnd"}
	= Datum->new( "Nahrwn Masirah Ilnd",
			"Clarke 1880", -247, -148, 369 );

$_datum{"Nahrwn Saudi Arbia"}
	= Datum->new( "Nahrwn Saudi Arbia",
			"Clarke 1880", -231, -196, 482 );

$_datum{"Nahrwn United Arab"}
	= Datum->new( "Nahrwn United Arab",
			"Clarke 1880", -249, -156, 381 );

$_datum{"Naparima BWI"}
	= Datum->new( "Naparima BWI",
			"International 1924", -2, 374, 172 );

$_datum{"Observatorio Meteorologico 1939"}
	= Datum->new( "Observatorio Meteorologico 1939",
			"International 1924", -425, -169, 81 );

$_datum{"Old Egyptian 1907"}
	= Datum->new( "Old Egyptian 1907",
			"Helmert 1906", -130, 110, -13 );

$_datum{"Old Hawaiian Hawaii"}
	= Datum->new( "Old Hawaiian Hawaii",
			"Clarke 1866", 89, -279, -183 );

$_datum{"Old Hawaiian Kauai"}
	= Datum->new( "Old Hawaiian Kauai",
			"Clarke 1866", 45, -290, -172 );

$_datum{"Old Hawaiian Maui"}
	= Datum->new( "Old Hawaiian Maui",
			"Clarke 1866", 65, -290, -190 );

$_datum{"Old Hawaiian MEAN"}
	= Datum->new( "Old Hawaiian MEAN",
			"Clarke 1866", 61, -285, -181 );

$_datum{"Old Hawaiian Oahu"}
	= Datum->new( "Old Hawaiian Oahu",
			"Clarke 1866", 58, -283, -182 );

$_datum{"Oman"}
	= Datum->new( "Oman",
			"Clarke 1880", -346, -1, 224 );

$_datum{"Ordnance Survey Great Britain 1936 England"}
	= Datum->new( "Ordnance Survey Great Britain 1936 England",
			"Airy 1830", 371, -112, 434 );

$_datum{"Ordnance Survey Great Britain 1936 England/Isle of Man/Wales"}
	= Datum->new( "Ordnance Survey Great Britain 1936 England/Isle of Man/Wales",
			"Airy 1830", 371, -111, 434 );

$_datum{"Ordnance Survey Great Britain 1936 MEAN:England/Isle of Man/Scotland/Shetland Is/Wales"}
	= Datum->new( "Ordnance Survey Great Britain 1936 MEAN:England/Isle of Man/Scotland/Shetland Is/Wales",
			"Airy 1830", 375, -111, 431 );

$_datum{"Ordnance Survey Great Britain 1936 Scotland/Shetland Is"}
	= Datum->new( "Ordnance Survey Great Britain 1936 Scotland/Shetland Is",
			"Airy 1830", 384, -111, 425 );

$_datum{"Ordnance Survey Great Britain 1936 Wales"}
	= Datum->new( "Ordnance Survey Great Britain 1936 Wales",
			"Airy 1830", 370, -108, 434 );

$_datum{"Pico De Las Nieves"}
	= Datum->new( "Pico De Las Nieves",
			"International 1924", -307, -92, 127 );

$_datum{"Pitcairn Astro 1967"}
	= Datum->new( "Pitcairn Astro 1967",
			"International 1924", 185, 165, 42 );

$_datum{"Point 58"}
	= Datum->new( "Point 58",
			"Clarke 1880", -106, -129, 165 );

$_datum{"Pointe Noire 1948"}
	= Datum->new( "Pointe Noire 1948",
			"Clarke 1880", -148, 51, -291 );

$_datum{"Porto Santo 1936"}
	= Datum->new( "Porto Santo 1936",
			"International 1924", -499, -249, 314 );

$_datum{"Provisional South American 1956 Bolivia"}
	= Datum->new( "Provisional South American 1956 Bolivia",
			"International 1924", -270, 188, -388 );

$_datum{"Provisional South American 1956 Chile Northern/Near 19S"}
	= Datum->new( "Provisional South American 1956 Chile Northern/Near 19S",
			"International 1924", -270, 183, -390 );

$_datum{"Provisional South American 1956 Chile Southern/Near 43S"}
	= Datum->new( "Provisional South American 1956 Chile Southern/Near 43S",
			"International 1924", -305, 243, -442 );

$_datum{"Provisional South American 1956 Columbia"}
	= Datum->new( "Provisional South American 1956 Columbia",
			"International 1924", -282, 169, -371 );

$_datum{"Provisional South American 1956 Ecuador"}
	= Datum->new( "Provisional South American 1956 Ecuador",
			"International 1924", -278, 171, -367 );

$_datum{"Provisional South American 1956 Guyana"}
	= Datum->new( "Provisional South American 1956 Guyana",
			"International 1924", -298, 159, -369 );

$_datum{"Provisional South American 1956 MEAN:Bolivia/Chile/Columbia/Ecuador/Guyana/Peru/Venezuela"}
	= Datum->new( "Provisional South American 1956 MEAN:Bolivia/Chile/Columbia/Ecuador/Guyana/Peru/Venezuela",
			"International 1924", -288, 175, -376 );

$_datum{"Provisional South American 1956 Peru"}
	= Datum->new( "Provisional South American 1956 Peru",
			"International 1924", -279, 175, -379 );

$_datum{"Provisional South American 1956 Venezuela"}
	= Datum->new( "Provisional South American 1956 Venezuela",
			"International 1924", -295, 173, -371 );

$_datum{"Provisional South Chilean 1963 Near 53S"}
	= Datum->new( "Provisional South Chilean 1963 Near 53S",
			"International 1924", 16, 196, 93 );

$_datum{"Puerto Rico/Virgin Is"}
	= Datum->new( "Puerto Rico/Virgin Is",
			"Clarke 1866", 11, 72, -101 );

$_datum{"Pulkovo 1942"}
	= Datum->new( "Pulkovo 1942",
			"Krassovsky 1940", 28, -130, -95 );

$_datum{"Qatar National"}
	= Datum->new( "Qatar National",
			"International 1924", -128, -283, 22 );

$_datum{"Qornoq"}
	= Datum->new( "Qornoq",
			"International 1924", 164, 138, -189 );

$_datum{"Reunion"}
	= Datum->new( "Reunion",
			"International 1924", 94, -948, -1262 );

$_datum{"Rome 1940"}
	= Datum->new( "Rome 1940",
			"International 1924", -225, -65, 9 );

$_datum{"RT 90"}
	= Datum->new( "RT 90",
			"Bessel 1841", 498, -36, 568 );

$_datum{"S-42 (Pulkovo 1942) Hungary"}
	= Datum->new( "S-42 (Pulkovo 1942) Hungary",
			"Krassovsky 1940", 28, -121, -77 );

$_datum{"S-42 (Pulkovo 1942) Poland"}
	= Datum->new( "S-42 (Pulkovo 1942) Poland",
			"Krassovsky 1940", 23, -124, -82 );

$_datum{"S-42 (Pulkovo 1942) Czechoslavakia"}
	= Datum->new( "S-42 (Pulkovo 1942) Czechoslavakia",
			"Krassovsky 1940", 26, -121, -78 );

$_datum{"S-42 (Pulkovo 1942) Latvia"}
	= Datum->new( "S-42 (Pulkovo 1942) Latvia",
			"Krassovsky 1940", 24, -124, -82 );

$_datum{"S-42 (Pulkovo 1942) Kazakhstan"}
	= Datum->new( "S-42 (Pulkovo 1942) Kazakhstan",
			"Krassovsky 1940", 15, -130, -84 );

$_datum{"S-42 (Pulkovo 1942) Albania"}
	= Datum->new( "S-42 (Pulkovo 1942) Albania",
			"Krassovsky 1940", 24, -130, -92 );

$_datum{"S-42 (Pulkovo 1942) Romania"}
	= Datum->new( "S-42 (Pulkovo 1942) Romania",
			"Krassovsky 1940", 28, -121, -77 );

$_datum{"S-JTSK Czechoslavakia"}
	= Datum->new( "S-JTSK Czechoslavakia",
			"Bessel 1841", 589, 76, 480 );

$_datum{"Santo (DOS) 1965"}
	= Datum->new( "Santo (DOS) 1965",
			"International 1924", 170, 42, 84 );

$_datum{"Sao Braz"}
	= Datum->new( "Sao Braz",
			"International 1924", -203, 141, 53 );

$_datum{"Sapper Hill 1943"}
	= Datum->new( "Sapper Hill 1943",
			"International 1924", -355, 21, 72 );

$_datum{"Schwarzeck"}
	= Datum->new( "Schwarzeck",
			"Bessel 1841 (Namibia)", 616, 97, -251 );

$_datum{"Selvagem Grande 1938"}
	= Datum->new( "Selvagem Grande 1938",
			"International 1924", -289, -124, 60 );

$_datum{"Sierra Leone 1960"}
	= Datum->new( "Sierra Leone 1960",
			"Clarke 1880", -88, 4, 101 );

$_datum{"South American 1969 Argentina"}
	= Datum->new( "South American 1969 Argentina",
			"South American 1969", -62, -1, -37 );

$_datum{"South American 1969 Bolivia"}
	= Datum->new( "South American 1969 Bolivia",
			"South American 1969", -61, 2, -48 );

$_datum{"South American 1969 Brazil"}
	= Datum->new( "South American 1969 Brazil",
			"South American 1969", -60, -2, -41 );

$_datum{"South American 1969 Chile"}
	= Datum->new( "South American 1969 Chile",
			"South American 1969", -75, -1, -44 );

$_datum{"South American 1969 Colombia"}
	= Datum->new( "South American 1969 Colombia",
			"South American 1969", -44, 6, -36 );

$_datum{"South American 1969 Ecuador"}
	= Datum->new( "South American 1969 Ecuador",
			"South American 1969", -48, 3, -44 );

$_datum{"South American 1969 Ecuador:Baltra/Galapagos"}
	= Datum->new( "South American 1969 Ecuador:Baltra/Galapagos",
			"South American 1969", -47, 26, -42 );

$_datum{"South American 1969 Guyana"}
	= Datum->new( "South American 1969 Guyana",
			"South American 1969", -53, 3, -47 );

$_datum{"South American 1969 MEAN"}
	= Datum->new( "South American 1969 MEAN",
			"South American 1969", -57, 1, -41 );

$_datum{"South American 1969 Paraguay"}
	= Datum->new( "South American 1969 Paraguay",
			"South American 1969", -61, 2, -33 );

$_datum{"South American 1969 Peru"}
	= Datum->new( "South American 1969 Peru",
			"South American 1969", -58, 0, -44 );

$_datum{"South American 1969 Trinidad/Tobago"}
	= Datum->new( "South American 1969 Trinidad/Tobago",
			"South American 1969", -45, 12, -33 );

$_datum{"South American 1969 Venezuela"}
	= Datum->new( "South American 1969 Venezuela",
			"South American 1969", -45, 8, -33 );

$_datum{"South Asia"}
	= Datum->new( "South Asia",
			"Modified Fischer 1960", 7, -10, -26 );

$_datum{"Southeast Base"}
	= Datum->new( "Southeast Base",
			"International 1924", -499, -249, 314 );

$_datum{"Southwest Base"}
	= Datum->new( "Southwest Base",
			"International 1924", -104, 167, -38 );

$_datum{"Tananarive Observatory 1925"}
	= Datum->new( "Tananarive Observatory 1925",
			"International 1924", -189, -242, -91 );

$_datum{"Timbalai 1948"}
	= Datum->new( "Timbalai 1948",
			"Everest (Sabah Sarawak)", -679, 669, -48 );

$_datum{"Tokyo Japan"}
	= Datum->new( "Tokyo Japan",
			"Bessel 1841", -148, 507, 685 );

$_datum{"Tokyo MEAN"}
	= Datum->new( "Tokyo MEAN",
			"Bessel 1841", -148, 507, 685 );

$_datum{"Tokyo Okinawa"}
	= Datum->new( "Tokyo Okinawa",
			"Bessel 1841", -158, 507, 676 );

$_datum{"Tokyo South Korea"}
	= Datum->new( "Tokyo South Korea",
			"Bessel 1841", -147, 506, 687 );

$_datum{"Tristan Astro 1968"}
	= Datum->new( "Tristan Astro 1968",
			"International 1924", -632, 438, -609 );

$_datum{"Viti Levu 1916"}
	= Datum->new( "Viti Levu 1916",
			"Clarke 1880", 51, 391, -36 );

$_datum{"Voirol 1960"}
	= Datum->new( "Voirol 1960",
			"Clarke 1880", -123, -206, 219 );
  
$_datum{"Wake Island Astro 1952"}
	= Datum->new( "Wake Island Astro 1952",
			"International 1924", 276, -57, 149 );

$_datum{"Wake-Eniwetok 1960"}
	= Datum->new( "Wake-Eniwetok 1960",
			"Hough 1960", 102, 52, -38 );

$_datum{"WGS 72"}
	= Datum->new( "WGS 72",
			"WGS 72", 0, 0, 0 );

$_datum{"WGS 84"}
	= Datum->new( "WGS 84",
			"WGS 84", 0, 0, 0 );

$_datum{"Yacare"}
	= Datum->new( "Yacare",
			"International 1924", -155, 171, 37 );

$_datum{"Zanderij"}
	= Datum->new( "Zanderij",
			"International 1924", -265, 120, -358 );


#------------------------------------------------------------------------------------------------


package Coordinate;


#
# Note that we export NOTHING!  This module strives to be object-oriented,
# therefore exporting is a bad thing.  You can get to any of the public
# methods by using normal method calls on an object.
#


$VERSION = do { my @r=(q$Revision: 1.4 $=~/\d+/g); sprintf "%d."."%02d"x$#r,@r };


use Math::Trig;
#use Math::BigFloat;    # Allows arbitrary length floating point operations
#use strict;





#
# Here is a Coordinate object.  It contains all of the data
# for describing a single point on the earth and the methods
# for setting/getting these values.
#

#
# This constructor can be used to create and initialize an object
# with Latitude/Longitude/Datum fields.
#
sub new
{
  my $class = shift;		# What class are we constructing?
  my $self      = { };		# Allocate new memory
  bless $self, $class;		# Mark it of the right type
  $self->_init(@_);		# Call _init with remaining args
  return $self;
}


sub _init
{
  my $self = shift;
  $self->{LATITUDE} = shift if @_;
  $self->{LONGITUDE} = shift if @_;
  $self->{DATUM} = shift if @_;
}


#
# This constructor can be used to create and initialize an object
# with easting/northing/zone/datum fields (UTM grid).
#
sub new_utm
{
  my $class = shift;            # What class are we constructing?
  my $self      = { };          # Allocate new memory
  bless $self, $class;          # Mark it of the right type
  $self->_init_utm(@_);         # Call _init with remaining args
  return $self;
}


sub _init_utm
{
  my $self = shift;
  $self->{EASTING} = shift if @_;
  $self->{NORTHING} = shift if @_;
  $self->{ZONE} = shift if @_;
  $self->{DATUM} = shift if @_;
}


#
# Returns the version number (RCS version tag) of this module.
#
sub version
{
  return $VERSION;
}



sub latitude
{
  my $self = shift;
  $self->{LATITUDE} = shift if @_;
  return $self->{LATITUDE};
}





sub longitude
{
  my $self = shift;
  $self->{LONGITUDE} = shift if @_;
  return $self->{LONGITUDE};
}





sub formatted_latitude
{
  my $self = shift;
  $self->{FORMATTED_LATITUDE} = shift if @_;
  return $self->{FORMATTED_LATITUDE};
}





sub formatted_longitude
{
  my $self = shift;
  $self->{FORMATTED_LONGITUDE} = shift if @_;
  return $self->{FORMATTED_LONGITUDE};
}





sub easting
{
  my $self = shift;
  $self->{EASTING} = shift if @_;
  return $self->{EASTING};
}





sub northing
{
  my $self = shift;
  $self->{NORTHING} = shift if @_;
  return $self->{NORTHING};
}





sub zone
{
  my $self = shift;
  $self->{ZONE} = shift if @_;
  return $self->{ZONE};
}





sub datum
{
  my $self = shift;
  $self->{DATUM} = shift if @_;
  return $self->{DATUM};
}





#
# Converts lat/lon to UTM coordinates.  Equations from USGS Bulletin 1532
# (According to comments in the original C code).
#
# East Longitudes are positive, West are negative. 
# North latitudes are positive, South are negative.
# Latitude and longitude are in decimal degrees.
#
# This method fills in the Easting/Northing/Zone fields in the object.
# The Zone field will look something like: "10U" when filled in.
#
sub lat_lon_to_utm
{
  my $position = shift;			# Snag the pointer to the object.

  my $PI = 3.14159265358979323846;
  my $deg_2_rad = $PI / 180;
  my $k0 = 0.9996;

  # First get the datum used in the position from the object:
  my $datum = $position->datum();

  # Now find out the ellipsoid used by consulting the DatumTable:
  my $ellipsoid = DatumTable->ellipsoid( $datum );
  
  # Do a lookup in the $ellipsoid{} hash to find the Radius value.
  my $a = EllipsoidTable->equatorial_radius( $ellipsoid );

  # Do a lookup in the $ellipsoid{} hash to find the Ecc value.
  my $ecc_Squared = EllipsoidTable->first_ecc_squared( $ellipsoid );

  my $Lat = $position->latitude();
  my $Long = $position->longitude();
 	
  # Make sure the longitude is between -180.00 .. 179.9
  my $Long_Temp = ($Long+180)-int(($Long+180)/360)*360-180; 	# -180.00 .. 179.9;

  my $Lat_Rad = $Lat * $deg_2_rad;
  my $Long_Rad = $Long_Temp * $deg_2_rad;
  my $Zone_Number = int(($Long_Temp + 180)/6) + 1;
 
  if( $Lat >= 56.0 && $Lat < 64.0 && $Long_Temp >= 3.0 && $Long_Temp < 12.0 )
  {
    $Zone_Number = 32;
  }

  # Special zones for Svalbard
  if( $Lat >= 72.0 && $Lat < 84.0 ) 
  {
    if(    $Long_Temp >= 0.0  && $Long_Temp <  9.0 ) { $Zone_Number = 31 }
    elsif( $Long_Temp >= 9.0  && $Long_Temp < 21.0 ) { $Zone_Number = 33 }
    elsif( $Long_Temp >= 21.0 && $Long_Temp < 33.0 ) { $Zone_Number = 35 }
    elsif( $Long_Temp >= 33.0 && $Long_Temp < 42.0 ) { $Zone_Number = 37 };
  }
  my $Long_Origin = ($Zone_Number - 1)*6 - 180 + 3;	# +3 puts origin in middle of zone
  my $Long_Origin_Rad = $Long_Origin * $deg_2_rad;

  # Compute the UTM Zone from the latitude and longitude
  # NOTE:  Need to check for out-of-range here.
  #
  my $Zone_Letter = &_utm_letter_designator($Lat);
  #printf( "Zone_Number: %d\t\tZone_Letter: %s\n", $Zone_Number, $Zone_Letter );
  my $UTM_Zone = sprintf( "%d%s", $Zone_Number, $Zone_Letter );

  my $ecc_Prime_Squared = ($ecc_Squared)/(1-$ecc_Squared);

  my $N = $a/sqrt(1-$ecc_Squared*sin($Lat_Rad)*sin($Lat_Rad));
  my $T = tan($Lat_Rad) * tan($Lat_Rad);
  my $C = $ecc_Prime_Squared * cos($Lat_Rad) * cos($Lat_Rad);
  my $A = cos($Lat_Rad) * ($Long_Rad-$Long_Origin_Rad);

  my $M = $a * ((1 - $ecc_Squared/4 - 3 * $ecc_Squared * $ecc_Squared/64 - 5 * $ecc_Squared * $ecc_Squared * $ecc_Squared/256) * $Lat_Rad
	- (3 * $ecc_Squared/8 + 3 * $ecc_Squared * $ecc_Squared/32 + 45 * $ecc_Squared * $ecc_Squared * $ecc_Squared/1024) * sin(2 * $Lat_Rad)
	+ (15 * $ecc_Squared * $ecc_Squared/256 + 45 * $ecc_Squared * $ecc_Squared * $ecc_Squared/1024) * sin(4 * $Lat_Rad) 
	- (35 * $ecc_Squared * $ecc_Squared * $ecc_Squared/3072) * sin(6 * $Lat_Rad));
	
  my $UTM_Easting = ($k0 * $N * ($A + (1 - $T + $C) * $A * $A * $A/6
	+ (5 - 18 * $T + $T * $T + 72 * $C - 58 * $ecc_Prime_Squared) * $A * $A * $A * $A * $A/120)
	+ 500000.0);

  my $UTM_Northing = ($k0 * ($M + $N * tan($Lat_Rad) * ($A * $A/2 + (5 - $T + 9 * $C + 4 * $C * $C) * $A * $A * $A * $A/24
	+ (61 - 58 * $T + $T * $T + 600 * $C - 330 * $ecc_Prime_Squared) * $A * $A * $A * $A * $A * $A/720)));

  if($Lat < 0)
  {
    $UTM_Northing += 10000000.0;	# 10000000 meter offset for southern hemisphere
  }

  # Write the results back to our position object
  $position->easting( $UTM_Easting );
  $position->northing( $UTM_Northing );
  $position->zone( $UTM_Zone );
  return(1);
}





#
# This routine determines the correct UTM letter designator for the given latitude.
# Returns 'Z' if latitude is outside the UTM limits of 84N to 80S.
#
# Note that this is a normal subroutine, not an object method.  It is to
# be used inside this module only and is not exported.
#
sub _utm_letter_designator
{
  my $Lat = shift;
  my $Letter_Designator;

  if    (( 84 >= $Lat) && ($Lat >=  72)) { $Letter_Designator = 'X' }
  elsif (( 72  > $Lat) && ($Lat >=  64)) { $Letter_Designator = 'W' }
  elsif (( 64  > $Lat) && ($Lat >=  56)) { $Letter_Designator = 'V' }
  elsif (( 56  > $Lat) && ($Lat >=  48)) { $Letter_Designator = 'U' }
  elsif (( 48  > $Lat) && ($Lat >=  40)) { $Letter_Designator = 'T' }
  elsif (( 40  > $Lat) && ($Lat >=  32)) { $Letter_Designator = 'S' }
  elsif (( 32  > $Lat) && ($Lat >=  24)) { $Letter_Designator = 'R' }
  elsif (( 24  > $Lat) && ($Lat >=  16)) { $Letter_Designator = 'Q' }
  elsif (( 16  > $Lat) && ($Lat >=   8)) { $Letter_Designator = 'P' }
  elsif ((  8  > $Lat) && ($Lat >=   0)) { $Letter_Designator = 'N' }
  elsif ((  0  > $Lat) && ($Lat >=  -8)) { $Letter_Designator = 'M' }
  elsif (( -8  > $Lat) && ($Lat >= -16)) { $Letter_Designator = 'L' }
  elsif ((-16  > $Lat) && ($Lat >= -24)) { $Letter_Designator = 'K' }
  elsif ((-24  > $Lat) && ($Lat >= -32)) { $Letter_Designator = 'J' }
  elsif ((-32  > $Lat) && ($Lat >= -40)) { $Letter_Designator = 'H' }
  elsif ((-40  > $Lat) && ($Lat >= -48)) { $Letter_Designator = 'G' }
  elsif ((-48  > $Lat) && ($Lat >= -56)) { $Letter_Designator = 'F' }
  elsif ((-56  > $Lat) && ($Lat >= -64)) { $Letter_Designator = 'E' }
  elsif ((-64  > $Lat) && ($Lat >= -72)) { $Letter_Designator = 'D' }
  elsif ((-72  > $Lat) && ($Lat >= -80)) { $Letter_Designator = 'C' }
  else { $Letter_Designator = 'Z' }; # An error flag: Latitude is outside UTM limits

  return $Letter_Designator;
}





#
# Converts UTM coordinates to lat/lon.  Equations from USGS Bulletin 1532.
# East longitudes are positive, West are negative. 
# North latitudes are positive, South are negative.
# Latitude and longitude are in decimal degrees. 
# Zone should look something like: "10U" before starting the conversion.
#
sub utm_to_lat_lon
{
  my $position = shift;		# Input is in the form of a "Coordinate" object

  my $PI = 3.14159265358979323846;
  my $rad_2_deg = 180.0 / $PI;
  my $k0 = 0.9996;

  # First get the datum used in the position from the object:
  my $datum = $position->datum();

  # Now find out the ellipsoid used by consulting the DatumTable:
  my $ellipsoid = DatumTable->ellipsoid( $datum );

  # Do a lookup in the $ellipsoid{} hash to find the Radius value.
  my $a = EllipsoidTable->equatorial_radius( $ellipsoid );

  # Do a lookup in the $ellipsoid{} hash to find the Ecc value.
  my $ecc_Squared = EllipsoidTable->first_ecc_squared( $ellipsoid );

  my $x = $position->easting();
  my $y = $position->northing();
  my $Zone_Letter = $position->zone();
  my $Zone_Number = $Zone_Letter;
  $Zone_Number =~ s/(\d+).*/$1/;	# Convert to an integer

  my $e1 = (1-sqrt(1-$ecc_Squared))/(1+sqrt(1-$ecc_Squared));

  $Zone_Letter =~ s/\d+(\w)/$1/;	# Convert UTM zone to just a letter
  #print "$Zone_Letter\n\n";

  my $Northern_Hemisphere;		# 1 for northern hemisphere, 0 for southern

  $x = $x - 500000.0;			# Remove 500,000 meter offset for longitude
  $y = $y;

  if(($Zone_Letter ge 'N'))
  {
    $Northern_Hemisphere = 1;		# 'N' or higher is in the northern hemisphere
    #print "Northern Hemisphere\n";
  }
  else
  {
    $Northern_Hemisphere = 0;		# Point is in southern hemisphere
    $y -= 10000000.0;			# Remove 10,000,000 meter offset used for southern hemisphere
    #print "Southern Hemisphere\n";
  }

  #print "Zone_Number: $Zone_Number\n";
  my $Long_Origin = ($Zone_Number - 1) * 6 - 180 + 3;	# +3 puts origin in middle of zone

  my $ecc_Prime_Squared = ($ecc_Squared)/(1 - $ecc_Squared);

  my $M = $y / $k0;
  my $mu = $M/($a * (1 - $ecc_Squared/4 - 3 * $ecc_Squared * $ecc_Squared/64 - 5 * $ecc_Squared * $ecc_Squared * $ecc_Squared/256));

  my $phi1_Rad = $mu + (3 * $e1/2 - 27 * $e1 * $e1 * $e1/32) * sin(2 * $mu) 
	+ (21 * $e1 * $e1/16 - 55 * $e1 * $e1 * $e1 * $e1/32) * sin(4 * $mu)
	+(151 * $e1 * $e1 * $e1/96) * sin(6 * $mu);

  my $phi1 = $phi1_Rad * $rad_2_deg;

  my $N1 = $a/sqrt(1 - $ecc_Squared * sin($phi1_Rad) * sin($phi1_Rad));
  my $T1 = tan($phi1_Rad) * tan($phi1_Rad);
  my $C1 = $ecc_Prime_Squared * cos($phi1_Rad) * cos($phi1_Rad);

  my $R1 = $a * (1 - $ecc_Squared)/(1 - $ecc_Squared * sin($phi1_Rad) * sin($phi1_Rad)**1.5);

  my $D = $x/($N1 * $k0);

  $Lat = $phi1_Rad - ($N1 * tan($phi1_Rad)/$R1) * ($D * $D/2 - (5 + 3 * $T1 + 10 * $C1 - 4 * $C1 * $C1 - 9 * $ecc_Prime_Squared) * $D * $D * $D * $D/24
	+(61 + 90 * $T1 + 298 * $C1 + 45 * $T1 * $T1 - 252 * $ecc_Prime_Squared - 3 * $C1 * $C1) * $D * $D * $D * $D * $D * $D/720);

  $Lat = $Lat * $rad_2_deg;

  $Long = ($D - (1 + 2 * $T1 + $C1) * $D * $D * $D/6 + (5 - 2 * $C1 + 28 * $T1 - 3 * $C1 * $C1 + 8 * $ecc_Prime_Squared + 24 * $T1 * $T1)
	*$D * $D * $D * $D * $D/120)/cos($phi1_Rad);

  $Long = $Long_Origin + $Long * $rad_2_deg;

  # Write the results back to our position object
  $position->latitude( $Lat );
  $position->longitude( $Long );

  return(1);
}





#
# Function to convert latitude and longitude in decimal degrees between WGS 84 and
# another datum. The arguments to this function are a Coordinate object ($position)
# and a direction flag ($from_WGS84).  The Datum you're translating to/from is
# stored in the Datum() field of the object (the other Datum is by default WGS 84).
#
sub _datum_shift
{
  my $position = shift;				# This is our position object

  my $from_WGS84 = shift;			# If 1, then we're converting from WGS 84
						# to the datum listed in the position object
						# If 0, we're going the other way.

  my $PI = 3.14159265358979323846;
  my $rad_2_deg = 180.0 / $PI;
  my $deg_2_rad = $PI / 180;

  #printf("%s\n", $position->datum() );

  my $datum = $position->datum();		# Snag the parameters from the position object
						# (Get the "other" datum).

  my $latitude = $position->latitude();
  my $longitude = $position->longitude();

  # Now find out the ellipsoid used by consulting the DatumTable:
  my $ellipsoid = DatumTable->ellipsoid( $datum );

  my $dx = DatumTable->dx($datum);		# Grab the parameters for the "other" datum.
  my $dy = DatumTable->dy($datum);		# Offsets between "other" datum and WGS84
  my $dz = DatumTable->dz($datum);		# ellipsoid centers.

  #print "$dx\t$dy\t$dz\n";

	
  my $WGS_a    = EllipsoidTable->equatorial_radius("WGS 84");	# WGS 84 semimajor axis
  my $WGS_inv_f = EllipsoidTable->inverse_flattening("WGS 84");	# WGS 84 1/f

  my $phi = $latitude * $deg_2_rad;
  my $lambda = $longitude * $deg_2_rad;
  my ($a0, $b0, $es0, $f0);			# Reference ellipsoid of input data
  my ($a1, $b1, $es1, $f1);			# Reference ellipsoid of output data
  my $psi;					# geocentric latitude 
  my ($x, $y, $z);				# 3D coordinates with respect to original datum 
  my $psi1;					# transformed geocentric latitude 

  if ($datum eq "WGS 84")			# do nothing if current datum is WGS 84
  {
    return(1);
  }
		
  if ($from_WGS84)				# convert from WGS 84 to new datum 
  {
    $a0 = $WGS_a;				# WGS 84 semimajor axis 
    $f0 = 1.0 / $WGS_inv_f;			# WGS 84 flattening 
    $a1 = EllipsoidTable->equatorial_radius($ellipsoid);
    $f1 = 1.0 / EllipsoidTable->inverse_flattening($ellipsoid);
  }
  else						# convert from old datum to WGS 84 
  {
    $a0 = EllipsoidTable->equatorial_radius($ellipsoid);	# semimajor axis 
    $f0 = 1.0 / EllipsoidTable->inverse_flattening($ellipsoid);	# flattening 
    $a1 = $WGS_a;				# WGS 84 semimajor axis 
    $f1 = 1 / $WGS_inv_f;				# WGS 84 flattening 
    $dx = -$dx;
    $dy = -$dy;
    $dz = -$dz;
  }

  $b0 = $a0 * (1 - $f0); 			# semiminor axis for input datum 
  $es0 = 2 * $f0 - $f0*$f0;			# eccentricity^2 

  $b1 = $a1 * (1 - $f1);			# semiminor axis for output datum 
  $es1 = 2 * $f1 - $f1*$f1;			# eccentricity^2 

  # Convert geodedic latitude to geocentric latitude, psi 
	
  if ($latitude == 0.0 || $latitude == 90.0 || $latitude == -90.0)
  {
    $psi = $phi;
  }
  else
  {
    $psi = atan((1 - $es0) * tan($phi));
  }

  # Calculate x and y axis coordinates with respect to the original ellipsoid 
	
  if ($longitude == 90.0 || $longitude == -90.0)
  {
    $x = 0.0;
    $y = abs($a0 * $b0 / sqrt($b0*$b0 + $a0*$a0* ( tan($psi)**2.0) ) );
  }
  else
  {
    $x = abs(($a0 * $b0) /
        sqrt( (1 + (tan($lambda)**2.0) ) * ($b0*$b0 + $a0*$a0 * (tan($psi)**2.0) ) ) );

    $y = abs($x * tan($lambda));
  }
	
  if ($longitude < -90.0 || $longitude > 90.0)
  {
    $x = -$x;
  }
  if ($longitude < 0.0)
  {
    $y = -$y;
  }
	
  # Calculate z axis coordinate with respect to the original ellipsoid 
	
  if ($latitude == 90.0)
  {
    $z = $b0;
  }
  elsif ($latitude == -90.0)
  {
    $z = -$b0;
  }
  else
  {
    $z = tan($psi) * sqrt( ($a0*$a0 * $b0*$b0) / ($b0*$b0 + $a0*$a0 * (tan($psi)**2.0) ) );
  }

  # Calculate the geocentric latitude with respect to the new ellipsoid 

  $psi1 = atan(($z - $dz) / sqrt(($x - $dx)*($x - $dx) + ($y - $dy)*($y - $dy)));
	
  # Convert to geocentric latitude and save return value 

  $latitude = atan(tan($psi1) / (1 - $es1)) * $rad_2_deg;

  # Calculate the longitude with respect to the new ellipsoid 
	
  $longitude = atan(($y - $dy) / ($x - $dx)) * $rad_2_deg;

  # Correct the resultant for negative x values 
	
  if ($x-$dx < 0.0)
  {
    if ($y-$dy > 0.0)
    {
      $longitude = 180.0 + $longitude;
    }
    else
    {
      $longitude = -180.0 + $longitude;
    }
  }

  # Write the results back to our position object
  $position->latitude($latitude);
  $position->longitude($longitude);

  if (! $from_WGS84)			# If we're converting to WGS 84 datum
  {
    $position->datum( "WGS 84" );	# Change the datum to correspond to
					# the new data in the object
  }

  #print "New one:  $latitude, $longitude\n";

  return(1);
}





#
# Method to convert latitude and longitude in decimal degrees from another
# datum to WGS 84.  The only argument to this method is a Coordinate object
# with the lat/lon/datum fields filled in.
#
# This method returns a NEW Coordinate object with lat/lon/datum fields
# filled in and the other fields empty.  The original object is not changed.
#
sub datum_shift_to_wgs84
{
  my $old_position = shift;	# This is our original position object

  my $to_WGS84 = 0;

  # Create a new object and fill in a few fields
  my $new_position = Coordinate->new(   $old_position->latitude(),
					$old_position->longitude(),
					$old_position->datum() );

  &_datum_shift( $new_position, $to_WGS84 );

  return( $new_position );	# Return the datum-shifted object
}





#
# Method to convert latitude and longitude in decimal degrees from WGS 84 to
# another datum.  The arguments to this method are a Coordinate object
# with the lat/lon/datum fields filled in (the datum field must be "WGS 84"),
# AND a datum (string) to shift to.
#
# This method returns a NEW Coordinate object with lat/lon/datum fields
# filled in.  The original object is not changed.
#
sub datum_shift_from_wgs84_to
{
  my $old_position = shift;			# This is our original position object
  my $new_datum = shift;			# The datum we wish to shift to

  my $from_WGS84 = 1;

  # Create a new object and fill in a few fields
  my $new_position = Coordinate->new(	$old_position->latitude(),
					$old_position->longitude(),
					$new_datum );

  &_datum_shift( $new_position, $from_WGS84 );

  return( $new_position );	# Return the datum-shifted object
}





#
# Degrees and Decimal Minutes
#
# Fills in the Formatted_latitude and Formatted_longitude variables
# in the object.
#
sub degrees_minutes
{
  my $self = shift;						# Get the object to work on

  $temp = CoordinateFormat->new();				# Create temporary object

  $temp->raw( $self->latitude() );				# Fill in with raw data
  $self->formatted_latitude ( $temp->degrees_minutes() );	# Process it & fill in our finished string

  $temp->raw( $self->longitude() );				# Fill in with raw data
  $self->formatted_longitude( $temp->degrees_minutes() );	# process it & fill in our finished string

  return(1);
}





#
# Degrees/Minutes/Seconds
#
# Fills in the Formatted_latitude and Formatted_longitude variables
# in the object.
#
sub degrees_minutes_seconds
{
  my $self = shift;							# Get the object to work on

  $temp = CoordinateFormat->new();					# Create the temporary object

  $temp->raw( $self->latitude() );					# Fill in with raw data
  $self->formatted_latitude ( $temp->degrees_minutes_seconds() );	# Process it & fill in our finished string

  $temp->raw( $self->longitude() );					# Fill in with raw data
  $self->formatted_longitude( $temp->degrees_minutes_seconds() );	# Process it & fill in our finished string

  return(1);
}





#
# Decimal Degrees
#
# Fills in the Formatted_latitude and Formatted_longitude variables
# in the object.
#
sub decimal_degrees
{
  my $self = shift;						# Get the object to work on

  $temp = CoordinateFormat->new();				# Create the temporary object

  $temp->raw( $self->latitude() );				# Fill in with raw data
  $self->formatted_latitude ( $temp->decimal_degrees() );	# Process it & fill in our finished string

  $temp->raw( $self->longitude() );				# Fill in with raw data
  $self->formatted_longitude( $temp->decimal_degrees() );	# Process it & fill in our finished string

  return(1);
}


#------------------------------------------------------------------------------------------------


#
# Auto-run code
#
# This code will get run automatically whenever this file gets "required"
# or "used" in another Perl program.
#

  
1;		# Needed to prevent exception when loading this module


