defra_ruby_address 0.1.0 → 0.4.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.
Files changed (38) hide show
  1. checksums.yaml +5 -5
  2. data/README.md +82 -6
  3. data/Rakefile +2 -2
  4. data/bin/console +2 -6
  5. data/lib/defra_ruby/address/helmert_transformation.rb +102 -0
  6. data/lib/defra_ruby/address/response.rb +1 -2
  7. data/lib/defra_ruby/address/services/base_service.rb +2 -2
  8. data/lib/defra_ruby/address/services/easting_northing_to_lat_lon_service.rb +18 -0
  9. data/lib/defra_ruby/address/services/lat_lon_to_easting_northing_service.rb +20 -0
  10. data/lib/defra_ruby/address/services/os_api_address_lookup_v1_service.rb +100 -0
  11. data/lib/defra_ruby/address/services/os_places_address_lookup_service.rb +11 -14
  12. data/lib/defra_ruby/address/transverse_mercator_projection.rb +151 -0
  13. data/lib/defra_ruby/address/version.rb +1 -1
  14. data/lib/defra_ruby/address.rb +5 -0
  15. metadata +11 -181
  16. data/spec/defra_ruby/address/configuration_spec.rb +0 -18
  17. data/spec/defra_ruby/address/response_spec.rb +0 -70
  18. data/spec/defra_ruby/address/services/ea_address_facade_v1_1_service_spec.rb +0 -73
  19. data/spec/defra_ruby/address/services/ea_address_facade_v1_service_spec.rb +0 -73
  20. data/spec/defra_ruby/address/services/os_places_address_lookup_service_spec.rb +0 -72
  21. data/spec/defra_ruby/address_spec.rb +0 -30
  22. data/spec/examples.txt +0 -27
  23. data/spec/fixtures/ea_address_facade_v1_1_blank.json +0 -8
  24. data/spec/fixtures/ea_address_facade_v1_1_not_found.json +0 -14
  25. data/spec/fixtures/ea_address_facade_v1_1_valid.json +0 -83
  26. data/spec/fixtures/ea_address_facade_v1_blank.json +0 -13
  27. data/spec/fixtures/ea_address_facade_v1_not_found.json +0 -8
  28. data/spec/fixtures/ea_address_facade_v1_valid.json +0 -45
  29. data/spec/fixtures/os_places_address_lookup_blank.json +0 -5
  30. data/spec/fixtures/os_places_address_lookup_not_found.json +0 -6
  31. data/spec/fixtures/os_places_address_lookup_valid.json +0 -56
  32. data/spec/spec_helper.rb +0 -83
  33. data/spec/support/defra_ruby_address.rb +0 -4
  34. data/spec/support/dotenv.rb +0 -4
  35. data/spec/support/pry.rb +0 -7
  36. data/spec/support/shared_examples/handle_request_errors.rb +0 -32
  37. data/spec/support/simplecov.rb +0 -17
  38. data/spec/support/webmock.rb +0 -4
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: f47c9a7a1e39ae9d9b7062dc77fe063bf443cbe0
4
- data.tar.gz: 37cf2259b5bb278a180f89fcb413636b50b4011c
2
+ SHA256:
3
+ metadata.gz: a5d4b459711e84d2d0caac1f48600cb928c1cede9b4ca85e5d23e53b12f4a881
4
+ data.tar.gz: 0c6b7e465734d6f4cc4cbea1df20b40a2d93d9e0d38035dc7d2d8ffbe639855e
5
5
  SHA512:
6
- metadata.gz: 10198b655e7d18bb9dc59d90bbe9ac1909d0a528bfc9df7a26be3dad6e5438881d501202962d1d102d4298ef9980dfe5b182772fe4bb934b74cd4e1f57cb27dc
7
- data.tar.gz: b1221cbdd838ba2ee750778987ff652f16b72dc33a20cd5fcef9ae8eaf9ad0248e277af12f3bf61ed63dc5ec01b5b765e5604125f725faf689dad43c677c8ea4
6
+ metadata.gz: c80f7606cbfffc7dc4f03c06b106c2df3d23ad5c4fd93e29849c3ff035e6b5d53836b8bad16816f40164674de1d9437b07792d13410d99b8df4775cb72c10c2c
7
+ data.tar.gz: 7dc7df096183d0422971325ba71e4f5607b9e279833f10d8bd86249ef808af6c758be28a610e16bfb6f2604489992fbfa3eba0c089543fabbe2d6bec9197029b
data/README.md CHANGED
@@ -1,9 +1,9 @@
1
1
  # Defra Ruby Address
