Converted from MIT licensed: github.com/chrisveness/geodesy/blob/master/mgrs.js Original author: Chris Veness chrisv@movable-type.co.uk
Note: Because this is a conversion, and performing important math operations I frankly don’t personally grok, I’ve retained the original author’s Greek variable naming conventions where possible. This is both to avoid confusion when comparing to the original code (and formulas), and also: not even sure what I’d name these variables otherwise.
The Universal Transverse Mercator (UTM
) system is a 2-dimensional Cartesian coordinate system providing locations on the surface of the Earth.
UTM
is a set of 60 transverse Mercator projections, normally based on the WGS-84 ellipsoid. Within each zone, coordinates are represented as eastings and northings, measures in metres; e.g. ‘31 N 448251 5411932’.
This method based on Karney 2011 ‘Transverse Mercator with an accuracy of a few nanometers’, building on Krüger 1912 ‘Konforme Abbildung des Erdellipsoids in der Ebene’.
Ref: arxiv.org/pdf/1002.1417.pdf
- FALSE_EASTING
Easting shift value.
- FALSE_NORTHING
Northing shift value.
- MATCHER
A valid UTM
string matcher.
- UTM_SCALE
Scaling based on the central meridian
- convergence R
Meridian convergence (bearing of grid north clockwise from true north), in degrees
- easting R
Easting in metres from false easting (-500km from central meridian)
- hemisphere R
N for northern hemisphere, S for southern hemisphere
- northing R
Northing in metres from equator (N) or from false northing -10,000km (S)
- scale R
Grid scale factor
- zone R
UTM
6° longitudinal zone (1..60 covering 180°W..180°E)
Convert a latitude and longitude position to a Universal Transverse Mercator (UTM
) object.
def self::from_position( pos )
zone = ( (pos.long + 180).to_f / 6 ).floor + 1
λ0 = ( ( zone - 1 ) * 6 - 180 + 3 ).to_radians
band = MGRS_LAT_BANDS[ ( pos.lat.to_f / 8 + 10 ).floor ]
adjust_inc = -> { zone += 1 && λ0 += 6.to_radians }
adjust_dec = -> { zone -= 1 && λ0 -= 6.to_radians }
if zone == 31 && band == 'V' && long >= 3
adjust_inc.call
end
if band == 'X'
if zone == 32
long >= 9 ? adjust_inc.call : adjust_dec.call
end
if zone == 34
long >= 21 ? adjust_inc.call : adjust_dec.call
end
if zone == 36
long >= 33? adjust_inc.call : adjust_dec.call
end
end
φ = pos.lat.to_radians
λ = pos.long.to_radians - λ0
a, f = WGS84.values_at( :a, :f )
e = Math.sqrt( f * ( 2 - f ) )
n = f / ( 2 - f )
n2 = n*n
n3 = n*n2
n4 = n*n3
n5 = n*n4
n6 = n*n5
cosλ = Math.cos( λ )
sinλ = Math.sin( λ )
tanλ = Math.tan( λ )
τ = Math.tan( φ )
σ = Math.sinh( e * Math.atanh( e*τ / Math.sqrt( 1+τ*τ ) ) )
τʹ = τ * Math.sqrt(1 + σ * σ) - σ * Math.sqrt( 1 + τ * τ)
ξʹ = Math.atan2( τʹ, cosλ )
ηʹ = Math.asinh( sinλ / Math.sqrt( τʹ * τʹ + cosλ * cosλ) )
a_ = a / (1 + n) * (1 + 1.0/4*n2 + 1.0/64*n4 + 1.0/256*n6)
α = [ nil,
1.0/2*n - 2.0/3*n2 + 5.0/16*n3 + 41.0/180*n4 - 127.0/288*n5 + 7891.0/37800*n6,
13.0/48*n2 - 3.0/5*n3 + 557.0/1440*n4 + 281.0/630*n5 - 1983433.0/1935360*n6,
61.0/240*n3 - 103.0/140*n4 + 15061.0/26880*n5 + 167603.0/181440*n6,
49561.0/161280*n4 - 179.0/168*n5 + 6601661.0/7257600*n6,
34729.0/80640*n5 - 3418889.0/1995840*n6,
212378941.0/319334400*n6
]
ξ = ξʹ
(1..6).each {|j| ξ += α[j] * Math.sin(2*j*ξʹ) * Math.cosh(2*j*ηʹ) }
η = ηʹ
(1..6).each {|j| η += α[j] * Math.cos(2*j*ξʹ) * Math.sinh(2*j*ηʹ) }
x = UTM_SCALE * a_ * η
y = UTM_SCALE * a_ * ξ
pʹ = 1
(1..6).each {|j| pʹ += 2*j*α[j] * Math.cos(2*j*ξʹ) * Math.cosh(2*j*ηʹ) }
qʹ = 0
(1..6).each {|j| qʹ += 2*j*α[j] * Math.sin(2*j*ξʹ) * Math.sinh(2*j*ηʹ) }
γʹ = Math.atan( τʹ / Math.sqrt(1+τʹ*τʹ)*tanλ )
γʺ = Math.atan2( qʹ, pʹ )
γ = γʹ + γʺ
sinφ = Math.sin( φ )
kʹ = Math.sqrt(1 - e*e*sinφ*sinφ) * Math.sqrt(1 + τ*τ) / Math.sqrt(τʹ*τʹ + cosλ*cosλ)
kʺ = a_ / a * Math.sqrt(pʹ*pʹ + qʹ*qʹ)
k = UTM_SCALE * kʹ * kʺ
x = x + FALSE_EASTING
y = y + FALSE_NORTHING if y < 0
x = x.round
y = y.round
convergence = γ.to_degrees.round( 9 )
scale = k.round( 12 )
h = pos.lat >= 0 ? 'N' : 'S'
return new(
convergence,
scale,
zone: zone,
hemisphere: h,
easting: x,
northing: y,
)
end
new( convergence=nil, scale=UTM_SCALE, zone:, hemisphere:, easting:, northing: )
Instance a new Utm object under the WGS84 datum.
convergence: Meridian convergence (bearing of grid north clockwise from true north), in degrees scale: Grid scale factor zone: UTM
6° longitudinal zone (1..60 covering 180°W..180°E) hemisphere: N for northern hemisphere, S for southern hemisphere easting: Easting in metres from false easting (-500km from central meridian) northing: Northing in metres from equator (N) or from false northing -10,000km (S)
def initialize( convergence=nil, scale=UTM_SCALE, zone:, hemisphere:, easting:, northing: )
@convergence = convergence
@scale = scale
@zone = zone
@hemisphere = hemisphere
@easting = easting
@northing = northing
raise RangeError, "Invalid zone %p" % [ zone ] if zone <= 1 || zone >= 60
raise RangeError, "Invalid hemisphere, must be 'N' or 'S'" unless hemisphere =~ /^[NS]$/
end
Parse a string to a UTM
object.
def self::parse( utm )
match = utm.match( MATCHER ) or raise ArgumentError, "Invalid UTM: %p" % [ utm ]
return new(
zone: match[ :zone ].to_i,
hemisphere: match[ :hemisphere ],
easting: match[ :easting ].to_i,
northing: match[ :northing ].to_i
)
end
Basic equality check.
def ==( other )
return self.zone == other.zone &&
self.hemisphere == other.hemisphere &&
self.easting == other.easting &&
self.northing == other.northing
end
Converts UTM
zone/easting/northing coordinate to a latitude/longitude position.
Implements Karney’s method, using Krüger series to order n⁶, giving results accurate to 5nm for distances up to 3900km from the central meridian.
def to_position
x = self.easting - FALSE_EASTING
y = self.hemisphere == 'S' ? self.northing - FALSE_NORTHING : self.northing
a, f = WGS84.values_at( :a, :f )
e = Math.sqrt( f * ( 2 - f ) )
n = f / (2 - f)
n2 = n*n
n3 = n*n2
n4 = n*n3
n5 = n*n4
n6 = n*n5
a_ = a/(1+n) * (1 + 1.0/4*n2 + 1.0/64*n4 + 1.0/256*n6)
η = x / ( UTM_SCALE * a_ )
ξ = y / ( UTM_SCALE * a_ )
β = [ nil,
1.0/2*n - 2.0/3*n2 + 37.0/96*n3 - 1.0/360*n4 - 81.0/512*n5 + 96199.0/604800*n6,
1.0/48*n2 + 1.0/15*n3 - 437.0/1440*n4 + 46.0/105*n5 - 1118711.0/3870720*n6,
17.0/480*n3 - 37.0/840*n4 - 209.0/4480*n5 + 5569.0/90720*n6,
4397.0/161280*n4 - 11.0/504*n5 - 830251.0/7257600*n6,
4583.0/161280*n5 - 108847.0/3991680*n6,
20648693.0/638668800*n6
]
ξʹ = ξ
(1..6).each {|j| ξʹ -= β[j] * Math.sin(2*j*ξ) * Math.cosh(2*j*η) }
ηʹ = η
(1..6).each {|j| ηʹ -= β[j] * Math.cos(2*j*ξ) * Math.sinh(2*j*η) }
sinhηʹ = Math.sinh(ηʹ)
sinξʹ = Math.sin(ξʹ)
cosξʹ = Math.cos(ξʹ)
τʹ = sinξʹ / Math.sqrt(sinhηʹ*sinhηʹ + cosξʹ*cosξʹ)
δτi = nil
τi = τʹ
while δτi.nil? || δτi.abs > 1e-12
σi = Math.sinh( e*Math.atanh( e*τi / Math.sqrt(1+τi*τi) ) )
τiʹ = τi * Math.sqrt(1+σi*σi) - σi * Math.sqrt(1+τi*τi)
δτi = (τʹ - τiʹ) / Math.sqrt(1+τiʹ*τiʹ) * (1 + (1-e*e)*τi*τi) / ((1-e*e)*Math.sqrt(1+τi*τi))
τi += δτi;
end
τ = τi
φ = Math.atan(τ)
λ = Math.atan2(sinhηʹ, cosξʹ)
λ0 = ( (self.zone - 1) * 6 - 180 + 3 ).to_radians
λ += λ0
lat = φ.to_degrees.round(14)
long = λ.to_degrees.round(14)
return Ravn::Geo::Position.new( lat, long )
end
Return the standard UTM
representation.
def to_s
return "%02d %s %d %d" % [
self.zone,
self.hemisphere,
self.easting,
self.northing
]
end