os_national_grid 0.1.0

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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 2b4ee02475482d888e215c0cc2f7eea4fe3b1d2b845a5f3a6d21ac00b970339d
4
+ data.tar.gz: c569c213f053264cb513119c7befe38e10267c579f87b5fdbd50079d0dbf7e21
5
+ SHA512:
6
+ metadata.gz: 73e5cf6df117050ebbed6ca67feaa9f04b1d8de90095b0619ef63d696898f5aab8f76d8de87b1aab5d234cd42486df01f20c7cc8cfdd81f905f9eedf504feb4e
7
+ data.tar.gz: 7d01f7691ad39926cae610dc9fb526701f797ade2f85b66ebc927cbea7e9a089052fd46ef8be53926c16285c2e73ca08d5463833570ac2adc947cfd123a8598a
data/.standard.yml ADDED
@@ -0,0 +1,3 @@
1
+ # For available configuration options, see:
2
+ # https://github.com/standardrb/standard
3
+ ruby_version: 3.1
data/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ ## [Unreleased]
2
+
3
+ ## [0.1.0] - 2025-06-04
4
+
5
+ - Initial release
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright 2025 Unboxed Consulting Limited
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,57 @@
1
+ # OsNationalGrid
2
+
3
+ [![Gem Version](https://badge.fury.io/rb/os_national_grid.svg)](https://badge.fury.io/rb/os_national_grid)
4
+
5
+ **OsNationalGrid** is a Ruby gem that provides accurate transformations between the [Ordnance Survey National Grid (OSGB36)](https://www.ordnancesurvey.co.uk/documents/resources/guide-coordinate-systems-great-britain.pdf) and WGS84 (latitude/longitude) coordinate systems.
6
+
7
+ It uses the official mathematical transformations from the Ordnance Survey, including a Helmert transformation for precise conversions between coordinate systems.
8
+
9
+ ---
10
+
11
+ ## Installation
12
+
13
+ Add to your Gemfile:
14
+
15
+ ```ruby
16
+ gem 'os_national_grid'
17
+ ```
18
+ Then install:
19
+
20
+ ```bash
21
+ bundle install
22
+ ```
23
+
24
+ Or install it manually:
25
+
26
+ ```bash
27
+ gem install os_national_grid
28
+ ```
29
+
30
+ ## Usage
31
+
32
+ ### Convert from OSGB36 (easting, northing) to WGS84 (longitude, latitude)
33
+
34
+ ```bash
35
+ lng, lat = OsNationalGrid.os_ng_to_wgs84(481_987.066, 213_552.27)
36
+
37
+ => [-0.812036, 51.814605]
38
+ ```
39
+
40
+ ### Convert from WGS84 (longitude, latitude) to OSGB36 (easting, northing)
41
+ ```bash
42
+ easting, northing = OsNationalGrid.wgs84_to_os_ng(-0.812036, 51.814605)
43
+
44
+ => [481987.066, 213552.27]
45
+ ```
46
+
47
+ ## Development
48
+
49
+ Run the test suite with:
50
+
51
+ ```bash
52
+ bundle exec rake test
53
+ ```
54
+
55
+ ## License
56
+
57
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "minitest/test_task"
5
+
6
+ Minitest::TestTask.create
7
+
8
+ require "standard/rake"
9
+
10
+ task default: %i[test standard]
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OsNationalGrid
4
+ VERSION = "0.1.0"
5
+ end
@@ -0,0 +1,223 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "os_national_grid/version"
4
+
5
+ module OsNationalGrid
6
+ extend self
7
+
8
+ # All the values and formula are from the Ordnance Survey publication:
9
+ # A guide to coordinate systems in Great Britain
10
+ # https://www.ordnancesurvey.co.uk/docs/support/guide-coordinate-systems-great-britain.pdf
11
+
12
+ # Scale factor on the central meridian for the Transverse Mercator projection
13
+ # https://en.wikipedia.org/wiki/Transverse_Mercator_projection
14
+ F0 = 0.9996012717
15
+
16
+ # True origin of the National Grid projection (from section A.2)
17
+ LAT0 = 0.8552113334772214
18
+ LNG0 = -0.03490658503988659
19
+
20
+ # Map coordinates of the true origin (from section A.2)
21
+ E0 = 400_000.0
22
+ N0 = -100_000.0
23
+
24
+ # Precision for iterative calculations
25
+ PRECISION = 1e-8
26
+
27
+ # Helmert transformation parameters (from section 6.6)
28
+ WGS84_TO_OSGB36 = {
29
+ tx: -446.4480, ty: 125.1570, tz: -542.0600,
30
+ rx: -0.1502, ry: -0.2470, rz: -0.8421,
31
+ s: 20.4894
32
+ }.freeze
33
+
34
+ OSGB36_TO_WGS84 = {
35
+ tx: 446.4480, ty: -125.1570, tz: 542.0600,
36
+ rx: 0.1502, ry: 0.2470, rz: 0.8421,
37
+ s: -20.4894
38
+ }.freeze
39
+
40
+ # Ellipsoid dimensions
41
+ WGS84 = [6_378_137.0, 6_356_752.3142].freeze
42
+ OSGB36 = [6_377_563.396, 6_356_256.909].freeze
43
+
44
+ include Math
45
+
46
+ # Based on the formula in C.2
47
+ def os_ng_to_wgs84(easting, northing)
48
+ a, b = OSGB36
49
+ e2 = ((a**2) - (b**2)) / (a**2)
50
+ n = (a - b) / (a + b)
51
+
52
+ lat = ((northing - N0) / a * F0) + LAT0
53
+ m = mercator(lat, n, b)
54
+
55
+ 10.times do
56
+ lat = ((northing - N0 - m) / a * F0) + lat
57
+ m = mercator(lat, n, b)
58
+
59
+ break if (northing - N0 - m) < PRECISION
60
+ end
61
+
62
+ sin_lat = sin(lat)
63
+ sec_lat = 1.0 / cos(lat)
64
+ tan_lat = tan(lat)
65
+
66
+ v = a * F0 * ((1 - (e2 * (sin_lat**2)))**-0.5)
67
+ rho = a * F0 * (1 - e2) * ((1 - (e2 * (sin_lat**2)))**-1.5)
68
+ n2 = (v / rho) - 1.0
69
+
70
+ c7 = tan_lat / (2.0 * rho * v)
71
+ c8 = tan_lat / (24.0 * rho * (v**3.0)) * (5 + (3.0 * (tan_lat**2.0)) + n2 - (9.0 * (tan_lat**2.0) * n2))
72
+ c9 = tan_lat / (720.0 * rho * (v**5.0)) * (61 + (90.0 * (tan_lat**2.0)) + (45.0 * (tan_lat**4.0)))
73
+ c10 = sec_lat / v
74
+ c11 = (sec_lat / (6.0 * (v**3.0))) * ((v / rho) + (2.0 * (tan_lat**2.0)))
75
+ c12 = (sec_lat / (120.0 * (v**5.0))) * (5.0 + (28.0 * (tan_lat**2.0)) + (24.0 * (tan_lat**4.0)))
76
+ c12a = (sec_lat / (5040.0 * (v**7.0))) * (61.0 + (662.0 * (tan_lat**2.0)) + (1320.0 * (tan_lat**4.0)) + (720.0 * (tan_lat**6.0)))
77
+ delta = easting - E0
78
+
79
+ lat = lat - (c7 * (delta**2.0)) + (c8 * (delta**4.0)) - (c9 * (delta**6.0))
80
+ lng = LNG0 + (c10 * delta) - (c11 * (delta**3.0)) + (c12 * (delta**5.0)) - (c12a * (delta**7.0))
81
+
82
+ lng, lat = osgb36_to_wgs84(lng, lat)
83
+ [rad_to_deg(lng).round(6), rad_to_deg(lat).round(6)]
84
+ end
85
+
86
+ # Based on the formula in C.1
87
+ def wgs84_to_os_ng(lng, lat)
88
+ lng, lat = wgs84_to_osgb36(deg_to_rad(lng), deg_to_rad(lat))
89
+
90
+ a, b = OSGB36
91
+ e2 = ((a**2) - (b**2)) / (a**2)
92
+ n = (a - b) / (a + b)
93
+
94
+ v = a * F0 * ((1 - (e2 * (sin(lat)**2)))**-0.5)
95
+ rho = a * F0 * (1 - e2) * ((1 - (e2 * (sin(lat)**2)))**-1.5)
96
+ n2 = (v / rho) - 1.0
97
+ m = mercator(lat, n, b)
98
+
99
+ tan_lat = tan(lat)
100
+ sin_lat = sin(lat)
101
+ cos_lat = cos(lat)
102
+
103
+ c1 = m + N0
104
+ c2 = v / 2.0 * sin_lat * cos_lat
105
+ c3 = v / 24.0 * sin_lat * (cos_lat**3.0) * (5.0 - (tan_lat**2.0) + (9 * n2))
106
+ c3a = v / 720.0 * sin_lat * (cos_lat**5.0) * (61.0 - (58.0 * (tan_lat**2.0)) + (tan_lat**4.0))
107
+ c4 = v * cos_lat
108
+ c5 = v / 6.0 * (cos_lat**3.0) * ((v / rho) - (tan_lat**2.0))
109
+ c6 = v / 120.0 * (cos_lat**5.0) * (5 - (18.0 * (tan_lat**2.0)) + (tan_lat**4.0) + (14.0 * n2) - (58.0 * (tan_lat**2.0) * n2))
110
+ delta = lng - LNG0
111
+
112
+ northing = c1 + (c2 * (delta**2.0)) + (c3 * (delta**4.0)) + (c3a * (delta**6.0))
113
+ easting = E0 + (c4 * delta) + (c5 * (delta**3.0)) + (c6 * (delta**5.0))
114
+
115
+ [easting.round(3), northing.round(3)]
116
+ end
117
+
118
+ private
119
+
120
+ def wgs84_to_osgb36(lng, lat, height = 0)
121
+ coords = spherical_to_cartesian(lng, lat, height, WGS84)
122
+ coords = transform(coords, WGS84_TO_OSGB36)
123
+
124
+ cartesian_to_spherical(*coords, OSGB36)[0..1]
125
+ end
126
+
127
+ def osgb36_to_wgs84(lng, lat, height = 0)
128
+ coords = spherical_to_cartesian(lng, lat, height, OSGB36)
129
+ coords = transform(coords, OSGB36_TO_WGS84)
130
+
131
+ cartesian_to_spherical(*coords, WGS84)[0..1]
132
+ end
133
+
134
+ # From 'A guide to coordinate systems in Great Britain - B.1'
135
+ def spherical_to_cartesian(lng, lat, height, ellipsoid)
136
+ a, b = ellipsoid
137
+ e2 = ((a**2) - (b**2)) / (a**2)
138
+
139
+ v = a / sqrt(1 - (e2 * (sin(lat)**2.0)))
140
+ x = (v + height) * cos(lat) * cos(lng)
141
+ y = (v + height) * cos(lat) * sin(lng)
142
+ z = (((1 - e2) * v) + height) * sin(lat)
143
+
144
+ [x, y, z]
145
+ end
146
+
147
+ # From 'A guide to coordinate systems in Great Britain - B.2'
148
+ def cartesian_to_spherical(x, y, z, ellipsoid)
149
+ a, b = ellipsoid
150
+ e2 = ((a**2) - (b**2)) / (a**2)
151
+
152
+ p = sqrt((x**2.0) + (y**2.0))
153
+ lat = atan(z / p * (1 - e2))
154
+ v = a / sqrt(1 - (e2 * (sin(lat)**2.0)))
155
+
156
+ # Although the algorithm says to loop until the delta is within the
157
+ # desired precision we put a hard limit of 10 iterations to prevent
158
+ # the application process from going into an infinite loop.
159
+ 10.times do
160
+ lat1 = atan((z + (e2 * v * sin(lat))) / p)
161
+ v = a / sqrt(1 - (e2 * (sin(lat1)**2.0)))
162
+
163
+ break if (lat1 - lat).abs < PRECISION
164
+
165
+ lat = lat1
166
+ end
167
+
168
+ lng = atan(y / x)
169
+ height = (p / cos(lat)) - v
170
+
171
+ [lng, lat, height]
172
+ end
173
+
174
+ def transform(coords, matrix)
175
+ x1, y1, z1 = coords
176
+ tx, ty, tz = matrix.values_at(:tx, :ty, :tz)
177
+
178
+ # Helmert values are in arc seconds so convert to radians
179
+ rx, ry, rz = matrix.values_at(:rx, :ry, :rz).map(&method(:sec_to_rad))
180
+
181
+ # Normalize from ppm
182
+ s1 = (matrix[:s] / 1e6) + 1
183
+
184
+ # Transformation matrix from section 6.2 (3)
185
+ x2 = tx + (x1 * s1) - (y1 * rz) + (z1 * ry)
186
+ y2 = ty + (x1 * rz) + (y1 * s1) - (z1 * rx)
187
+ z2 = tz - (x1 * ry) + (y1 * rx) + (z1 * s1)
188
+
189
+ [x2, y2, z2]
190
+ end
191
+
192
+ def sec_to_rad(seconds)
193
+ seconds / 3600.0 * PI / 180.0
194
+ end
195
+
196
+ def deg_to_rad(degrees)
197
+ degrees * PI / 180.0
198
+ end
199
+
200
+ def rad_to_deg(radians)
201
+ radians * 180.0 / PI
202
+ end
203
+
204
+ def mercator(lat, n, b)
205
+ b * F0 * (merc_1(lat, n) - merc_2(lat, n) + merc_3(lat, n) - merc_4(lat, n))
206
+ end
207
+
208
+ def merc_1(lat, n)
209
+ (1 + n + (1.2 * (n**2)) + (1.2 * (n**3))) * (lat - LAT0)
210
+ end
211
+
212
+ def merc_2(lat, n)
213
+ ((3 * n) + (3 * (n**2)) + (2.625 * (n**3))) * sin(lat - LAT0) * cos(lat + LAT0)
214
+ end
215
+
216
+ def merc_3(lat, n)
217
+ ((1.875 * (n**2)) + (1.875 * (n**3))) * sin(2 * (lat - LAT0)) * cos(2 * (lat + LAT0))
218
+ end
219
+
220
+ def merc_4(lat, n)
221
+ (35.0 * (n**3) / 24.0) * sin(3 * (lat - LAT0)) * cos(3 * (lat + LAT0))
222
+ end
223
+ end
metadata ADDED
@@ -0,0 +1,51 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: os_national_grid
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Unboxed Consulting
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 2025-06-06 00:00:00.000000000 Z
11
+ dependencies: []
12
+ description: Transforms OS National Grid (OSGB36) to WGS84 coordinates using official
13
+ Ordnance Survey methods.
14
+ email:
15
+ - github@unboxedconsulting.com
16
+ executables: []
17
+ extensions: []
18
+ extra_rdoc_files: []
19
+ files:
20
+ - ".standard.yml"
21
+ - CHANGELOG.md
22
+ - LICENSE
23
+ - README.md
24
+ - Rakefile
25
+ - lib/os_national_grid.rb
26
+ - lib/os_national_grid/version.rb
27
+ homepage: https://github.com/unboxed/os_national_grid
28
+ licenses:
29
+ - MIT
30
+ metadata:
31
+ homepage_uri: https://github.com/unboxed/os_national_grid
32
+ source_code_uri: https://github.com/unboxed/os_national_grid
33
+ changelog_uri: https://github.com/unboxed/os_national_grid/blob/main/CHANGELOG.md
34
+ rdoc_options: []
35
+ require_paths:
36
+ - lib
37
+ required_ruby_version: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - ">="
40
+ - !ruby/object:Gem::Version
41
+ version: 3.1.0
42
+ required_rubygems_version: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '0'
47
+ requirements: []
48
+ rubygems_version: 3.6.5
49
+ specification_version: 4
50
+ summary: Convert between Ordnance Survey National Grid and WGS84 coordinates.
51
+ test_files: []