Version 0 of Universal Transverse Mercator

Updated 2004-03-16 04:59:13

Keith Vetter 2004-04-15 : Universal Transverse Mercator (UTM) is another coordinate system for the earth that has some nice advantages over the more familiar latitude and longitude especially at large scales. The TerraServer online database of USGS topo maps and aerial photographs is based on UTM coordinates (see TkTopomap).

The basic idea behind UTM is that the world is divided into 60 north-south zones. Within each zone, a coordinate is measured in meters north from the equator--northing--and meters east from a central meridian--easting. For gory details see http://mac.usgs.gov/mac/isb/pubs/factsheets/fs07701.html or http://www.nps.gov/prwi/readutm.htm .

Below are two routines to convert from UTM into lat/lon and vice versa. It uses the WGS-84 datum but a table is provided if you want to use other ones. As usual a simple demo is also provided (warning, absolutely no error checking is done.)

====

 ##+##########################################################################
 #
 # ll2utm -- Converts latitude and longitude into Universal Transverse
 # Mercator (UTM) coordinates. Lots of fun math which I got off the
 # web.
 #
 proc ll2utm {latitude longitude} {
    if {$longitude > 0} {set longitude [expr {-1 * $longitude}]}
    set PI [expr {atan(1) * 4}]
    set K0 0.9996

    # WGS-84
    set er 6378137                              ;# EquatorialRadius
    set es2 0.00669438                          ;# EccentricitySquared
    set es4 [expr {$es2 * $es2}]
    set es6 [expr {$es2 * $es2 * $es2}]

    # Must be in the range -180 <= long < 180
    while {$longitude < -180} { set longitude [expr {$longitude + 360}]}
    while {$longitude >= 180}  { set longitude [expr {$longitude - 360}]}

    # Now convert
    set lat_rad [expr {$latitude * $PI / 180.0}]
    set long_rad [expr {$longitude * $PI / 180.0}]

    set zone [expr {int(($longitude + 180) / 6) + 1}]
    if {$latitude >= 56.0 && $latitude < 64.0 &&
        $longitude >= 3.0 && $longitude < 12.0} {
        $zone = 32
    }
    if { $latitude >= 72.0 && $latitude < 84.0 } {
        if { $longitude >= 0.0  && $longitude <  9.0 } {$zone = 31;}
        if { $longitude >= 9.0  && $longitude < 21.0 } {$zone = 33;}
        if { $longitude >= 21.0 && $longitude < 33.0 } {$zone = 35;}
        if { $longitude >= 33.0 && $longitude < 42.0 } {$zone = 37;}
    }
    # +3 puts origin in middle of zone
    set long_origin [expr {( $zone - 1 ) * 6 - 180 + 3}]
    set long_origin_rad [expr {$long_origin * $PI / 180.0}]
    set eccPrimeSquared [expr {$es2 / ( 1.0 - $es2 )}]
    set N [expr {$er / sqrt( 1.0 - $es2 * sin( $lat_rad ) * sin( $lat_rad ) )}]
    set T [expr {tan( $lat_rad ) * tan( $lat_rad )}]
    set C [expr {$eccPrimeSquared * cos( $lat_rad ) * cos( $lat_rad )}]
    set A [expr {cos( $lat_rad ) * ( $long_rad - $long_origin_rad )}]
    set M [expr { $er * ( \
     (1.0 - $es2 / 4 - 3 * $es4 / 64 - 5 * $es6 / 256) * $lat_rad \
     - (3 * $es2 / 8 + 3 * $es4 / 32 + 45 * $es6 / 1024) * sin(2 * $lat_rad) \
     + (15 * $es4 / 256 + 45 * $es6 / 1024 )             * sin(4 * $lat_rad) \
     - (35 * $es6 / 3072 )                               * sin(6 * $lat_rad) \
     )}]
    set easting [expr {$K0 * $N * ( $A + ( 1 - $T + $C ) * $A * $A * $A / 6 \
     + ( 5 - 18 * $T + $T * $T + 72 * $C - 58 * $eccPrimeSquared ) * \
     $A * $A * $A * $A * $A / 120 ) + 500000.0}]
    set northing [expr {$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 * $eccPrimeSquared ) * \
     $A * $A * $A * $A * $A * $A / 720 ) )}]

    if {$latitude < 0} {  ;# 1e7 meter offset for southern hemisphere
        set northing [expr {$northing + 10000000.0}]
    }

    set northing [expr {int($northing)}]
    set easting [expr {int($easting)}]
    if {$latitude > 84.0 || $latitude < -80.0} {
        set letter "Z"
    } else {
        set l [expr {int(($latitude + 80) / 8.0)}]
        set letter [string index "CDEFGHJKLMNPQRSTUVWXX" $l]
    }

    return [list $northing $easting $zone $letter]
 }
 ##+##########################################################################
 #
 # utm2utm -- Converts Universal Transverse Mercator (UTM) into
 # latitude and longitude coordinates. More fun math which I got off
 # the web.
 #
 proc utm2ll {northing easting zone {letter S}} {
    set PI [expr {atan(1) * 4}]
    set K0 0.9996

    # WGS-84
    set er 6378137                              ;# EquatorialRadius
    set es2 0.00669438                          ;# EccentricitySquared
    set es2x [expr {1.0 - $es2}]

    set x [expr {$easting - 500000.0}]
    set northernHemisphere [expr {$letter >= "N"}]
    set y [expr {$northing - ($northernHemisphere ? 0.0 : 10000000.0)}]
    set long_origin [expr {($zone - 1) * 6 - 180 + 3}] ;# +3 puts in middle
    set ep2 [expr {$es2 / $es2x}]
    set e1 [expr {(1.0 - sqrt($es2x)) / (1.0 + sqrt($es2x))}]
    set M [expr {$y / $K0}]
    set mu [expr {$M / ($er * (1.0 - $es2 /4.0 - 3 * $es2 * $es2 /64.0
                               - 5 * $es2 * $es2 * $es2 /256.0))}]
    set phi [expr {$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)}]
    set N1 [expr {$er / sqrt(1.0 - $es2 * sin($phi) * sin($phi))}]
    set T1 [expr {tan($phi) * tan($phi)}]
    set C1 [expr {$ep2 * cos($phi) * cos($phi)}]
    set R1 [expr {$er * $es2x / pow(1.0 - $es2 * sin($phi) * sin($phi), 1.5)}]
    set D [expr {$x / ($N1 * $K0)}]
    set latitude [expr {$phi - ($N1 * tan($phi) / $R1) 
  • ($D * $D / 2
                           - (5 + 3 * $T1 + 10 * $C1 - 4 * $C1 * $C1 - 9 * $ep2)
  • $D * $D * $D * $D / 24
                           + (61 + 90 * $T1 + 298 * $C1 + 45 * $T1 * $T1
                              - 252 * $ep2 - 3 * $C1 * $C1 )
  • $D * $D * $D * $D * $D * $D / 720)}]
    set latitude [expr {$latitude * 180.0 / $PI}]
    set longitude [expr {($D - (1 + 2 * $T1 + $C1)
  • $D * $D * $D / 6
                          + (5 - 2 * $C1 + 28 * $T1 - 3 * $C1 * $C1
                             + 8 * $ep2 + 24 * $T1 * $T1)
  • $D * $D * $D * $D * $D / 120)
                         / cos($phi)}]
    set longitude [expr {$long_origin + $longitude * 180.0 / $PI}]

    return [list $latitude $longitude]
 }

 ## Other datums which could be used here instead:
 ##
 ## Name                         EquatorialRadius        EccentricitySquared
 ## ----                         ----------------        -------------------
 ## 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                      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


 ################################################################
 #
 # DEMO code
 #
 package require Tk

 proc DoDisplay {} {

    labelframe .ll -text "Lat/Lon" -padx 5
    label .ll.lat -text Latitude:
    entry .ll.elat -textvariable C(lat)
    label .ll.lon -text Longitude:
    entry .ll.elon -textvariable C(lon)
    grid .ll.lat .ll.elat -sticky w
    grid .ll.lon .ll.elon -sticky w
    grid rowconfigure .ll 1000 -weight 1

    labelframe .utm -text UTM -padx 5
    label .utm.north -text Northing:
    entry .utm.enorth -textvariable C(north)
    label .utm.east -text Easting:
    entry .utm.eeast -textvariable C(east)
    label .utm.zone -text Zone:
    entry .utm.ezone -textvariable C(zone)
    label .utm.letter -text Letter:
    entry .utm.eletter -textvariable C(letter)

    grid .utm.north .utm.enorth -sticky w
    grid .utm.east .utm.eeast -sticky w
    grid .utm.zone .utm.ezone -sticky w
    grid .utm.letter .utm.eletter -sticky w

    frame .2
    button .2.utm -text "==>" -command [list Convert 1]
    button .2.ll -text "<==" -command [list Convert 0]
    grid .2.utm -pady 5
    grid .2.ll

    grid .ll .2 .utm -sticky ns -pady {5 10} -padx 5
    grid configure .2 -padx 20

    image create photo ::img::blank -width 1 -height 1
    button .about -image ::img::blank -highlightthickness 0 -command \
        [list tk_messageBox -message "UTM Converter\nby Keith Vetter\nMarch, 2004"]
    place .about -in . -relx 1 -rely 1 -anchor se
    focus .ll.elat
 }
 proc lat2dec {value} {
    set cnt [scan "$value 0 0 0" "%g %g %g" l1 l2 l3]
    set dec [expr {abs($l1) + $l2 / 60.0 + $l3 / 3600.0}]
    if {$l1 < 0} {set dec [expr {-1 * $dec}]}
    return $dec
 }    
 proc Convert {toUTM} {
    global C

    if {$toUTM} {
        foreach var {north east zone letter} { set C($var) "" }
        foreach var {lat lon} {
            set C($var) [string trim $C($var)]
            if {$C($var) eq ""} return
            set $var [lat2dec $C($var)]
        }
        set utm [ll2utm $lat $lon]
        foreach {C(north) C(east) C(zone) C(letter)} $utm break
    } else {
        foreach var {lat lon} { set C($var) "" } 
        foreach var {north east zone letter} { 
            set C($var) [string trim $C($var)]
            if {$C($var) eq "" && $var ne "letter"} return
        }
        set ll [utm2ll $C(north) $C(east) $C(zone) $C(letter)]
        foreach {C(lat) C(lon)} $ll break
    }
 }
 DoDisplay
 set C(lat) "40 4 4.5"
 set C(lon) [lat2dec "-82 31 12.6"]

====

Category Application