silva 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore CHANGED
@@ -15,3 +15,4 @@ spec/reports
15
15
  test/tmp
16
16
  test/version_tmp
17
17
  tmp
18
+ .#
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ Copyright 2012 Robert Dallas Gray
2
+
3
+ Redistribution and use in source and binary forms, with or without modification, are
4
+ permitted provided that the following conditions are met:
5
+
6
+ 1. Redistributions of source code must retain the above copyright notice, this list of
7
+ conditions and the following disclaimer.
8
+
9
+ 2. Redistributions in binary form must reproduce the above copyright notice, this list
10
+ of conditions and the following disclaimer in the documentation and/or other materials
11
+ provided with the distribution.
12
+
13
+ THIS SOFTWARE IS PROVIDED BY <COPYRIGHT HOLDER> ''AS IS'' AND ANY EXPRESS OR IMPLIED
14
+ WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
15
+ FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> OR
16
+ CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
17
+ CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
18
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
19
+ ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
20
+ NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
21
+ ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
data/README.md CHANGED
@@ -1,27 +1,29 @@
1
- What is Silva?
2
- =============
3
- A RubyGem which provides utilities to convert between the WGS84 standard for location information (as used by Google Maps, the GPS system, etc.) and the UK Ordnance Survey's OSGB36 datum, easting/northing format and standard grid references.
4
1
 
5
2
  How do I use it?
6
3
  ================
7
- `gem install silva`
8
-
9
- `require 'silva'`
10
4
 
11
- `Silva::Location.from(:wgs84, :lat => 52.658008, :long => 1.716077).to(:gridref)`
5
+ gem install silva
6
+
7
+ require 'silva'
8
+
9
+ Silva::Location.from(:wgs84, :lat => 52.658008, :long => 1.716077).to(:gridref)
12
10
 