2
2
 
3
- [![Build Status](https://travis-ci.com/DEFRA/defra-ruby-address.svg?branch=master)](https://travis-ci.com/DEFRA/defra-ruby-address)
4
- [![Maintainability](https://api.codeclimate.com/v1/badges/1a0b68efe00098e0734f/maintainability)](https://codeclimate.com/github/DEFRA/defra-ruby-address/maintainability)
5
- [![Test Coverage](https://api.codeclimate.com/v1/badges/1a0b68efe00098e0734f/test_coverage)](https://codeclimate.com/github/DEFRA/defra-ruby-address/test_coverage)
6
- [![security](https://hakiri.io/github/DEFRA/defra-ruby-address/master.svg)](https://hakiri.io/github/DEFRA/defra-ruby-address/master)
3
+ ![Build Status](https://github.com/DEFRA/defra-ruby-address/workflows/CI/badge.svg?branch=main)
4
+ [![Maintainability Rating](https://sonarcloud.io/api/project_badges/measure?project=DEFRA_defra-ruby-address&metric=sqale_rating)](https://sonarcloud.io/dashboard?id=DEFRA_defra-ruby-address)
5
+ [![Coverage](https://sonarcloud.io/api/project_badges/measure?project=DEFRA_defra-ruby-address&metric=coverage)](https://sonarcloud.io/dashboard?id=DEFRA_defra-ruby-address)
6
+ [![security](https://hakiri.io/github/DEFRA/defra-ruby-address/main.svg)](https://hakiri.io/github/DEFRA/defra-ruby-address/main)
7
7
  [![Gem Version](https://badge.fury.io/rb/defra_ruby_address.svg)](https://badge.fury.io/rb/defra_ruby_address)
8
8
  [![Licence](https://img.shields.io/badge/Licence-OGLv3-blue.svg)](http://www.nationalarchives.gov.uk/doc/open-government-licence/version/3)
9
9
 
@@ -47,14 +47,23 @@ DefraRuby::Address.configure do |config|
47
47
  end
48
48
  ```
49
49
 
50
+ To use the OS API service, you need to configure your API key from the [OS Data Hub](https://osdatahub.os.uk/). The host defaults to `https://api.os.uk/search/places/v1`:
51
+
52
+ ```ruby
53
+ DefraRuby::Address.configure do |config|
54
+ config.key = ENV["OS_API_KEY"]
55
+ end
56
+ ```
57
+
50
58
  ## Usage
51
59
 
52
- The gem interfaces with 2 address lookups
60
+ The gem interfaces with 3 address lookups
53
61
 
54
62
  - [OS Places Address Lookup](https://github.com/DEFRA/os-places-address-lookup)
55
63
  - [EA Address Facade](https://github.com/DEFRA/ea-address-facade) (repo is private)
64
+ - [OS API](https://docs.os.uk/os-apis/accessing-os-apis/os-places-api) (Ordnance Survey Places API)
56
65
 
57
- The **EA Address Facade** has 2 versions, so the gem provides 3 separate services a host app can use.
66
+ The **EA Address Facade** has 2 versions, and combined with the other lookups, the gem provides 4 separate services a host app can use.
58
67
 
59
68
  ### Response object
60
69
 
@@ -210,6 +219,73 @@ The expected format of each result is
210
219
  }
211
220
  ```
212
221
 
222
+ ### OS API Address Lookup v1
223
+
224
+ This service calls the [Ordnance Survey Places API](https://docs.os.uk/os-apis/accessing-os-apis/os-places-api) directly, without going through an intermediary service. It returns results mapped to the same format as the EA Address Facade v1.1, making it a drop-in replacement.
225
+
226
+ > **Note:** The OS API is subject to a 600 transactions-per-minute throttle. Applications using this service should implement their own caching and/or rate limiting to stay within this limit.
227
+
228
+ ```ruby
229
+ response = DefraRuby::Address::OsApiAddressLookupV1Service.run("BS1 5AH")
230
+
231
+ puts response.results.first["uprn"] # 340116
232
+ ```
233
+
234
+ The expected format of each result matches the EA Address Facade v1.1 format:
235
+
236
+ ```ruby
237
+ {
238
+ "uprn"=>340116,
239
+ "address"=>"ENVIRONMENT AGENCY, HORIZON HOUSE, DEANERY ROAD, BRISTOL, BS1 5AH",
240
+ "organisation"=>"ENVIRONMENT AGENCY",
241
+ "premises"=>"HORIZON HOUSE",
242
+ "street_address"=>"DEANERY ROAD",
243
+ "locality"=>nil,
244
+ "city"=>"BRISTOL",
245
+ "postcode"=>"BS1 5AH",
246
+ "country"=>"United Kingdom",
247
+ "x"=>358205.03,
248
+ "y"=>172708.07,
249
+ "coordinate_system"=>nil,
250
+ "blpu_state_date"=>"12/10/2009",
251
+ "blpu_state_code"=>"2",
252
+ "postal_address_code"=>"D",
253
+ "logical_status_code"=>"1",
254
+ "source_data_type"=>"dpa",
255
+ "blpu_state_code_description"=>"In use",
256
+ "classification_code"=>"CO01",
257
+ "classification_code_description"=>"Office / Work Studio",
258
+ "lpi_logical_status_code"=>nil,
259
+ "lpi_logical_status_code_description"=>nil,
260
+ "match"=>1.0,
261
+ "match_description"=>"EXACT",
262
+ "topography_layer_toid"=>"osgb1000002529079737",
263
+ "parent_uprn"=>nil,
264
+ "last_update_date"=>"10/02/2016",
265
+ "status"=>"APPROVED",
266
+ "entry_date"=>"12/10/2009",
267
+ "postal_address_code_description"=>"A record which is linked to PAF",
268
+ "usrn"=>nil,
269
+ "language"=>"EN"
270
+ }
271
+ ```
272
+
273
+ ### Coordinate conversion
274
+
275
+ The gem also provides a pair of services for converting British National Grid easting and northing coordinates (OSGB36) to WGS84 latitude and longitude, and back again, for example to support geospatial queries.
276
+
277
+ The conversion is pure Ruby, using the 7-parameter Helmert transformation and Transverse Mercator formulae from the OS guide [A guide to coordinate systems in Great Britain](https://www.ordnancesurvey.co.uk/documents/resources/guide-coordinate-systems-great-britain.pdf), and is accurate to around 5 metres.
278
+
279
+ Unlike the address lookup services these do not call an external API, and they return a plain hash rather than a `Response` object. String arguments are coerced to floats.
280
+
281
+ ```ruby
282
+ DefraRuby::Address::EastingNorthingToLatLonService.run(358_130, 172_688)
283
+ # => { latitude: 51.45161..., longitude: -2.60394... }
284
+
285
+ DefraRuby::Address::LatLonToEastingNorthingService.run(51.451616, -2.603943)
286
+ # => { easting: 358129.8..., northing: 172687.8... }
287
+ ```
288
+
213
289
  ## Contributing to this project
214
290
 
215
291
  If you have an idea you'd like to contribute please log an issue.
data/Rakefile CHANGED
@@ -11,7 +11,6 @@ Bundler::GemHelper.install_tasks
11
11
  # This is wrapped to prevent an error when rake is called in environments where
12
12
  # rspec may not be available, e.g. production. As such we don't need to handle
13
13
  # the error.
14
- # rubocop:disable Lint/HandleExceptions
15
14
  begin
16
15
  require "rspec/core/rake_task"
17
16
 
@@ -26,8 +25,9 @@ begin
26
25
  require "github_changelog_generator/task"
27
26
 
28
27
  GitHubChangelogGenerator::RakeTask.new :changelog do |config|
28
+ config.user = "defra"
29
+ config.project = "defra-ruby-address"
29
30
  end
30
31
  rescue LoadError
31
32
  # no changelog available
32
33
  end
33
- # rubocop:enable Lint/HandleExceptions
data/bin/console CHANGED
@@ -6,9 +6,5 @@ require "defra_ruby/address"
6
6
  # You can add fixtures and/or initialization code here to make experimenting
7
7
  # with your gem easier. You can also use a different console, if you like.
8
8
 
9
- # (If you use this, don't forget to add pry to your Gemfile!)
10
- # require "pry"
11
- # Pry.start
12
-
13
- require "irb"
14
- IRB.start(__FILE__)
9
+ require "pry"
10
+ Pry.start
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DefraRuby
4
+ module Address
5
+ # Shifts latitude/longitude between the OSGB36 and WGS84 datums using the
6
+ # 7-parameter Helmert transformation from the OS guide "A guide to
7
+ # coordinate systems in Great Britain". Accurate to around 5 metres.
8
+ #
9
+ # The OSGB36 to WGS84 direction is ported from the breasal gem
10
+ # (https://github.com/theodi/breasal, MIT licence, copyright 2013
11
+ # pezholio), corrected to the published OS guide parameters. The reverse
12
+ # direction just negates them.
13
+ class HelmertTransformation
14
+ AIRY_1830 = { a: 6_377_563.396, b: 6_356_256.909 }.freeze
15
+ WGS84 = { a: 6_378_137.0, b: 6_356_752.3141 }.freeze
16
+
17
+ ARC_SECOND_IN_RADIANS = Math::PI / 648_000
18
+
19
+ # Translations (tx, ty, tz) in metres, scale change (s) in parts per
20
+ # unit and rotations (rx, ry, rz) in radians, for the OSGB36 to WGS84
21
+ # direction; negated they transform in the opposite direction.
22
+ PARAMETERS = {
23
+ tx: 446.448,
24
+ ty: -125.157,
25
+ tz: 542.060,
26
+ s: -0.0000204894,
27
+ rx: 0.1502 * ARC_SECOND_IN_RADIANS,
28
+ ry: 0.2470 * ARC_SECOND_IN_RADIANS,
29
+ rz: 0.8421 * ARC_SECOND_IN_RADIANS
30
+ }.freeze
31
+
32
+ def self.osgb36_to_wgs84(latitude, longitude)
33
+ new(AIRY_1830, WGS84, 1).transform(latitude, longitude)
34
+ end
35
+
36
+ def self.wgs84_to_osgb36(latitude, longitude)
37
+ new(WGS84, AIRY_1830, -1).transform(latitude, longitude)
38
+ end
39
+
40
+ def initialize(source_ellipsoid, target_ellipsoid, direction)
41
+ @source_ellipsoid = source_ellipsoid
42
+ @target_ellipsoid = target_ellipsoid
43
+ @direction = direction
44
+ end
45
+
46
+ def transform(latitude, longitude)
47
+ to_geodetic(apply_helmert(to_cartesian(latitude, longitude)))
48
+ end
49
+
50
+ private
51
+
52
+ attr_reader :source_ellipsoid, :target_ellipsoid, :direction
53
+
54
+ def to_cartesian(latitude, longitude)
55
+ e_squared = eccentricity_squared(source_ellipsoid)
56
+ phi = latitude * Math::PI / 180
57
+ lambda = longitude * Math::PI / 180
58
+ nu = source_ellipsoid[:a] / Math.sqrt(1.0 - (e_squared * (Math.sin(phi)**2)))
59
+
60
+ [nu * Math.cos(phi) * Math.cos(lambda),
61
+ nu * Math.cos(phi) * Math.sin(lambda),
62
+ (1.0 - e_squared) * nu * Math.sin(phi)]
63
+ end
64
+
65
+ def apply_helmert(cartesian)
66
+ x, y, z = cartesian
67
+ scale = 1.0 + parameter(:s)
68
+ rx = parameter(:rx)
69
+ ry = parameter(:ry)
70
+ rz = parameter(:rz)
71
+
72
+ [parameter(:tx) + (scale * x) - (rz * y) + (ry * z),
73
+ parameter(:ty) + (rz * x) + (scale * y) - (rx * z),
74
+ parameter(:tz) - (ry * x) + (rx * y) + (scale * z)]
75
+ end
76
+
77
+ def to_geodetic(cartesian)
78
+ x, y, z = cartesian
79
+ e_squared = eccentricity_squared(target_ellipsoid)
80
+ p = Math.hypot(x, y)
81
+ phi = Math.atan(z / (p * (1.0 - e_squared)))
82
+
83
+ 10.times do
84
+ nu = target_ellipsoid[:a] / Math.sqrt(1.0 - (e_squared * (Math.sin(phi)**2)))
85
+ phi = Math.atan((z + (e_squared * nu * Math.sin(phi))) / p)
86
+ end
87
+
88
+ [phi * 180 / Math::PI, Math.atan2(y, x) * 180 / Math::PI]
89
+ end
90
+
91
+ def parameter(key)
92
+ direction * PARAMETERS[key]
93
+ end
94
+
95
+ def eccentricity_squared(ellipsoid)
96
+ a = ellipsoid[:a]
97
+ b = ellipsoid[:b]
98
+ ((a * a) - (b * b)) / (a * a)
99
+ end
100
+ end
101
+ end
102
+ end
@@ -3,8 +3,7 @@
3
3
  module DefraRuby
4
4
  module Address
5
5
  class Response
6
- attr_reader :error
7
- attr_reader :results
6
+ attr_reader :error, :results
8
7
 
9
8
  def initialize(response_exe)
10
9
  @success = true
@@ -6,8 +6,8 @@ require "rest-client"
6
6
  module DefraRuby
7
7
  module Address
8
8
  class BaseService
9
- def self.run(attrs = nil)
10
- new.run(attrs)
9
+ def self.run(*)
10
+ new.run(*)
11
11
  end
12
12
  end
13
13
  end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DefraRuby
4
+ module Address
5
+ # Converts a British National Grid easting and northing to a WGS84
6
+ # latitude and longitude. Accurate to around 5 metres.
7
+ class EastingNorthingToLatLonService < BaseService
8
+ def run(easting, northing)
9
+ osgb36_latitude, osgb36_longitude = TransverseMercatorProjection.easting_northing_to_lat_lon(
10
+ Float(easting), Float(northing)
11
+ )
12
+ latitude, longitude = HelmertTransformation.osgb36_to_wgs84(osgb36_latitude, osgb36_longitude)
13
+
14
+ { latitude: latitude, longitude: longitude }
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DefraRuby
4
+ module Address
5
+ # Converts a WGS84 latitude and longitude to a British National Grid
6
+ # easting and northing. Accurate to around 5 metres.
7
+ class LatLonToEastingNorthingService < BaseService
8
+ def run(latitude, longitude)
9
+ osgb36_latitude, osgb36_longitude = HelmertTransformation.wgs84_to_osgb36(
10
+ Float(latitude), Float(longitude)
11
+ )
12
+ easting, northing = TransverseMercatorProjection.lat_lon_to_easting_northing(
13
+ osgb36_latitude, osgb36_longitude
14
+ )
15
+
16
+ { easting: easting, northing: northing }
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DefraRuby
4
+ module Address
5
+ class OsApiAddressLookupV1Service < BaseService
6
+
7
+ # Mapping from OS API DPA field names to EaAddressFacade field names
8
+ # rubocop:disable Layout/HashAlignment
9
+ FIELD_MAPPING = {
10
+ "UPRN" => "uprn",
11
+ "ADDRESS" => "address",
12
+ "ORGANISATION_NAME" => "organisation",
13
+ "BUILDING_NAME" => "premises",
14
+ "THOROUGHFARE_NAME" => "street_address",
15
+ "DEPENDENT_LOCALITY" => "locality",
16
+ "POST_TOWN" => "city",
17
+ "POSTCODE" => "postcode",
18
+ "X_COORDINATE" => "x",
19
+ "Y_COORDINATE" => "y",
20
+ "BLPU_STATE_DATE" => "blpu_state_date",
21
+ "BLPU_STATE_CODE" => "blpu_state_code",
22
+ "POSTAL_ADDRESS_CODE" => "postal_address_code",
23
+ "LOGICAL_STATUS_CODE" => "logical_status_code",
24
+ "BLPU_STATE_CODE_DESCRIPTION" => "blpu_state_code_description",
25
+ "CLASSIFICATION_CODE" => "classification_code",
26
+ "CLASSIFICATION_CODE_DESCRIPTION" => "classification_code_description",
27
+ "MATCH" => "match",
28
+ "MATCH_DESCRIPTION" => "match_description",
29
+ "TOPOGRAPHY_LAYER_TOID" => "topography_layer_toid",
30
+ "PARENT_UPRN" => "parent_uprn",
31
+ "LAST_UPDATE_DATE" => "last_update_date",
32
+ "STATUS" => "status",
33
+ "ENTRY_DATE" => "entry_date",
34
+ "POSTAL_ADDRESS_CODE_DESCRIPTION" => "postal_address_code_description",
35
+ "USRN" => "usrn",
36
+ "LANGUAGE" => "language"
37
+ }.freeze
38
+ # rubocop:enable Layout/HashAlignment
39
+
40
+ DEFAULT_HOST = "https://api.os.uk/search/places/v1"
41
+
42
+ def run(postcode)
43
+ @postcode = postcode
44
+ Response.new(response_exe)
45
+ end
46
+
47
+ private
48
+
49
+ attr_reader :postcode
50
+
51
+ def url
52
+ host = DefraRuby::Address.configuration.host || DEFAULT_HOST
53
+
54
+ File.join(
55
+ host,
56
+ "postcode?postcode=#{postcode}&key=#{DefraRuby::Address.configuration.key}"
57
+ )
58
+ end
59
+
60
+ def response_exe
61
+ lambda do
62
+ response = RestClient::Request.execute(
63
+ method: :get,
64
+ url: url,
65
+ timeout: DefraRuby::Address.configuration.timeout
66
+ )
67
+ parsed = JSON.parse(response)
68
+ results = parsed["results"] || []
69
+
70
+ raise DefraRuby::Address::NoMatchError if results.empty?
71
+
72
+ results.map { |r| map_dpa_to_result(r["DPA"]) }
73
+ end
74
+ end
75
+
76
+ def map_dpa_to_result(dpa)
77
+ result = map_fields(dpa)
78
+ result["uprn"] = result["uprn"]&.to_i
79
+ result.merge(hardcoded_fields)
80
+ end
81
+
82
+ def map_fields(dpa)
83
+ FIELD_MAPPING.each_with_object({}) do |(os_key, facade_key), hash|
84
+ hash[facade_key] = dpa[os_key]
85
+ end
86
+ end
87
+
88
+ def hardcoded_fields
89
+ {
90
+ "country" => "United Kingdom",
91
+ "coordinate_system" => nil,
92
+ "source_data_type" => "dpa",
93
+ "lpi_logical_status_code" => nil,
94
+ "lpi_logical_status_code_description" => nil
95
+ }
96
+ end
97
+
98
+ end
99
+ end
100
+ end
@@ -3,6 +3,7 @@
3
3
  module DefraRuby
4
4
  module Address
5
5
  class OsPlacesAddressLookupService < BaseService
6
+ OS_PLACES_ADDRESS_LOOKUP_NO_MATCH_ERROR_MSG = "Parameters are not valid"
6
7
 
7
8
  def run(postcode)
8
9
  @postcode = postcode
@@ -11,8 +12,6 @@ module DefraRuby
11
12
 
12
13
  private
13
14
 
14
- OS_PLACES_ADDRESS_LOOKUP_NO_MATCH_ERROR_MSG = "Parameters are not valid"
15
-
16
15
  attr_reader :postcode
17
16
 
18
17
  def url
@@ -24,16 +23,16 @@ module DefraRuby
24
23
 
25
24
  def response_exe
26
25
  lambda do
27
- begin
28
- response = RestClient::Request.execute(
29
- method: :get,
30
- url: url,
31
- timeout: DefraRuby::Address.configuration.timeout
32
- )
33
- JSON.parse(response)
34
- rescue RestClient::BadRequest => e
35
- raise process_error(e)
36
- end
26
+
27
+ response = RestClient::Request.execute(
28
+ method: :get,
29
+ url: url,
30
+ timeout: DefraRuby::Address.configuration.timeout
31
+ )
32
+ JSON.parse(response)
33
+ rescue RestClient::BadRequest => e
34
+ raise process_error(e)
35
+
37
36
  end
38
37
  end
39
38
 
@@ -43,8 +42,6 @@ module DefraRuby
43
42
  end
44
43
  rescue StandardError
45
44
  error
46
- ensure
47
- error
48
45
  end
49
46
  end
50
47
  end
@@ -0,0 +1,151 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DefraRuby
4
+ module Address
5
+ # Converts between British National Grid easting/northing and OSGB36
6
+ # latitude/longitude, a Transverse Mercator projection on the Airy 1830
7
+ # ellipsoid. Variable names follow the terms in the OS guide "A guide to
8
+ # coordinate systems in Great Britain".
9
+ #
10
+ # The easting/northing to lat/lon direction is ported from the breasal
11
+ # gem (https://github.com/theodi/breasal, MIT licence, copyright 2013
12
+ # pezholio). Breasal has no reverse conversion, so that direction comes
13
+ # straight from the OS guide's forward projection formulae.
14
+ class TransverseMercatorProjection
15
+ A = HelmertTransformation::AIRY_1830[:a]
16
+ B = HelmertTransformation::AIRY_1830[:b]
17
+ E_SQUARED = ((A * A) - (B * B)) / (A * A)
18
+ N = (A - B) / (A + B)
19
+
20
+ F0 = 0.9996012717
21
+ PHI0 = 49.0 * Math::PI / 180
22
+ LAMBDA0 = -2.0 * Math::PI / 180
23
+ E0 = 400_000.0
24
+ N0 = -100_000.0
25
+
26
+ def self.easting_northing_to_lat_lon(easting, northing)
27
+ new.easting_northing_to_lat_lon(easting, northing)
28
+ end
29
+
30
+ def self.lat_lon_to_easting_northing(latitude, longitude)
31
+ new.lat_lon_to_easting_northing(latitude, longitude)
32
+ end
33
+
34
+ def easting_northing_to_lat_lon(easting, northing)
35
+ phi_prime = converged_phi_prime(northing)
36
+ delta_e = easting - E0
37
+
38
+ [latitude_from(phi_prime, delta_e) * 180 / Math::PI,
39
+ longitude_from(phi_prime, delta_e) * 180 / Math::PI]
40
+ end
41
+
42
+ def lat_lon_to_easting_northing(latitude, longitude)
43
+ phi = latitude * Math::PI / 180
44
+ delta_lambda = (longitude * Math::PI / 180) - LAMBDA0
45
+
46
+ [easting_from(phi, delta_lambda), northing_from(phi, delta_lambda)]
47
+ end
48
+
49
+ private
50
+
51
+ # Converges within a few iterations for any real coordinates; the cap
52
+ # guards against non-finite input looping forever
53
+ def converged_phi_prime(northing)
54
+ phi_prime = ((northing - N0) / (A * F0)) + PHI0
55
+
56
+ 100.times do
57
+ delta = northing - N0 - meridional_arc(phi_prime)
58
+ break if delta < 0.001
59
+
60
+ phi_prime += delta / (A * F0)
61
+ end
62
+
63
+ phi_prime
64
+ end
65
+
66
+ # The methods below are a straight port of the OS guide formulae; the
67
+ # ABC metric counts every arithmetic operator, so is disabled for them.
68
+ # rubocop:disable Metrics/AbcSize
69
+
70
+ # M in the OS guide
71
+ def meridional_arc(phi)
72
+ delta_phi = phi - PHI0
73
+ sum_phi = phi + PHI0
74
+
75
+ B * F0 * (((1 + N + ((5.0 / 4) * (N**2)) + ((5.0 / 4) * (N**3))) * delta_phi) -
76
+ (((3 * N) + (3 * (N**2)) + ((21.0 / 8) * (N**3))) * Math.sin(delta_phi) * Math.cos(sum_phi)) +
77
+ ((((15.0 / 8) * (N**2)) + ((15.0 / 8) * (N**3))) * Math.sin(2 * delta_phi) * Math.cos(2 * sum_phi)) -
78
+ (((35.0 / 24) * (N**3)) * Math.sin(3 * delta_phi) * Math.cos(3 * sum_phi)))
79
+ end
80
+
81
+ # Terms VII, VIII and IX in the OS guide
82
+ def latitude_from(phi_prime, delta_e)
83
+ tan = Math.tan(phi_prime)
84
+ tan2 = tan**2
85
+ nu = nu(phi_prime)
86
+ rho = rho(phi_prime)
87
+ eta_squared = (nu / rho) - 1.0
88
+
89
+ vii = tan / (2 * rho * nu)
90
+ viii = (tan / (24 * rho * (nu**3))) * (5 + (3 * tan2) + eta_squared - (9 * tan2 * eta_squared))
91
+ ix = (tan / (720 * rho * (nu**5))) * (61 + (90 * tan2) + (45 * (tan2**2)))
92
+
93
+ phi_prime - (vii * (delta_e**2)) + (viii * (delta_e**4)) - (ix * (delta_e**6))
94
+ end
95
+
96
+ # Terms X, XI, XII and XIIA in the OS guide
97
+ def longitude_from(phi_prime, delta_e)
98
+ sec = 1.0 / Math.cos(phi_prime)
99
+ tan2 = Math.tan(phi_prime)**2
100
+ nu = nu(phi_prime)
101
+
102
+ x = sec / nu
103
+ xi = (sec / (6 * (nu**3))) * ((nu / rho(phi_prime)) + (2 * tan2))
104
+ xii = (sec / (120 * (nu**5))) * (5 + (28 * tan2) + (24 * (tan2**2)))
105
+ xiia = (sec / (5040 * (nu**7))) * (61 + (662 * tan2) + (1320 * (tan2**2)) + (720 * (tan2**3)))
106
+
107
+ LAMBDA0 + (x * delta_e) - (xi * (delta_e**3)) + (xii * (delta_e**5)) - (xiia * (delta_e**7))
108
+ end
109
+
110
+ # Terms IV, V and VI in the OS guide
111
+ def easting_from(phi, delta_lambda)
112
+ nu = nu(phi)
113
+ rho = rho(phi)
114
+ eta_squared = (nu / rho) - 1.0
115
+ cos_phi = Math.cos(phi)
116
+ tan2 = Math.tan(phi)**2
117
+
118
+ iv = nu * cos_phi
119
+ v = (nu / 6) * (cos_phi**3) * ((nu / rho) - tan2)
120
+ vi = (nu / 120) * (cos_phi**5) * (5 - (18 * tan2) + (tan2**2) + (14 * eta_squared) - (58 * tan2 * eta_squared))
121
+
122
+ E0 + (iv * delta_lambda) + (v * (delta_lambda**3)) + (vi * (delta_lambda**5))
123
+ end
124
+
125
+ # Terms I, II, III and IIIA in the OS guide
126
+ def northing_from(phi, delta_lambda)
127
+ nu = nu(phi)
128
+ eta_squared = (nu / rho(phi)) - 1.0
129
+ sin_phi = Math.sin(phi)
130
+ cos_phi = Math.cos(phi)
131
+ tan2 = Math.tan(phi)**2
132
+
133
+ i = meridional_arc(phi) + N0
134
+ ii = (nu / 2) * sin_phi * cos_phi
135
+ iii = (nu / 24) * sin_phi * (cos_phi**3) * (5 - tan2 + (9 * eta_squared))
136
+ iiia = (nu / 720) * sin_phi * (cos_phi**5) * (61 - (58 * tan2) + (tan2**2))
137
+
138
+ i + (ii * (delta_lambda**2)) + (iii * (delta_lambda**4)) + (iiia * (delta_lambda**6))
139
+ end
140
+ # rubocop:enable Metrics/AbcSize
141
+
142
+ def nu(phi)
143
+ A * F0 * ((1.0 - (E_SQUARED * (Math.sin(phi)**2)))**-0.5)
144
+ end
145
+
146
+ def rho(phi)
147
+ A * F0 * (1.0 - E_SQUARED) * ((1.0 - (E_SQUARED * (Math.sin(phi)**2)))**-1.5)
148
+ end
149
+ end
150
+ end
151
+ end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module DefraRuby
4
4
  module Address
5
- VERSION = "0.1.0"
5
+ VERSION = "0.4.0"
6
6
  end
7
7
  end
@@ -2,12 +2,17 @@
2
2
 
3
3
  require_relative "address/configuration"
4
4
  require_relative "address/ea_address_facade_request"
5
+ require_relative "address/helmert_transformation"
5
6
  require_relative "address/no_match_error"
6
7
  require_relative "address/response"
8
+ require_relative "address/transverse_mercator_projection"
7
9
 
8
10
  require_relative "address/services/base_service"
9
11
  require_relative "address/services/ea_address_facade_v1_1_service"
10
12
  require_relative "address/services/ea_address_facade_v1_service"
13
+ require_relative "address/services/easting_northing_to_lat_lon_service"
14
+ require_relative "address/services/lat_lon_to_easting_northing_service"
15
+ require_relative "address/services/os_api_address_lookup_v1_service"
11
16
  require_relative "address/services/os_places_address_lookup_service"
12
17
 
13
18
  module DefraRuby