# https://raw.github.com/LambethCouncil/OSGB36_Converter/master/OSGB36.rb
module OSGB36

  extend self

  # Takes OSGB36 Easting/Northing coords 
  # and returns WGS84 Latitude and Longitude
  def en_to_ll(easting, northing)
    @OSGB36_ll = to_OSGB36(easting, northing)
    to_WGS84(@OSGB36_ll[:latitude], @OSGB36_ll[:longitude])
  end

  # Takes OSGB36 Easting/Northing coords and returns
  # OSGB36 Latitude and Longitude
  def to_OSGB36(easting, northing)
  
       @OSGB_F0  = 0.9996012717
       @N0       = -100000.0
       @E0       = 400000.0
       @phi0     = deg_to_rad(49.0)
       @lambda0  = deg_to_rad(-2.0)
       @a        = 6377563.396
       @b        = 6356256.909
       @eSquared = ((@a * @a) - (@b * @b)) / (@a * @a)
       @phi      = 0.0
       @lambda   = 0.0
       @E        = easting
       @N        = northing
       @n        = (@a - @b) / (@a + @b)
       @M        = 0.0
       @phiPrime = ((@N - @N0) / (@a * @OSGB_F0)) + @phi0
       
       begin
         
         @M =
           (@b * @OSGB_F0)\
             * (((1 + @n + ((5.0 / 4.0) * @n * @n) + ((5.0 / 4.0) * @n * @n * @n))\
               * (@phiPrime - @phi0))\
               - (((3 * @n) + (3 * @n * @n) + ((21.0 / 8.0) * @n * @n * @n))\
                 * Math.sin(@phiPrime - @phi0)\
                 * Math.cos(@phiPrime + @phi0))\
               + ((((15.0 / 8.0) * @n * @n) + ((15.0 / 8.0) * @n * @n * @n))\
                 * Math.sin(2.0 * (@phiPrime - @phi0))\
                 * Math.cos(2.0 * (@phiPrime + @phi0)))\
               - (((35.0 / 24.0) * @n * @n * @n)\
                 * Math.sin(3.0 * (@phiPrime - @phi0))\
                 * Math.cos(3.0 * (@phiPrime + @phi0))))
                 
         @phiPrime += (@N - @N0 - @M) / (@a * @OSGB_F0)
       
       end while ((@N - @N0 - @M) >= 0.001)
       
       @v = @a * @OSGB_F0 * ((1.0 - @eSquared * sin_pow_2(@phiPrime)) ** -0.5)
       @rho =
         @a\
           * @OSGB_F0\
           * (1.0 - @eSquared)\
           * ((1.0 - @eSquared * sin_pow_2(@phiPrime)) ** -1.5)
       @etaSquared = (@v / @rho) - 1.0
       @VII = Math.tan(@phiPrime) / (2 * @rho * @v)
       @VIII =
         (Math.tan(@phiPrime) / (24.0 * @rho * (@v ** 3.0)))\
           * (5.0\
             + (3.0 * tan_pow_2(@phiPrime))\
             + @etaSquared\
             - (9.0 * tan_pow_2(@phiPrime) * @etaSquared))
       @IX =
         (Math.tan(@phiPrime) / (720.0 * @rho * (@v ** 5.0)))\
           * (61.0\
             + (90.0 * tan_pow_2(@phiPrime))\
             + (45.0 * tan_pow_2(@phiPrime) * tan_pow_2(@phiPrime)))
       @X = sec(@phiPrime) / @v
       @XI =
         (sec(@phiPrime) / (6.0 * @v * @v * @v))\
           * ((@v / @rho) + (2 * tan_pow_2(@phiPrime)))
       @XII =
         (sec(@phiPrime) / (120.0 * (@v ** 5.0)))\
           * (5.0\
             + (28.0 * tan_pow_2(@phiPrime))\
             + (24.0 * tan_pow_2(@phiPrime) * tan_pow_2(@phiPrime)))
       @XIIA =
         (sec(@phiPrime) / (5040.0 * (@v ** 7.0)))\
           * (61.0\
             + (662.0 * tan_pow_2(@phiPrime))\
             + (1320.0 * tan_pow_2(@phiPrime) * tan_pow_2(@phiPrime))\
             + (720.0\
               * tan_pow_2(@phiPrime)\
               * tan_pow_2(@phiPrime)\
               * tan_pow_2(@phiPrime)))
       @phi =
         @phiPrime\
           - (@VII * ((@E - @E0) ** 2.0))\
           + (@VIII * ((@E - @E0) ** 4.0))\
           - (@IX * ((@E - @E0) ** 6.0))
       @lambda =
         @lambda0\
           + (@X * (@E - @E0))\
           - (@XI * ((@E - @E0) ** 3.0))\
           + (@XII * ((@E - @E0) ** 5.0))\
           - (@XIIA * ((@E - @E0) ** 7.0))

      { :latitude => rad_to_deg(@phi), :longitude => rad_to_deg(@lambda) }
       
    end
    
    # Takes OSGB36 Latitude and Longitude coords 
    # and returns WGS84 Latitude and Longitude
    def to_WGS84(latitude,longitude)
      
      @a        = 6377563.396
      @b        = 6356256.909
      @eSquared = ((@a * @a) - (@b * @b)) / (@a * @a)

      @phi = deg_to_rad(latitude)
      @lambda = deg_to_rad(longitude)
      @v = @a / (Math.sqrt(1 - @eSquared * sin_pow_2(@phi)))
      @H = 0
      @x = (@v + @H) * Math.cos(@phi) * Math.cos(@lambda)
      @y = (@v + @H) * Math.cos(@phi) * Math.sin(@lambda)
      @z = ((1 - @eSquared) * @v + @H) * Math.sin(@phi)

      @tx =        446.448
      @ty =       -124.157
      @tz =        542.060
      
      @s  =         -0.0000204894
      @rx = deg_to_rad( 0.00004172222)
      @ry = deg_to_rad( 0.00006861111)
      @rz = deg_to_rad( 0.00023391666)

      @xB = @tx + (@x * (1 + @s)) + (-@rx * @y)     + (@ry * @z)
      @yB = @ty + (@rz * @x)      + (@y * (1 + @s)) + (-@rx * @z)
      @zB = @tz + (-@ry * @x)     + (@rx * @y)      + (@z * (1 + @s))

      @a        = 6378137.000
      @b        = 6356752.3141
      @eSquared = ((@a * @a) - (@b * @b)) / (@a * @a)

      @lambdaB = rad_to_deg(Math.atan(@yB / @xB))
      @p = Math.sqrt((@xB * @xB) + (@yB * @yB))
      @phiN = Math.atan(@zB / (@p * (1 - @eSquared)))
      
      (1..10).each do |i|
        @v = @a / (Math.sqrt(1 - @eSquared * sin_pow_2(@phiN)))
        @phiN1 = Math.atan((@zB + (@eSquared * @v * Math.sin(@phiN))) / @p)
        @phiN = @phiN1
      end

      @phiB = rad_to_deg(@phiN)

      { :latitude => @phiB, :longitude => @lambdaB }
         
    end
    
    private # Some Math
    
    def deg_to_rad(degrees)
      degrees / 180.0 * Math::PI
    end

    def rad_to_deg(r)
      (r/Math::PI)*180
    end

    def sin_pow_2(x)
      Math.sin(x) * Math.sin(x)
    end

    def cos_pow_2(x)
      Math.cos(x) * Math.cos(x)
    end

    def tan_pow_2(x)
      Math.tan(x) * Math.tan(x)
    end

    def sec(x)
      1.0 / Math.cos(x)
    end
  
end