13
- `"TG51411318"``
11
+ "TG51411318"
14
12
 
15
13
  What else can it do?
16
14
  ===================
17
15
  Silva works with four different location systems:
18
16
 
19
- - WGS84 `(:wgs84, :lat => [latitude], :long => [longitude], :alt => [optional altitude])`
20
- - OSGB36 `(:osgb36, params as above)`
21
- - EN `(:en, :easting => [easting], :northing => [northing])`
22
- - GridRef `(:gridref, :gridref => [gridref] OR :easting => [easting], :northing => [northing])`
17
+ - WGS84
18
+ `(:wgs84, :lat => [latitude], :long => [longitude], :alt => [optional altitude])`
19
+ - OSGB36
20
+ `(:osgb36, params as above)`
21
+ - EN
22
+ `(:en, :easting => [easting], :northing => [northing])`
23
+ - GridRef
24
+ `(:gridref, :gridref => [gridref] OR :easting => [easting], :northing => [northing])`
23
25
 
24
- It can convert freely among each of them using the syntax Silva::Location.from(:system, params).to(:system, params).
26
+ It can convert freely among each of them using the syntax `Silva::Location.from(:system, params).to(:system, params)`.
25
27
 
26
28
  Why did you write it?
27
29
  =====================
@@ -29,7 +31,7 @@ I needed to convert between WGS84 co-ordinates and Ordnance Survey grid referenc
29
31
 
30
32
  Anything else?
31
33
  =============
32
- I began with code written by (Harry Wood)[http://www.harrywood.co.uk/blog/2010/06/29/ruby-code-for-converting-to-uk-ordnance-survey-coordinate-systems-from-wgs84], who adapted code written by (Chris Veness)[http://www.movable-type.co.uk/scripts/latlong-convert-coords.html]. I subsequently went back over the maths to make sure I understood it reasonably well, and clarified it so that it's easy to check against the resources supplied by the Ordnance Survey. Some of Chris's and Harry's code is probably still lurking in there.
34
+ I began with code written by [Harry Wood](http://www.harrywood.co.uk/blog/2010/06/29/ruby-code-for-converting-to-uk-ordnance-survey-coordinate-systems-from-wgs84), who adapted code written by [Chris Veness](http://www.movable-type.co.uk/scripts/latlong-convert-coords.html). I subsequently went back over the maths to make sure I understood it reasonably well, and clarified it so that it's easy to check against the resources supplied by the Ordnance Survey. Some of Chris's and Harry's code is probably still lurking in there.
33
35
 
34
36
  It should be accurate to about 5 to 10 metres.
35
37
 
@@ -0,0 +1,7 @@
1
+ module Silva
2
+ class InvalidSystemError < StandardError; end
3
+ class InvalidTransformError < StandardError; end
4
+ class InvalidParamError < StandardError; end
5
+ class InvalidParamValueError < StandardError; end
6
+ class InsufficentParamsError < StandardError; end
7
+ end
@@ -0,0 +1,19 @@
1
+ module Silva
2
+ ##
3
+ # A spoonful of syntactic sugar for creating location systems from the relevant parameters:
4
+ #
5
+ # loc = Silva::Location.from(:en, :easting => 651409, :northing => 31377).to(:wgs84)
6
+ # lat = loc.lat, long = loc.long
7
+ #
8
+ module Location
9
+ ##
10
+ # Create a location system from the given parameters.
11
+ #
12
+ # @param [Symbol] system_name The name of the system -- at present, :wgs84, :en, :osgb36 or :gridref.
13
+ # @param [Hash] options Parameters relevant to the system (see individual systems for details).
14
+ # @return [Silva::System] A new location system.
15
+ def self.from(system_name, options)
16
+ Silva::System.create(system_name, options)
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,68 @@
1
+ module Silva
2
+ ##
3
+ # A location system -- :wgs84, :osgb36, :en or :gridref.
4
+ module System
5
+ ##
6
+ # A factory method to simplify and moderate creation of location systems.
7
+ # @param [Symbol] system_name The name of the system to be created.
8
+ # @param [Hash] options Parameters relevant to the given system.
9
+ # @return [Silva::System] A valid location system.
10
+ # @raises Silva::InvalidSystemError If the given system can't be created.
11
+ def self.create(system_name, options)
12
+ return Silva::System.const_get(system_name.to_s.capitalize).new(options)
13
+ rescue NameError
14
+ raise Silva::InvalidSystemError, "Can't create system: #{system_name}"
15
+ end
16
+
17
+ ##
18
+ # Provides basic utility functions.
19
+ #
20
+ class Base
21
+ # Default parameters can be specified for each system.
22
+ DEFAULT_PARAMS = {}
23
+ # Some parameters must be given
24
+ REQUIRED_PARAMS = []
25
+
26
+ ##
27
+ # Create the given system with relevant options.
28
+ #
29
+ # @param [Hash] options Parameters relevant to the given system.
30
+ #
31
+ def initialize(options)
32
+ @system_name = self.class.name.split('::').last.downcase.to_sym
33
+ options = DEFAULT_PARAMS.merge(options)
34
+ params_satisfied?(options)
35
+ options.each {|param, val| set_param(param, val) }
36
+ end
37
+
38
+ ##
39
+ # Transforms the base system to a different system.
40
+ #
41
+ # @param [Symbol] target_system_name The system to convert to.
42
+ # @param [Hash] options Parameters relevant to the target system.
43
+ # @return [Silva::System] A new location system of type target_system_name.
44
+ def to(target_system_name, options = nil)
45
+ return self if target_system_name == @system_name
46
+ to_method = "to_#{target_system_name}".to_sym
47
+ unless respond_to?(to_method, true)
48
+ raise Silva::InvalidTransformError, "#{@system_name} cannot be transformed to #{target_system_name}."
49
+ end
50
+ send(to_method, options)
51
+ end
52
+
53
+ private
54
+
55
+ # Set the params given in options, if they pass validation.
56
+ def set_param(param, val)
57
+ val_method = "validate_#{param}".to_sym
58
+ raise Silva::InvalidParamError, "Invalid param: #{param}." unless respond_to?(val_method, true)
59
+ raise Silva::InvalidParamValueError, "Invalid #{param}: #{val}." unless (send(val_method, val))
60
+ instance_variable_set("@#{param}", val)
61
+ end
62
+
63
+ def params_satisfied?(options)
64
+ raise InsufficientParamsError unless REQUIRED_PARAMS & options.keys == REQUIRED_PARAMS
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,39 @@
1
+ module Silva
2
+ module System
3
+ ##
4
+ # Provides simple validations and accessors for a lat, long, alt co-ordinate system.
5
+ #
6
+ module CoOrdinate
7
+ # Allowed range of latitude
8
+ LAT_RANGE = (-90..90)
9
+ # Allowed range of longitude
10
+ LONG_RANGE = (-180..180)
11
+ # Allowed range of altitude
12
+ ALT_RANGE = (-500..4000)
13
+
14
+ # Default altitude = 0
15
+ DEFAULT_PARAMS = { :alt => 0 }
16
+ REQUIRED_PARAMS = [:lat, :long]
17
+
18
+ attr_reader :lat, :long, :alt
19
+
20
+ def inspect
21
+ [lat, long, alt].to_s
22
+ end
23
+
24
+ private
25
+
26
+ def validate_lat(lat)
27
+ lat.is_a?(Numeric) && LAT_RANGE.cover?(lat)
28
+ end
29
+
30
+ def validate_long(long)
31
+ long.is_a?(Numeric) && LONG_RANGE.cover?(long)
32
+ end
33
+
34
+ def validate_alt(alt)
35
+ alt.is_a?(Numeric) && ALT_RANGE.cover?(alt)
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,30 @@
1
+ module Silva
2
+ module System
3
+ ##
4
+ # Location system representing Ordnance Survey OSGB36 eastings and northings.
5
+ #
6
+ class En < Base
7
+ include OsEn
8
+
9
+ def inspect
10
+ [easting, northing].to_s
11
+ end
12
+
13
+ private
14
+
15
+ def to_osgb36(options = nil)
16
+ Silva::Transform.en_to_osgb36(self)
17
+ end
18
+
19
+ def to_wgs84(options = nil)
20
+ Silva::Transform.osgb36_to_wgs84(to_osgb36)
21
+ end
22
+
23
+ def to_gridref(options = nil)
24
+ options ||= {}
25
+ options = options.merge({ :easting => easting, :northing => northing })
26
+ System.create(:gridref, options)
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,105 @@
1
+ module Silva
2
+ module System
3
+ ##
4
+ # Location system representing Ordnance Survey Standard Grid References.
5
+ #
6
+ # Can be created given the options :easting => e, :northing => n or :gridref => g
7
+ #
8
+ class Gridref < Base
9
+ include OsEn
10
+ # :digits can be 6, 8 or 10
11
+ DEFAULT_PARAMS = { :digits => 8 }
12
+ # UK two-letter prefixes
13
+ OSGB_PREFIXES = ["SV", "SW", "SX", "SY", "SZ", "TV", "TW",
14
+ "SQ", "SR", "SS", "ST", "SU", "TQ", "TR",
15
+ "SL", "SM", "SN", "SO", "SP", "TL", "TM",
16
+ "SF", "SG", "SH", "SJ", "SK", "TF", "TG",
17
+ "SA", "SB", "SC", "SD", "SE", "TA", "TB",
18
+ "NV", "NW", "NX", "NY", "NZ", "OV", "OW",
19
+ "NQ", "NR", "NS", "NT", "NU", "OQ", "OR",
20
+ "NL", "NM", "NN", "NO", "NP", "OL", "OM",
21
+ "NF", "NG", "NH", "NJ", "NK", "OF", "OG",
22
+ "NA", "NB", "NC", "ND", "NE", "OA", "OB",
23
+ "HV", "HW", "HX", "HY", "HZ", "JV", "JW",
24
+ "HQ", "HR", "HS", "HT", "HU", "JQ", "JR",
25
+ "HL", "HM", "HN", "HO", "HP", "JL", "JM"]
26
+ # Width of the UK grid
27
+ OSGB_GRID_WIDTH = 7
28
+ # Height of the UK grid
29
+ OSGB_GRID_SCALE = 100000
30
+
31
+ attr_reader :gridref
32
+
33
+ ##
34
+ # Lazily create the gridref from given eastings and northings, or just return it if already set.
35
+ #
36
+ # @return [String] A standard Ordnance Survey grid reference with the given number of digits.
37
+ #
38
+ def gridref
39
+ unless @gridref
40
+ e100k = (easting / 100000).floor
41
+ n100k = (northing / 100000).floor
42
+
43
+ index = n100k * OSGB_GRID_WIDTH + e100k
44
+ prefix = OSGB_PREFIXES[index]
45
+
46
+ e = ((easting % OSGB_GRID_SCALE) / (10**(5 - @digits / 2))).round
47
+ n = ((northing % OSGB_GRID_SCALE) / (10**(5 - @digits / 2))).round
48
+
49
+ @gridref = prefix + e.to_s.rjust(@digits / 2) + n.to_s.rjust(@digits / 2)
50
+ end
51
+
52
+ @gridref
53
+ end
54
+
55
+ def inspect
56
+ gridref
57
+ end
58
+
59
+
60
+ private
61
+
62
+ def to_wgs84(options = nil)
63
+ Silva::Transform.osgb36_to_wgs84(to_osgb36)
64
+ end
65
+
66
+ def to_osgb36(options = nil)
67
+ Silva::Transform.en_to_osgb36(to_en)
68
+ end
69
+
70
+ def to_en(options = nil)
71
+ e100k, n100k = prefix_to_en
72
+ gridref.delete!(' ')
73
+ en = gridref[2..-1]
74
+ e = en[0, (en.length / 2)].ljust(5, '5').to_i + e100k
75
+ n = en[(en.length / 2)..-1].ljust(5, '5').to_i + n100k
76
+
77
+ System.create(:en, :easting => e, :northing => n )
78
+ end
79
+
80
+ def get_prefix
81
+ gridref[0..1]
82
+ end
83
+
84
+ def prefix_to_en
85
+ index = OSGB_PREFIXES.index(get_prefix)
86
+ e = index % OSGB_GRID_WIDTH
87
+ n = index / OSGB_GRID_WIDTH
88
+
89
+ [e * OSGB_GRID_SCALE, n * OSGB_GRID_SCALE]
90
+ end
91
+
92
+ def validate_digits(digits)
93
+ [6, 8, 10].include?(digits)
94
+ end
95
+
96
+ def validate_gridref(gridref)
97
+ gridref.match /[HJNOST][A-Z][0-9]{3,5}[0-9]{3,5}/
98
+ end
99
+
100
+ def params_satisfied?(options)
101
+ super or options.include?(:gridref)
102
+ end
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,28 @@
1
+ module Silva
2
+ module System
3
+ module OsEn
4
+ ##
5
+ # Provides simple validations for OS easting/northing systems.
6
+ #
7
+
8
+ # Allowed range of eastings
9
+ EASTING_RANGE = (0..700000)
10
+ # Allowed range of northings
11
+ NORTHING_RANGE = (0..1300000)
12
+
13
+ REQUIRED_PARAMS = [:easting, :northing]
14
+
15
+ attr_reader :easting, :northing
16
+
17
+ private
18
+
19
+ def validate_easting(e)
20
+ e.is_a?(Numeric) && EASTING_RANGE.cover?(e)
21
+ end
22
+
23
+ def validate_northing(n)
24
+ n.is_a?(Numeric) && NORTHING_RANGE.cover?(n)
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,24 @@
1
+ module Silva
2
+ module System
3
+ ##
4
+ # Location system representing OSGB36 co-ordinates.
5
+ #
6
+ class Osgb36 < Base
7
+ include CoOrdinate
8
+
9
+ private
10
+
11
+ def to_en(options = nil)
12
+ Silva::Transform.osgb36_to_en(self)
13
+ end
14
+
15
+ def to_gridref(options = nil)
16
+ to_en.to(:gridref, options)
17
+ end
18
+
19
+ def to_wgs84(options = nil)
20
+ Silva::Transform.osgb36_to_wgs84(self)
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,24 @@
1
+ module Silva
2
+ module System
3
+ ##
4
+ # Location system representing WGS84 co-ordinates.
5
+ #
6
+ class Wgs84 < Base
7
+ include CoOrdinate
8
+
9
+ private
10
+
11
+ def to_osgb36(options = nil)
12
+ Silva::Transform.wgs84_to_osgb36(self)
13
+ end
14
+
15
+ def to_en(options = nil)
16
+ to_osgb36.to(:en)
17
+ end
18
+
19
+ def to_gridref(options = nil)
20
+ to_en.to(:gridref, options)
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,235 @@
1
+ module Silva
2
+ ##
3
+ # Encapsulates the hairy maths required to perform the various transforms.
4
+ # Checked against documentation provided by Orndance Survey:
5
+ # http://www.ordnancesurvey.co.uk/oswebsite/gps/osnetfreeservices/furtherinfo/questdeveloper.html
6
+ # http://www.ordnancesurvey.co.uk/oswebsite/gps/information/coordinatesystemsinfo/guidecontents/guide6.html
7
+ #
8
+ class Transform
9
+ # The Airy ellipsoid used by the OSGB36 datum.
10
+ AIRY1830 = { :a => 6377563.396, :b => 6356256.91 }
11
+ # The GRS80 ellipsoid used by the WGS84 datum.
12
+ GRS80 = { :a => 6378137, :b=> 6356752.3141 }
13
+
14
+ # The northing of true origin
15
+ N0 = -100000.0
16
+ # The easting of true origin
17
+ E0 = 400000.0
18
+ # The scale factor on central meridian
19
+ F0 = 0.9996012717
20
+ # The latitude of true origin, in radians
21
+ PHI0 = 49 * Math::PI / 180
22
+ # The longitude of true origin, in radians
23
+ LAMBDA0 = -2 * Math::PI / 180
24
+
25
+ # Helmert transform parameters
26
+ HELMERT_PARAMS = {
27
+ :tx=> -446.448, :ty=> 125.157, :tz=> -542.060, # m
28
+ :rx=> -0.1502, :ry=> -0.2470, :rz=> -0.8421, # sec
29
+ :s=> 20.4894 # ppm
30
+ }
31
+
32
+ # Decimal places to round results in degrees.
33
+ DEGREE_ROUNDING_PLACES = 6
34
+
35
+ ##
36
+ # Convert an :osgb36 co-ordinate system :wgs84
37
+ #
38
+ # @param [Silva::System::Osgb36] osgb36 A valid :osgb36 location system.
39
+ # @return [Silva::System::Wgs84] A valid :wgs84 location system.
40
+ #
41
+ def self.osgb36_to_wgs84(osgb36)
42
+ helmert_transform(osgb36, :wgs84, AIRY1830, HELMERT_PARAMS.inject({}) { |h, (k, v)| h[k] = v * -1; h }, GRS80)
43
+ end
44
+
45
+ ##
46
+ # Convert a :wgs84 co-ordinate system to :osgb36
47
+ #
48
+ # @param [Silva::System::Wgs84] wgs84 A valid :wgs84 location system.
49
+ # @return [Silva::System::Osgb36] A valid :osgb36 location system.
50
+ #
51
+ def self.wgs84_to_osgb36(wgs84)
52
+ helmert_transform(wgs84, :osgb36, GRS80, HELMERT_PARAMS, AIRY1830)
53
+ end
54
+
55
+ ##
56
+ # Convert an :osgb36 co-ordinate system to :en (eastings and northings)
57
+ #
58
+ # @param [Silva::System::Osgb36] osgb36 A valid :wgs84 location system.
59
+ # @return [Silva::System::En] A valid :en location system.
60
+ #
61
+ def self.osgb36_to_en(osgb36)
62
+ self.latlong_to_en(osgb36, AIRY1830)
63
+ end
64
+
65
+ ##
66
+ # Convert an OSGB36 easting, northing system to :osgb36 co-ordinate system
67
+ #
68
+ # @param [Silva::System::En] osgb36 A valid :en location system.
69
+ # @return [Silva::System::Osgb36] A valid :osgb36 location system.
70
+ #
71
+ def self.en_to_osgb36(en)
72
+ # Algorithm from:
73
+ # http://www.ordnancesurvey.co.uk/oswebsite/gps/osnetfreeservices/furtherinfo/questdeveloper.html
74
+ a, b = AIRY1830[:a], AIRY1830[:b]
75
+ e = en.easting
76
+ n = en.northing
77
+ phi_prime = PHI0
78
+ m = 0
79
+
80
+ while n - N0 - m >= 0.0001 do
81
+ phi_prime = (n - N0 - m) / (a * F0) + phi_prime
82
+ m = meridional_arc(phi_prime)
83
+ end
84
+
85
+ nu, rho, eta2 = transverse_and_meridional_radii(phi_prime)
86
+
87
+ vii = Math.tan(phi_prime) / (2 * rho * nu)
88
+ viii = Math.tan(phi_prime)/(24 * rho * nu**3) * \
89
+ (5 + 3 * Math.tan(phi_prime)**2 + eta2 - 9 * Math.tan(phi_prime)**2 * eta2)
90
+ ix = Math.tan(phi_prime) / (720 * rho * nu**5) * (61 + 90 * Math.tan(phi_prime)**2 + 45 * Math.tan(phi_prime)**4)
91
+ x = (1 / Math.cos(phi_prime)) / nu
92
+ xi = (1 / Math.cos(phi_prime)) / (6 * nu**3) * (nu / rho + 2 * Math.tan(phi_prime)**2)
93
+ xii = (1 / Math.cos(phi_prime)) / (120 * nu**5) * (5 + 28 * Math.tan(phi_prime)**2 + 24 * Math.tan(phi_prime)**4)
94
+ xiia = (1 / Math.cos(phi_prime)) / (5040 * nu**7) * \
95
+ (61 + 662 * Math.tan(phi_prime)**2 + 1320 * Math.tan(phi_prime)**4 + 720 * Math.tan(phi_prime)**6)
96
+
97
+ phi = phi_prime - vii * (e - E0)**2 + viii * (e - E0)**4 - ix * (e - E0)**6
98
+ lambda = LAMBDA0 + x * (e - E0) - xi * (e - E0)**3 + xii * (e - E0)**5 - xiia * (e - E0)**7
99
+
100
+ System.create(:osgb36, :lat => to_deg(phi).round(DEGREE_ROUNDING_PLACES), \
101
+ :long => to_deg(lambda).round(DEGREE_ROUNDING_PLACES), :alt => 0)
102
+ end
103
+
104
+
105
+ private
106
+
107
+ # Convert an :osgb36 or :wgs84 co-ordinate system to OSGB36 easting and northing.
108
+ def self.latlong_to_en(system, ellipsoid)
109
+ phi = to_rad(system.lat)
110
+ lambda = to_rad(system.long)
111
+
112
+ nu, rho, eta2 = transverse_and_meridional_radii(phi, ellipsoid)
113
+ m = meridional_arc(phi, ellipsoid)
114
+
115
+ i = m + N0
116
+ ii = (nu / 2) * Math.sin(phi) * Math.cos(phi)
117
+ iii = (nu / 24) * Math.sin(phi) * Math.cos(phi)**3 * (5 - Math.tan(phi)**2 + 9 * eta2)
118
+ iiia = (nu / 720) * Math.sin(phi) * Math.cos(phi)**5 * (61 - 58 * Math.tan(phi)**2 + Math.tan(phi)**4)
119
+ iv = nu * Math.cos(phi)
120
+ v = (nu / 6) * Math.cos(phi)**3 * (nu / rho - Math.tan(phi)**2)
121
+ vi = (nu / 120) * Math.cos(phi)**5 * \
122
+ (5 - 18 * Math.tan(phi)**2 + Math.tan(phi)**4 + 14 * eta2 - 58 * Math.tan(phi)**4 * eta2)
123
+
124
+ n = i + ii * (lambda - LAMBDA0)**2 + iii * (lambda - LAMBDA0)**4 + iiia * (lambda - LAMBDA0)**6
125
+ e = E0 + iv * (lambda - LAMBDA0) + v * (lambda - LAMBDA0)**3 + vi * (lambda - LAMBDA0)**5
126
+
127
+ System.create(:en, :easting => e.round, :northing => n.round)
128
+ end
129
+
130
+ # Transform to/from :osgb36/:wgs84 co-ordinate systems.
131
+ # Algorithm from:
132
+ # http://www.ordnancesurvey.co.uk/oswebsite/gps/information/coordinatesystemsinfo/guidecontents/guide6.html
133
+ # Portions of code from:
134
+ # http://www.harrywood.co.uk/blog/2010/06/29/ruby-code-for-converting-to-uk-ordnance-survey-coordinate-systems-from-wgs84/
135
+
136
+ def self.helmert_transform(system, target_system, ellipsoid_1, transform, ellipsoid_2)
137
+ phi = to_rad(system.lat)
138
+ lambda = to_rad(system.long)
139
+ alt = system.alt
140
+
141
+ a1 = ellipsoid_1[:a]
142
+ b1 = ellipsoid_1[:b]
143
+
144
+ sin_phi = Math.sin(phi)
145
+ cos_phi = Math.cos(phi)
146
+ sin_lambda = Math.sin(lambda)
147
+ cos_lambda = Math.cos(lambda)
148
+
149
+ e_sq1 = eccentricity_squared(ellipsoid_1)
150
+ nu = a1 / Math.sqrt(1 - e_sq1 * sin_phi**2)
151
+
152
+ x1 = (nu + alt) * cos_phi * cos_lambda
153
+ y1 = (nu + alt) * cos_phi * sin_lambda
154
+ z1 = ((1 - e_sq1) * nu + alt) * sin_phi
155
+
156
+ tx = transform[:tx]
157
+ ty = transform[:ty]
158
+ tz = transform[:tz]
159
+ rx = transform[:rx] / 3600 * Math::PI / 180
160
+ ry = transform[:ry] / 3600 * Math::PI / 180
161
+ rz = transform[:rz] / 3600 * Math::PI / 180
162
+ s1 = transform[:s] / 1e6 + 1
163
+
164
+ x2 = tx + x1 * s1 - y1 * rz + z1 * ry
165
+ y2 = ty + x1 * rz + y1 * s1 - z1 * rx
166
+ z2 = tz - x1 * ry + y1 * rx + z1 * s1
167
+
168
+ a2 = ellipsoid_2[:a]
169
+ b2 = ellipsoid_2[:b]
170
+ precision = 4 / a2
171
+
172
+ e_sq2 = eccentricity_squared(ellipsoid_2)
173
+ p = Math.sqrt(x2 * x2 + y2 * y2)
174
+ phi = Math.atan2(z2, p * (1 - e_sq2))
175
+ phi_p = 2 * Math::PI
176
+
177
+ while ((phi - phi_p).abs > precision) do
178
+ nu = a2 / Math.sqrt(1 - e_sq2 * Math.sin(phi)**2)
179
+ phi_p = phi
180
+ phi = Math.atan2(z2 + e_sq2 * nu * Math.sin(phi), p)
181
+ end
182
+
183
+ lambda = Math.atan2(y2, x2)
184
+ h = p / Math.cos(phi) - nu
185
+
186
+ System.create(target_system, :lat => to_deg(phi).round(DEGREE_ROUNDING_PLACES),\
187
+ :long => to_deg(lambda).round(DEGREE_ROUNDING_PLACES), :alt => h)
188
+ end
189
+
190
+ # Calculate required n parameter given the relevant ellipsoid
191
+ def self.n(ellipsoid)
192
+ (ellipsoid[:a] - ellipsoid[:b]) / (ellipsoid[:a] + ellipsoid[:b])
193
+ end
194
+
195
+ # Calculate M (meridional arc) given latitude and relevant ellipsoid
196
+ def self.meridional_arc(phi, ellipsoid = AIRY1830)
197
+ a, b = ellipsoid[:a], ellipsoid[:b]
198
+ n = self.n(ellipsoid)
199
+
200
+ ma = (1 + n + (5.0 / 4.0) * n**2 + (5.0 / 4.0) * n**3) * (phi - PHI0)
201
+ mb = (3 * n + 3 * n**2 + (21.0 / 8.0) * n**3) * Math.sin(phi - PHI0) * Math.cos(phi + PHI0)
202
+ mc = ((15.0 / 8.0) * n**2 + (15.0 / 8.0) * n**3) * Math.sin(2 * (phi - PHI0)) * Math.cos(2 * (phi + PHI0))
203
+ md = (35.0 / 24.0) * n**3 * Math.sin(3 * (phi - PHI0)) * Math.cos(3 * (phi + PHI0))
204
+
205
+ b * F0 * (ma - mb + mc - md)
206
+ end
207
+
208
+ # Calculate nu, rho, eta2 (transverse and meridional radii) given latitude and relevant ellipsoid.
209
+ def self.transverse_and_meridional_radii(phi, ellipsoid = AIRY1830)
210
+ a, b = ellipsoid[:a], ellipsoid[:b]
211
+ e2 = self.eccentricity_squared(ellipsoid)
212
+
213
+ nu = a * F0 / Math.sqrt(1 - e2 * Math.sin(phi)**2)
214
+ rho = a * F0 * (1 - e2) / ((1 - e2 * Math.sin(phi)**2)**1.5)
215
+ eta2 = nu / rho - 1
216
+
217
+ [nu, rho, eta2]
218
+ end
219
+
220
+ # Calculate eccentricity**2 given relevant ellipsoid.
221
+ def self.eccentricity_squared(ellipsoid)
222
+ (ellipsoid[:a]**2 - ellipsoid[:b]**2) / ellipsoid[:a]**2
223
+ end
224
+
225
+ # Degrees to radians.
226
+ def self.to_rad(degrees)
227
+ degrees * Math::PI / 180
228
+ end
229
+
230
+ # Radians to degrees.
231
+ def self.to_deg(rads)
232
+ rads * 180 / Math::PI
233
+ end
234
+ end
235
+ end
data/lib/silva/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Silva
2
- VERSION = "0.0.1"
2
+ VERSION = "0.0.2"
3
3
  end
data/test/test_data.rb ADDED
@@ -0,0 +1,22 @@
1
+ module Silva
2
+ module Test
3
+ DATA = [{
4
+ # from os worked examples
5
+ :wgs84 => { :lat => 52.658007833, :long => 1.716073973, :alt => 180.05 },
6
+ :osgb36 => { :lat => 52.65757, :long => 1.717922, :alt => 180.05 },
7
+ :en => { :easting => 651409.903, :northing => 313177.270 },
8
+ :gridref => "TG51411318"
9
+ },
10
+ {
11
+ # greenwich
12
+ :wgs84 => { :lat => 51.478017, :long => -0.001619, :alt => 0},
13
+ :osgb36 => { :lat => 51.477501, :long => 0, :alt => 0 },
14
+ :en => { :easting => 538874, :northing => 177344 },
15
+ :gridref => "TQ38877734"
16
+ }]
17
+ LAT_DELTA = 5e-5
18
+ LONG_DELTA = 2.5e-5
19
+ EN_DELTA = 10
20
+ COORD_FROM_GRIDREF_DELTA = 1e-4
21
+ end
22
+ end
data/test/test_en.rb ADDED
@@ -0,0 +1,24 @@
1
+ class TestEn < Test::Unit::TestCase
2
+ def test_en_to_wgs84
3
+ Silva::Test::DATA.each do |data|
4
+ l = Silva::Location.from(:en, data[:en]).to(:wgs84)
5
+ assert_in_delta data[:wgs84][:lat], l.lat, Silva::Test::LAT_DELTA
6
+ assert_in_delta data[:wgs84][:long], l.long, Silva::Test::LONG_DELTA
7
+ end
8
+ end
9
+
10
+ def test_en_to_osgb36
11
+ Silva::Test::DATA.each do |data|
12
+ l = Silva::Location.from(:en, data[:en]).to(:osgb36)
13
+ assert_in_delta data[:osgb36][:lat], l.lat, Silva::Test::LAT_DELTA
14
+ assert_in_delta data[:osgb36][:long], l.long, Silva::Test::LONG_DELTA
15
+ end
16
+ end
17
+
18
+ def test_en_to_gridref
19
+ Silva::Test::DATA.each do |data|
20
+ l = Silva::Location.from(:en, data[:en]).to(:gridref, :digits => 8)
21
+ assert_equal data[:gridref], l.gridref
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,28 @@
1
+ class TestGridref < Test::Unit::TestCase
2
+ def test_gridref_to_en
3
+ Silva::Test::DATA.each do |data|
4
+ options = { :gridref => data[:gridref], :digits => 8 }
5
+ l = Silva::Location.from(:gridref, options).to(:en)
6
+ assert_in_delta data[:en][:easting], l.easting, Silva::Test::EN_DELTA
7
+ assert_in_delta data[:en][:northing], l.northing, Silva::Test::EN_DELTA
8
+ end
9
+ end
10
+
11
+ def test_gridref_to_osgb36
12
+ Silva::Test::DATA.each do |data|
13
+ options = { :gridref => data[:gridref], :digits => 8 }
14
+ l = Silva::Location.from(:gridref, options).to(:osgb36)
15
+ assert_in_delta data[:osgb36][:lat], l.lat, Silva::Test::COORD_FROM_GRIDREF_DELTA
16
+ assert_in_delta data[:osgb36][:long], l.long, Silva::Test::COORD_FROM_GRIDREF_DELTA
17
+ end
18
+ end
19
+
20
+ def test_gridref_to_wgs84
21
+ Silva::Test::DATA.each do |data|
22
+ options = { :gridref => data[:gridref], :digits => 8 }
23
+ l = Silva::Location.from(:gridref, options).to(:wgs84)
24
+ assert_in_delta data[:wgs84][:lat], l.lat, Silva::Test::COORD_FROM_GRIDREF_DELTA
25
+ assert_in_delta data[:wgs84][:long], l.long, Silva::Test::COORD_FROM_GRIDREF_DELTA
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,8 @@
1
+ require 'test/unit'
2
+ require_relative '../lib/silva'
3
+ require_relative 'test_data'
4
+ require_relative 'test_location'
5
+ require_relative 'test_wgs84'
6
+ require_relative 'test_en'
7
+ require_relative 'test_osgb36'
8
+ require_relative 'test_gridref'
@@ -0,0 +1,33 @@
1
+ class TestLocation < Test::Unit::TestCase
2
+ def test_invalid_system_raises_error
3
+ assert_raise Silva::InvalidSystemError do
4
+ l = Silva::Location.from(:invalid, nil)
5
+ end
6
+ end
7
+
8
+ def test_invalid_param_raises_error
9
+ assert_raise Silva::InvalidParamError do
10
+ l = Silva::Location.from(:wgs84, :lat => 0, :long => 0, :altitude => 0)
11
+ end
12
+ end
13
+
14
+ def test_invalid_value_raises_error
15
+ assert_raise Silva::InvalidParamValueError do
16
+ l = Silva::Location.from(:wgs84, :lat => "five", :long => 0, :alt => 0)
17
+ end
18
+ end
19
+
20
+ def test_co_ordinate_system_out_of_range_param_raises_error
21
+ assert_raise Silva::InvalidParamValueError do
22
+ l = Silva::Location.from(:wgs84, :lat => 185, :long => 0, :altitude => 0)
23
+ end
24
+ end
25
+
26
+ def test_co_ordinates_set_correctly
27
+ Silva::Test::DATA.each do |data|
28
+ l = Silva::Location.from(:wgs84, data[:wgs84]).to(:wgs84)
29
+ assert(l.lat == data[:wgs84][:lat] && l.long == data[:wgs84][:long] && l.alt == data[:wgs84][:alt], \
30
+ "Failed assigning co-ords to System::Wgs84")
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,2 @@
1
+ class TestOsgb36 < Test::Unit::TestCase
2
+ end
@@ -0,0 +1,31 @@
1
+ class TestWgs84 < Test::Unit::TestCase
2
+ def setup
3
+ end
4
+
5
+ def teardown
6
+ end
7
+
8
+ def test_wgs84_to_en
9
+ Silva::Test::DATA.each do |data|
10
+ l = Silva::Location.from(:wgs84, data[:wgs84]).to(:en)
11
+ assert_in_delta data[:en][:easting], l.easting, Silva::Test::EN_DELTA
12
+ assert_in_delta data[:en][:northing], l.northing, Silva::Test::EN_DELTA
13
+ end
14
+ end
15
+
16
+ def test_wgs84_to_osgb36
17
+ Silva::Test::DATA.each do |data|
18
+ l = Silva::Location.from(:wgs84, data[:wgs84]).to(:osgb36)
19
+ assert_in_delta data[:osgb36][:lat], l.lat, Silva::Test::LAT_DELTA
20
+ assert_in_delta data[:osgb36][:long], l.long, Silva::Test::LONG_DELTA
21
+ end
22
+ end
23
+
24
+ def test_wgs84_to_gridref
25
+ Silva::Test::DATA.each do |data|
26
+ l = Silva::Location.from(:wgs84, data[:wgs84]).to(:gridref, :digits => 8)
27
+ assert_equal data[:gridref], l.gridref
28
+ end
29
+ end
30
+ end
31
+
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: silva
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.0.2
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -37,11 +37,29 @@ extra_rdoc_files: []
37
37
  files:
38
38
  - .gitignore
39
39
  - Gemfile
40
+ - LICENSE.txt
40
41
  - README.md
41
42
  - Rakefile
42
43
  - lib/silva.rb
44
+ - lib/silva/exception.rb
45
+ - lib/silva/location.rb
46
+ - lib/silva/system/base.rb
47
+ - lib/silva/system/co_ordinate.rb
48
+ - lib/silva/system/en.rb
49
+ - lib/silva/system/gridref.rb
50
+ - lib/silva/system/osen.rb
51
+ - lib/silva/system/osgb36.rb
52
+ - lib/silva/system/wgs84.rb
53
+ - lib/silva/transform.rb
43
54
  - lib/silva/version.rb
44
55
  - silva.gemspec
56
+ - test/test_data.rb
57
+ - test/test_en.rb
58
+ - test/test_gridref.rb
59
+ - test/test_helper.rb
60
+ - test/test_location.rb
61
+ - test/test_osgb36.rb
62
+ - test/test_wgs84.rb
45
63
  homepage: http://github.com/rdallasgray/silva
46
64
  licenses:
47
65
  - FreeBSD
@@ -68,5 +86,12 @@ signing_key:
68
86
  specification_version: 3
69
87
  summary: Convert between the GPS (WGS84) location standard and UK Ordnance Survey
70
88
  standards.
71
- test_files: []
89
+ test_files:
90
+ - test/test_data.rb
91
+ - test/test_en.rb
92
+ - test/test_gridref.rb
93
+ - test/test_helper.rb
94
+ - test/test_location.rb
95
+ - test/test_osgb36.rb
96
+ - test/test_wgs84.rb
72
97
  has_rdoc: