Class: GridRef

Inherits:
Object
  • Object
show all
Defined in:
lib/grid_ref.rb

Constant Summary collapse

OsTiles =
{
	:a => [0,4], :b => [1,4], :c => [2,4], :d => [3,4], :e => [4,4],
	:f => [0,3], :g => [1,3], :h => [2,3], :j => [3,3], :k => [4,3],
	:l => [0,2], :m => [1,2], :n => [2,2], :o => [3,2], :p => [4,2],
	:q => [0,1], :r => [1,1], :s => [2,1], :t => [3,1], :u => [4,1],
	:v => [0,0], :w => [1,0], :x => [2,0], :y => [3,0], :z => [4,0],
}
FalseOrigin =
[2,1]
SquareSize =

shorter grid ref = larger square.

[nil, 10000, 1000, 100, 10, 1]
@@iteration_ceiling =
1000
@@ellipsoids =
{
  :osgb36 => Ellipsoid.new(6377563.396, 6356256.910),
  :wgs84 => Ellipsoid.new(6378137.000, 6356752.3141),
  :ie65 => Ellipsoid.new(6377340.189, 6356034.447),
  :utm => Ellipsoid.new(6378388.000, 6356911.946)
}
@@projections =
{
  :gb => {:scale => 0.9996012717, :Phio => 49.to_radians, :Lambdao => -2.to_radians, :Eo => 400000, :No => -100000, :ellipsoid => :osgb36},
  :ie => {:scale => 1.000035, :Phio => 53.5.to_radians, :Lambdao => -8.to_radians, :Eo => 250000, :No => 250000, :ellipsoid => :ie65},
  :utm29 => {:scale => 0.9996, :Phio => 0, :Lambdao => -9.to_radians, :Eo => 500000, :No => 0, :ellipsoid => :utm},
  :utm30 => {:scale => 0.9996, :Phio => 0, :Lambdao => -3.to_radians, :Eo => 500000, :No => 0, :ellipsoid => :utm},
  :utm31 => {:scale => 0.9996, :Phio => 0, :Lambdao => 3.to_radians, :Eo => 500000, :No => 0, :ellipsoid => :utm}
}
@@helmerts =
{
  :wgs84 => { :tx => 446.448, :ty => -125.157, :tz => 542.060, :rx => 0.1502, :ry => 0.2470, :rz => 0.8421, :s => -20.4894 }
}
@@defaults =
{
  :projection => :gb,   # mercator projection of input gridref. Can be any projection name: usually :ie or :gb
  :precision => 6       # decimal places in the output lat/long
}

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(string, options = {}) ⇒ GridRef

Returns a new instance of GridRef.

Raises:

  • (ArgumentError)


55
56
57
58
59
60
61
62
63
# File 'lib/grid_ref.rb', line 55

def initialize(string, options={})
  raise ArgumentError, "invalid grid reference string '#{string}'." unless string.is_gridref?
  @gridref = string.upcase
  @options = @@defaults.merge(options)
  @projection = @@projections[@options[:projection]]
  @ellipsoid = @@ellipsoids[@projection[:ellipsoid]]
  @datum = @options[:datum]
  self
end

Instance Attribute Details

#datumObject

Returns the value of attribute datum.



48
49
50
# File 'lib/grid_ref.rb', line 48

def datum
  @datum
end

#ellipsoidObject

Returns the value of attribute ellipsoid.



48
49
50
# File 'lib/grid_ref.rb', line 48

def ellipsoid
  @ellipsoid
end

#gridrefObject

Returns the value of attribute gridref.



48
49
50
# File 'lib/grid_ref.rb', line 48

def gridref
  @gridref
end

#optionsObject

Returns the value of attribute options.



48
49
50
# File 'lib/grid_ref.rb', line 48

def options
  @options
end

#projectionObject

Returns the value of attribute projection.



48
49
50
# File 'lib/grid_ref.rb', line 48

def projection
  @projection
end

Instance Method Details

#coordinatesObject



114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
# File 'lib/grid_ref.rb', line 114

