silva 0.0.1 → 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +1 -0
- data/LICENSE.txt +21 -0
- data/README.md +16 -14
- data/lib/silva/exception.rb +7 -0
- data/lib/silva/location.rb +19 -0
- data/lib/silva/system/base.rb +68 -0
- data/lib/silva/system/co_ordinate.rb +39 -0
- data/lib/silva/system/en.rb +30 -0
- data/lib/silva/system/gridref.rb +105 -0
- data/lib/silva/system/osen.rb +28 -0
- data/lib/silva/system/osgb36.rb +24 -0
- data/lib/silva/system/wgs84.rb +24 -0
- data/lib/silva/transform.rb +235 -0
- data/lib/silva/version.rb +1 -1
- data/test/test_data.rb +22 -0
- data/test/test_en.rb +24 -0
- data/test/test_gridref.rb +28 -0
- data/test/test_helper.rb +8 -0
- data/test/test_location.rb +33 -0
- data/test/test_osgb36.rb +2 -0
- data/test/test_wgs84.rb +31 -0
- metadata +27 -2
data/.gitignore
CHANGED
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
|
-
|
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
|
-
|
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
|
20
|
-
|
21
|
-
-
|
22
|
-
|
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
|
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
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
|
data/test/test_helper.rb
ADDED
@@ -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
|
data/test/test_osgb36.rb
ADDED
data/test/test_wgs84.rb
ADDED
@@ -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.
|
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:
|