32

I'm writing a program which expects a number of lat/long points, and I convert them internally to UTM in order to do some calculations in metres.

The range of the lat/long points themselves is quite small -- about 200m x 200m. They can be relied on almost always to be within a single UTM zone (unless you get unlucky and are across the border of a zone).

However, the zone that the lat/longs are in is unrestricted. One day the program might be run for people in Australia (and oh, how many zones does even a single state lie across, and how much pain has that caused me already...), and another day for people in Mexico.

My question is -- is there a way to determine which zone a particular long/lat is in so that it may be fed into a conversion library (I currently use proj4 and also the R package rgdal).

My language is R, but the answer doesn't have to be -- maybe it's just a simple calculation, or maybe I can embed a system call to the proj exectuable.

cheers.

Kara
  • 6,115
  • 16
  • 50
  • 57
mathematical.coffee
  • 55,977
  • 11
  • 154
  • 194

6 Answers6

48

Edit: For (non-R) code that works for all non-polar areas on earth, see here or here.


Unless you are dealing with data from a couple of exceptional areas (Svalbard and parts of Norway), this is a simple enough calculation that you might as well just do it yourself in R. Here is Wikipedia's description of how longitude relates to UTM Zone number:

The UTM system divides the surface of Earth between 80°S and 84°N latitude into 60 zones, each 6° of longitude in width. Zone 1 covers longitude 180° to 174° W; zone numbering increases eastward to zone 60 that covers longitude 174 to 180 East.

So, assuming that in your data longitudes to the west of the Prime Meridian are encoded as running from -180 to 0 degrees, here's an R-code version of the above:

long2UTM <- function(long) {
    (floor((long + 180)/6) %% 60) + 1
}

# Trying it out for San Francisco, clearly in UTM Zone 10 
# in the figure in the Wikipedia article linked above
SFlong <- -122.4192
long2UTM(SFlong)
# [1] 10

That expression could obviously be simplified a bit, but I think in this form the logic underlying its construction is most clear. The %% 60 bit is in there just in case some of your longitudes are greater than 180 or less than -180.

Community
  • 1
  • 1