def coordinates
  unless @coordinates
    # variable names correspond roughly to symbols in the OS algorithm, lowercased:
    # n0 = northing of true origin 
    # e0 = easting of true origin 
    # f0 = scale factor on central meridian
    # phi0 = latitude of true origin 
    # lambda0 = longitude of true origin and central meridian.
    # e2 = eccentricity squared
    # a = length of polar axis of ellipsoid
    # b = length of equatorial axis of ellipsoid
    # ning & eing are the northings and eastings of the supplied gridref
    # phi and lambda are the discovered latitude and longitude
    
    ning = northing
    eing = easting

    n0 = projection[:No]
    e0 = projection[:Eo]
    phi0 = projection[:Phio]
    l0 = projection[:Lambdao]
    f0 = projection[:scale]
    
    a = ellipsoid.a
    b = ellipsoid.b
    e2 = ellipsoid.ecc
    
    # the rest is taken from the OS equations with help from CPAN's Geography::NationalGrid
    # and only enough understanding to transliterate it, and sometimes not even that.

    n = (a - b) / (a + b)
  	m = 0
    phi = phi0
  
    # iterate to within acceptable distance of solution
    
  	count = 0
  	while ((ning - n0 - m) >= 0.001) do
      raise RuntimeError "Demercatorising equation has not converged. Discrepancy after #{count} cycles is #{ning - n0 - m}" if count >= @@iteration_ceiling

  		phi = ((ning - n0 - m) / (a * f0)) + phi
      ma = (1 + n + (1.25 * n**2) + (1.25 * n**3)) * (phi - phi0)
      mb = ((3 * n) + (3 * n**2) + (2.625 * n**3)) * Math.sin(phi - phi0) * Math.cos(phi + phi0)
      mc = ((1.875 * n**2) + (1.875 * n**3)) * Math.sin(2 * (phi - phi0)) * Math.cos(2 * (phi + phi0))
      md = (35/24) * (n**3) * Math.sin(3 * (phi - phi0)) * Math.cos(3 * (phi + phi0))
      m = b * f0 * (ma - mb + mc - md)
  		count += 1
	  end
	  
	  # engage alphabet soup
	  
	  nu = a * f0 * ((1-(e2) * ((Math.sin(phi)**2))) ** -0.5)
  	rho = a * f0 * (1-(e2)) * ((1-(e2)*((Math.sin(phi)**2))) ** -1.5)
  	eta2 = (nu/rho - 1)
  	
  	# fire
  	
  	vii = Math.tan(phi) / (2 * rho * nu)
  	viii = (Math.tan(phi) / (24 * rho * (nu ** 3))) * (5 + (3 * (Math.tan(phi) ** 2)) + eta2 - 9 * eta2 * (Math.tan(phi) ** 2) )
  	ix = (Math.tan(phi) / (720 * rho * (nu ** 5))) * (61 + (90 * (Math.tan(phi) ** 2)) + (45 * (Math.tan(phi) ** 4)) )
  	x = sec(phi) / nu
  	xi = (sec(phi) / (6 * nu ** 3)) * ((nu/rho) + 2 * (Math.tan(phi) ** 2))
  	xii = (sec(phi) / (120 * nu ** 5)) * (5 + (28 * (Math.tan(phi) ** 2)) + (24 * (Math.tan(phi) ** 4)))
  	xiia = (sec(phi) / (5040 * nu ** 7)) * (61 + (662 * (Math.tan(phi) ** 2)) + (1320 * (Math.tan(phi) ** 4)) + (720 * (Math.tan(phi) ** 6)))

    d = eing-e0

    # all of which was just to populate these last two equations:
    
  	phi = phi - vii*(d**2) + viii*(d**4) - ix*(d**6)
    lambda = l0 + x*d - xi*(d**3) + xii*(d**5) - xiia*(d**7)

    # here coordinates are still in radians and osgb36

    @coordinates = helmerted(phi, lambda)
  end
  
  @coordinates
end

#digitsObject



69
70
71
# File 'lib/grid_ref.rb', line 69

def digits
  @digits ||= gridref[2,10]
end

#eastingObject



90
91
92
# File 'lib/grid_ref.rb', line 90

def easting
  @east ||= offsets[:e] + digits[0, resolution].to_i * SquareSize[resolution]
end

#helmerted(phi, lambda) ⇒ Object



194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
# File 'lib/grid_ref.rb', line 194

def helmerted(phi, lambda)
  return {:lat => phi, :lng => lambda} unless @datum && @datum != :osgb36
  target_datum = @@ellipsoids[@datum] 
  t = @@helmerts[@datum]
  
  if t && target_datum

    # convert polar to cartesian coordinates using osgb datum

    a = @@ellipsoids[:osgb36].a
    b = @@ellipsoids[:osgb36].b
    e2 = @@ellipsoids[:osgb36].ecc
    
    nu = a / (Math.sqrt(1 - e2 * Math.sin(phi)**2))
    h = 0

    x1 = (nu + h) * Math.cos(phi) * Math.cos(lambda)
    y1 = (nu + h) * Math.cos(phi) * Math.sin(lambda)
    z1 = ((1 - e2) * nu + h) * Math.sin(phi)
    
    # parameterise helmert transformation

    tx = t[:tx]
    ty = t[:ty]
    tz = t[:tz]
    rx = (t[:rx]/3600).to_radians
    ry = (t[:ry]/3600).to_radians
    rz = (t[:rz]/3600).to_radians
    s1 = t[:s]/1e6 + 1

    # apply helmert transformation
    
    xp = tx + x1*s1 - y1*rz + z1*ry
    yp = ty + x1*rz + y1*s1 - z1*rx
    zp = tz - x1*ry + y1*rx + z1*s1
          
    # convert back to polar coordinates using target datum

    a = target_datum.a
    b = target_datum.b
    e2 = target_datum.ecc
    precision = 4 / a
    
    p = Math.sqrt(xp**2 + yp**2)
    phi = Math.atan2(zp, p*(1-e2));
    phip = 2 * Math::PI

    count = 0
    while (phi-phip).abs > precision do
      raise RuntimeError "Helmert transformation has not converged. Discrepancy after #{count} cycles is #{phi-phip}" if count >= @@iteration_ceiling
      
      nu = a / Math.sqrt(1 - e2 * Math.sin(phi)**2)
      phip = phi
      phi = Math.atan2(zp + e2 * nu * Math.sin(phi), p)
      count += 1
    end 

    lambda = Math.atan2(yp, xp)

    {:lat => phi, :lng => lambda}
    
  else
    raise RuntimeError, "Missing ellipsoid or Helmert transformation for #{@datum}"
  end
end

#latObject



98
99
100
# File 'lib/grid_ref.rb', line 98

def lat
  coordinates[:lat].to_degrees.round(self.options[:precision])
end

#lngObject



102
103
104
# File 'lib/grid_ref.rb', line 102

def lng
  coordinates[:lng].to_degrees.round(self.options[:precision])
end

#northingObject



94
95
96
# File 'lib/grid_ref.rb', line 94

def northing
  @north ||= offsets[:n] + digits[resolution, resolution].to_i * SquareSize[resolution]
end

#offsetsObject



77
78
79
80
81
82
83
84
85
86
87
88
# File 'lib/grid_ref.rb', line 77

def offsets
  if tile
    major = OsTiles[tile[0,1].downcase.to_sym ]
    minor = OsTiles[tile[1,1].downcase.to_sym]
  	@offset ||= {
      :e => (500000 * (major[0] - FalseOrigin[0])) + (100000 * minor[0]),
    	:n => (500000 * (major[1] - FalseOrigin[1])) + (100000 * minor[1])
  	}
  else
    { :e => 0, :n => 0 }
  end
end

#resolutionObject



73
74
75
# File 'lib/grid_ref.rb', line 73

def resolution
  digits.length / 2
end

#tileObject



65
66
67
# File 'lib/grid_ref.rb', line 65

def tile
  @tile ||= gridref[0,2]
end

#to_latlngObject



110
111
112
# File 'lib/grid_ref.rb', line 110

def to_latlng
  "#{lat}, #{lng}"
end

#to_sObject



106
107
108
# File 'lib/grid_ref.rb', line 106

def to_s
  gridref.to_s
end