The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
#------------------------------------------------------------------------------
# File:         gps2utm.config
#
# Description:  Generate UTM tags from GPS information
#
# Requires:     ExifTool version 7.00 or later
#
# Notes:        Uses GPSMapDatum, GPSLatitude and GPSLongitude to generate
#               UTMCoordinates, UTMZone, UTMEasting and UTMNorthing.  If
#               GPSMapDatum is not available then "WGS84" is assumed.
#
# Example:      > exiftool -config gps2utm.config "-utm*" t/images/GPS.jpg
#               UTM Coordinates                 : 30U 569475.596m E 6094180.754m N
#               UTM Easting                     : 569475.595558165
#               UTM Northing                    : 6094180.75443061
#               UTM Zone                        : 30U
#
# Caveats:      When used to convert EXIF GPS coordinates, the reference
#               direction tags (GPSLatitudeRef/GPSLongitudeRef) must exist or
#               the calculated UTM coordinates may be in the wrong hemisphere
#
# Revisions:    2016/03/08 - Bryan K. Williams (aka StarGeek) Created
#               2016/03/09 - PH removed library dependency and re-organized
#------------------------------------------------------------------------------

my $deg2rad = 3.14159265358979 / 180;

sub tan($)
{
    return sin($_[0]) / cos($_[0]);
}

#===============================================================================
# the following code is by Graham Crookham:
# http://search.cpan.org/~grahamc/Geo-Coordinates-UTM/ (version 0.11)

# remove all markup from an ellipsoid name, to increase the chance
# that a match is found.
sub _cleanup_name($)
{   my $copy = lc(shift);
    for($copy)
    {   s/\([^)]+\)//g;   # remove text between parantheses
        s/[\s-]//g;      # no blanks or dashes
    }
    $copy;
}

# Ellipsoid array (name,equatorial radius,square of eccentricity)
# Same data also as hash with key eq name (in variations)

my (@Ellipsoid, %Ellipsoid);

BEGIN {  # Initialize this before other modules get a chance
   @Ellipsoid =
    ( [ "Airy", 6377563, 0.00667054]
    , [ "Australian National", 6378160, 0.006694542]
    , [ "Bessel 1841", 6377397, 0.006674372]
    , [ "Bessel 1841 Nambia", 6377484, 0.006674372]
    , [ "Clarke 1866", 6378206, 0.006768658]
    , [ "Clarke 1880", 6378249, 0.006803511]
    , [ "Everest 1830 India", 6377276, 0.006637847]
    , [ "Fischer 1960 Mercury", 6378166, 0.006693422]
    , [ "Fischer 1968", 6378150, 0.006693422]
    , [ "GRS 1967", 6378160, 0.006694605]
    , [ "GRS 1980", 6378137, 0.00669438]
    , [ "Helmert 1906", 6378200, 0.006693422]
    , [ "Hough", 6378270, 0.00672267]
    , [ "International", 6378388, 0.00672267]
    , [ "Krassovsky", 6378245, 0.006693422]
    , [ "Modified Airy", 6377340, 0.00667054]
    , [ "Modified Everest", 6377304, 0.006637847]
    , [ "Modified Fischer 1960", 6378155, 0.006693422]
    , [ "South American 1969", 6378160, 0.006694542]
    , [ "WGS 60", 6378165, 0.006693422]
    , [ "WGS 66", 6378145, 0.006694542]
    , [ "WGS-72", 6378135, 0.006694318]
    , [ "WGS-84", 6378137, 0.00669438 ]
    , [ "Everest 1830 Malaysia", 6377299, 0.006637847]
    , [ "Everest 1956 India", 6377301, 0.006637847]
    , [ "Everest 1964 Malaysia and Singapore", 6377304, 0.006637847]
    , [ "Everest 1969 Malaysia", 6377296, 0.006637847]
    , [ "Everest Pakistan", 6377296, 0.006637534]
    , [ "Indonesian 1974", 6378160, 0.006694609]
    , [ "Arc 1950", 6378249.145,0.006803481]
    , [ "NAD 27",6378206.4,0.006768658]
    , [ "NAD 83",6378137,0.006694384]
    );

# calc ecc  as
# a = semi major axis
# b = semi minor axis
# e^2 = (a^2-b^2)/a^2
# For clarke 1880 (Arc1950) a=6378249.145 b=6356514.966398753
# e^2 (40682062155693.23 - 40405282518051.34) / 40682062155693.23
# e^2 = 0.0068034810178165

  foreach my $el (@Ellipsoid)
  {   my ($name, $eqrad, $eccsq) = @$el;
      $Ellipsoid{$name} = $el;
      $Ellipsoid{_cleanup_name $name} = $el;
  }
}

# Returns "official" name, equator radius and square eccentricity
# The specified name can be numeric (for compatibility reasons) or
# a more-or-less exact name
# Examples:   my($name, $r, $sqecc) = ellipsoid_info 'wgs84';
#             my($name, $r, $sqecc) = ellipsoid_info 'WGS 84';
#             my($name, $r, $sqecc) = ellipsoid_info 'WGS-84';
#             my($name, $r, $sqecc) = ellipsoid_info 'WGS-84 (new specs)';
#             my($name, $r, $sqecc) = ellipsoid_info 22;

sub ellipsoid_info($)
{   my $id = shift;

    my $el = $id !~ m/\D/
           ? $Ellipsoid[$id-1]   # old system counted from 1
           : $Ellipsoid{$id} || $Ellipsoid{_cleanup_name $id};

    defined $el ? @$el : ();
}

# Expects Ellipsoid Number or name, Latitude, Longitude
# (Latitude and Longitude in decimal degrees)
# Returns UTM Zone, UTM Easting, UTM Northing