Josh O'Brien
  • 159,210
  • 26
  • 366
  • 455
  • Aha, this was the calculation I was after - I spend ages googling "how to calculate UTM zone from latitude/longitude" and didn't even think to check Wiki. cheers! – mathematical.coffee Feb 08 '12 at 07:04
  • Yeah, I went through just the same process not too long ago. Glad to be of assistance. By the way, if you'll be doing much with spatial data, the [R-sig-geo](https://stat.ethz.ch/mailman/listinfo/r-sig-geo) list serve is invaluable. I recently asked a question there, and got near immediate assistance from Roger Bivand himself, who gave me an answer that nobody but he and a few R-core members could have provided. Cheers! – Josh O'Brien Feb 08 '12 at 07:09
  • @ToolmakerSteve -- Thanks for the suggested correction (which was incorrectly rejected by the 3 of 5 SO editors who reviewed it). – Josh O'Brien Jan 28 '13 at 06:21
  • 1
    @JoshO'Brien The formula is to simple: it does not work for the both UTM Zone Exceptions in Norway and Svalbard – AlexWien Mar 18 '13 at 01:17
  • @AlexWien -- Thanks for pointing that out! I've edited to make note of those exceptions. Since you're 'around', would you mind taking a quick look at [this recent question of mine](http://r-sig-geo.2731867.n2.nabble.com/Checking-for-equivalence-of-PROJ4-strings-td7583035.html), and letting me know if you have any advice? – Josh O'Brien Mar 18 '13 at 02:08
  • @AlexWien - while its true that the official zone designations have exceptions, it is equally true that applying a zone to locations somewhat outside of its official range still works mathematically. Indeed, looking at those exceptions, they are purely a matter of convenience: they extend the use of some zones beyond the 6 degree range, to lessen the number of zones one needs to work with (especially as approach the north pole, where the zones are less wide). Using this simple formula won't cause any miscalculations, as long as the zone number is always provided. – ToolmakerSteve Jan 23 '19 at 21:41
  • Note for programmers in C-style languages (in which `%` "mod" doesn't match the mathematical definition; negative input yields negative output), `% 60` won't help "latitudes less than -180"; the result will still be a negative value. The best fix in such a language, if you need to handle an angle that might come in more negative than -180, is to write a "mathematically correct modulus" function and use it. [Examples here](https://stackoverflow.com/questions/1082917/mod-of-negative-number-is-melting-my-brain/1082938) – ToolmakerSteve Jan 23 '19 at 23:04
  • 2
    **Additional note:** To get the `EPSG` code for a northern hemisphere UTM, add `32600` to its value. For southern hemisphere UTMs, add `32700`. – Josh O'Brien Mar 29 '20 at 01:22
4

I made this function for me using the previous answers. Maybe it is useful to someone here =)

utmzone <- function(lon,lat) {
## Special Cases for Norway & Svalbard
if (lat > 55 & lat < 64 & lon > 2 & lon < 6){ 
    band <- 32
  } else {
if (lat > 71 & lon >= 6 & lon < 9){
    band <- 31
  } else {
if (lat > 71 & lon >= 9 & lon < 12){
    band <- 33
  } else {
if (lat > 71 & lon >= 18 & lon < 21){
    band <- 33
  } else {
if (lat > 71 & lon >= 21 & lon < 24){
    band <- 35
  } else {
if (lat > 71 & lon >= 30 & lon < 33){
    band <- 35
  } else {
## Rest of the world
if (lon >= -180 & lon <= 180){
    band <- (floor((lon + 180)/6) %% 60) + 1
  } else {
    band <- "something is wrong"
    }}}}}}}
return(band)
}

utmzone(-43,-22)
#[1] 23
Luiz Bondi
  • 41
  • 1
3

I don't know r-code but I suppose this PL/SQL code can help you with the exceptions:

   UTMZone := Trunc((lon - Zone0WestMeridian) / d);
    --Special Cases for Norway & Svalbard
    CASE 
    WHEN (lat > 55) AND (UTMZone = 31) AND (lat < 64) AND (lon >  2) THEN UTMZone := 32;
    WHEN (lat > 71) AND (UTMZone = 32) AND (lon <  9) THEN UTMZone := 31;
    WHEN (lat > 71) AND (UTMZone = 32) AND (lon >  8) THEN UTMZone := 33;
    WHEN (lat > 71) AND (UTMZone = 34) AND (lon < 21) THEN UTMZone := 33;
    WHEN (lat > 71) AND (UTMZone = 34) AND (lon > 20) THEN UTMZone := 35; 
    WHEN (lat > 71) AND (UTMZone = 36) AND (lon < 33) THEN UTMZone := 35;
    WHEN (lat > 71) AND (UTMZone = 36) AND (lon > 32) THEN UTMZone := 37;
    ELSE UTMZone := UTMZone;  
    END CASE;
wittrup
  • 1,535
  • 1
  • 13
  • 23
1

So I had this problem today, that I needed to find the UTM zone from lat/long for points all over the globe. The trouble is that there's all these curly edge cases like Svalbard, Norway, and the poles:UTM curly edge cases (shown in red on this map) which will catch you out if you assume it's all regular!

Here's my R function to find UTM zones from lat/long pairs, with tests at the end for all of the curly edge cases.

require(tidyverse)
require(purrr)
require(testthat)

find_one_utm_zone <- function(longitude, latitude) {

  # Special zones for Svalbard
  if (latitude >= 72.0 && latitude <= 84.0 ) {
    if (longitude >= 0.0  && longitude <  9.0)
      return("31X");
    if (longitude >= 9.0  && longitude < 21.0)
      return("33X")
    if (longitude >= 21.0 && longitude < 33.0)
      return("35X")
    if (longitude >= 33.0 && longitude < 42.0)
      return("37X")
  }
  # Special zones for Norway
  if (latitude >= 56.0 && latitude < 64.0 ) {
    if (longitude >= 0.0  && longitude <  3.0)
      return("31V");
    if (longitude >= 3.0  && longitude < 12.0)
      return("32V")
  }

  # North + South Poles

  if (latitude > 84.0){
    if ((longitude+180)%%360-180 < 0) {return("Y")}
    if ((longitude+180)%%360-180 > 0) {return("Z")}
  } else if (latitude < -80.0){
    if ((longitude+180)%%360-180 < 0) {return("A")}
    if ((longitude+180)%%360-180 > 0) {return("B")}
  }

  # Everything in the middle

  if ( (latitude>-80.0) && (latitude<=84.0) ){

    mid_zones <- LETTERS[c(3:8,10:14,16:24)] # C to X, skip I and O
    utm_letter <- mid_zones[ min(floor( (latitude + 80) / 8 )+1 , 20) ]
    utm_number <- (floor( (longitude + 180) / 6 ) %% 60) + 1 # modulo in case longitude is 0 to 360 instead of -180 to 180
    utm_zone <- paste0(utm_number, utm_letter)
    return(utm_zone)

  } else {
      stop("lat long not valid (or something else broke)")
    }
}
find_utm_zone <- function(lon, lat){
  purrr::map2_chr(.x = lon, .y = lat, .f = find_one_utm_zone)
}

Example of use

locs <-
  tibble(lon = c(-100,30,150, 4, 7, 22, 0, 12, -34, -20),
         lat = c(-45, 85, 12, 57, 81, 83, 5, -81, 85, 83),
         desired_utm_zone = c("14G","Z","56P", "32V" ,"31X","35X","31N", "B","Y","27X"))

locs2 <-
  locs %>%
  mutate(utm_zone = find_utm_zone(lon = lon,lat = lat))

Test that it worked:

testthat::expect_equal(locs2$utm_zone, locs2$desired_utm_zone)
Gordon McDonald
  • 269
  • 2
  • 12
0

Version for TypeScript, based on Luiz Bondis summary:

export function utmZoneFromLatLng(lat: number, lon: number) {
  // Special Cases for Norway & Svalbard
  if (lat > 55 && lat < 64 && lon > 2 && lon < 6) {
    return 32;
  }
  if (lat > 71 && lon >= 6 && lon < 9) {
    return 31;
  }
  if (lat > 71 && ((lon >= 9 && lon < 12) || (lon >= 18 && lon < 21))) {
    return 33;
  }
  if (lat > 71 && ((lon >= 21 && lon < 24) || (lon >= 30 && lon < 33))) {
    return 35;
  }
  // Rest of the world
  if (lon >= -180 && lon <= 180) {
    return (Math.floor((lon + 180) / 6) % 60) + 1;
  }

  throw new Error(`utmZoneFromLatLng: Cannot figure out UTM zone from give Lat: ${lat}, Lng: ${lon}`);
}
jayarjo
  • 16,124
  • 24
  • 94
  • 138
0

Python version:

def get_utm_fromLatLon(lat, lon):
    #Special Cases for Norway and Svalbard
    if (lat > 55 and lat < 64 and lon > 2 and lon < 6):
        return 32
    elif (lat > 71 and lon >= 6 and lon < 9):
        return 31
    elif (lat > 71 and ((lon >= 9 and lon < 12) or (lon >= 18 and lon < 21))):
        return 33
    elif (lat > 71 and ((lon >= 21 and lon < 24) or (lon >= 30 and lon < 33))):
        return 35
    # Rest of the world
    elif (lon >= -180 and lon <= 180):
        return (math.floor((lon + 180) / 6) % 60) + 1
    else:
        raise ValueError('Cannot figure out UTM zone from given Lat: {0}, Lon: {1}.'.format(lat, lon))

FelipeGTX
  • 91
  • 1
  • 1
  • 8