sub latlon_to_utm(@)
{   my ($ellips, $latitude, $longitude) = @_;

    die("Longitude value ($longitude) invalid\n")
        if $longitude < -180 || $longitude > 180;

    my $long2 = $longitude - int(($longitude + 180)/360) * 360;
    my $zone  = _latlon_zone_number($latitude, $long2);

    _latlon_to_utm($ellips || 'WGS84', $zone, $latitude, $long2);
}

sub _latlon_zone_number
{   my  ($latitude, $long2) = @_;

    my $zone = int( ($long2 + 180)/6) + 1;
    if($latitude >= 56.0 && $latitude < 64.0 && $long2 >= 3.0 && $long2 < 12.0)
    {   $zone = 32;
    }
    if($latitude >= 72.0 && $latitude < 84.0) {
        $zone = ($long2 >=  0.0 && $long2 <  9.0) ? 31
             : ($long2 >=  9.0 && $long2 < 21.0) ? 33
             : ($long2 >= 21.0 && $long2 < 33.0) ? 35
                 : ($long2 >= 33.0 && $long2 < 42.0) ? 37
         :                                     $zone;
    }
    return $zone;
}

sub _latlon_to_utm
{   my ($ellips, $zone, $latitude, $long2) = @_;

    my ($name, $radius, $eccentricity) = ellipsoid_info $ellips
        or die("Ellipsoid value ($ellips) invalid\n");

    my $lat_radian  = $deg2rad * $latitude;
    my $long_radian = $deg2rad * $long2;

    my $k0          = 0.9996;   # scale

    my $longorigin       = ($zone - 1)*6 - 180 + 3;
    my $longoriginradian = $deg2rad * $longorigin;
    my $eccentprime      = $eccentricity/(1-$eccentricity);

    my $N = $radius / sqrt(1-$eccentricity * sin($lat_radian)*sin($lat_radian));
    my $T = tan($lat_radian) * tan($lat_radian);
    my $C = $eccentprime * cos($lat_radian)*cos($lat_radian);
    my $A = cos($lat_radian) * ($long_radian - $longoriginradian);
    my $M = $radius
            * ( ( 1 - $eccentricity/4 - 3 * $eccentricity * $eccentricity/64
                  - 5 * $eccentricity * $eccentricity * $eccentricity/256
                ) * $lat_radian
              - ( 3 * $eccentricity/8 + 3 * $eccentricity * $eccentricity/32
                  + 45 * $eccentricity * $eccentricity * $eccentricity/1024
                ) * sin(2 * $lat_radian)
              + ( 15 * $eccentricity * $eccentricity/256 +
                  45 * $eccentricity * $eccentricity * $eccentricity/1024
                ) * sin(4 * $lat_radian)
              - ( 35 * $eccentricity * $eccentricity * $eccentricity/3072
                ) * sin(6 * $lat_radian)
              );

    my $utm_easting = $k0*$N*($A+(1-$T+$C)*$A*$A*$A/6
                    + (5-18*$T+$T*$T+72*$C-58*$eccentprime)*$A*$A*$A*$A*$A/120)
                    + 500000.0;

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

    $utm_northing += 10000000.0 if $latitude < 0;

    my $utm_letter
      = ( 84 >= $latitude && $latitude >=  72) ? 'X'
      : ( 72 >  $latitude && $latitude >=  64) ? 'W'
      : ( 64 >  $latitude && $latitude >=  56) ? 'V'
      : ( 56 >  $latitude && $latitude >=  48) ? 'U'
      : ( 48 >  $latitude && $latitude >=  40) ? 'T'
      : ( 40 >  $latitude && $latitude >=  32) ? 'S'
      : ( 32 >  $latitude && $latitude >=  24) ? 'R'
      : ( 24 >  $latitude && $latitude >=  16) ? 'Q'
      : ( 16 >  $latitude && $latitude >=   8) ? 'P'
      : (  8 >  $latitude && $latitude >=   0) ? 'N'
      : (  0 >  $latitude && $latitude >=  -8) ? 'M'
      : ( -8 >  $latitude && $latitude >= -16) ? 'L'
      : (-16 >  $latitude && $latitude >= -24) ? 'K'
      : (-24 >  $latitude && $latitude >= -32) ? 'J'
      : (-32 >  $latitude && $latitude >= -40) ? 'H'
      : (-40 >  $latitude && $latitude >= -48) ? 'G'
      : (-48 >  $latitude && $latitude >= -56) ? 'F'
      : (-56 >  $latitude && $latitude >= -64) ? 'E'
      : (-64 >  $latitude && $latitude >= -72) ? 'D'
      : (-72 >  $latitude && $latitude >= -80) ? 'C'
      : die("Latitude ($latitude) out of UTM range\n");

    $zone .= $utm_letter;

    ($zone, $utm_easting, $utm_northing);
}
# End Graham Crookham code
#===============================================================================

%Image::ExifTool::UserDefined = (
    'Image::ExifTool::Composite' => {
        UTMCoordinates => {
            Desire => {
                0 => 'GPSMapDatum',
            },
            Require => {
                1 => 'GPSLatitude',
                2 => 'GPSLongitude',
            },
            ValueConv => 'join " ", latlon_to_utm(@val)',
            PrintConv => 'sprintf("%s %.3fm E %.3fm N", split(" ", $val))',
        },
        UTMZone => {
            Require => 'UTMCoordinates',
            ValueConv => 'my @a=split(" ",$val); $a[0]',
        },
        UTMEasting => {
            Require => 'UTMCoordinates',
            ValueConv => 'my @a=split(" ",$val); $a[1]',
        },
        UTMNorthing => {
            Require => 'UTMCoordinates',
            ValueConv => 'my @a=split(" ",$val); $a[2]',
        },
    },
);

#------------------------------------------------------------------------------
1;  #end