Skip to content

Commit

Permalink
Implement Geocoding and Geometry distance search
Browse files Browse the repository at this point in the history
  • Loading branch information
Lockyy committed May 24, 2023
1 parent a1ddc28 commit 7b65b4b
Show file tree
Hide file tree
Showing 11 changed files with 246 additions and 3 deletions.
6 changes: 6 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,12 @@ gem "sentry-ruby"
gem "stimulus-rails"
gem "webpacker"

gem "activerecord-postgis-adapter"
gem "geocoder"
gem "rgeo"
gem "rgeo-geojson"
gem "rgeo-proj4"

gem "net-imap", require: false
gem "net-pop", require: false
gem "net-smtp", require: false
Expand Down
17 changes: 17 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ GEM
activerecord (6.1.7.3)
activemodel (= 6.1.7.3)
activesupport (= 6.1.7.3)
activerecord-postgis-adapter (7.1.1)
activerecord (~> 6.1)
rgeo-activerecord (~> 7.0.0)
activerecord-session_store (2.0.0)
actionpack (>= 5.2.4.1)
activerecord (>= 5.2.4.1)
Expand Down Expand Up @@ -178,6 +181,7 @@ GEM
rack (>= 1.4, < 3)
rack-protection (>= 1.5.3, <= 4.0.0)
sanitize (< 7)
geocoder (1.8.1)
globalid (1.1.0)
activesupport (>= 5.0)
govuk-components (3.1.1)
Expand Down Expand Up @@ -356,6 +360,14 @@ GEM
actionpack (>= 5.2)
railties (>= 5.2)
rexml (3.2.5)
rgeo (3.0.0)
rgeo-activerecord (7.0.1)
activerecord (>= 5.0)
rgeo (>= 1.0.0)
rgeo-geojson (2.1.1)
rgeo (>= 1.0.0)
rgeo-proj4 (4.0.0)
rgeo (~> 3.0.0)
rspec-core (3.12.2)
rspec-support (~> 3.12.0)
rspec-expectations (3.12.3)
Expand Down Expand Up @@ -507,6 +519,7 @@ PLATFORMS
x86_64-linux

DEPENDENCIES
activerecord-postgis-adapter
activerecord-session_store
axe-core-capybara
axe-core-rspec
Expand All @@ -525,6 +538,7 @@ DEPENDENCIES
flipper (~> 0.28.0)
flipper-active_record (~> 0.28.0)
flipper-ui (~> 0.28.0)
geocoder
govuk-components
govuk_design_system_formbuilder
httparty (~> 0.21)
Expand All @@ -551,6 +565,9 @@ DEPENDENCIES
puma (~> 6.2)
rack-attack
rails (~> 6.1.7)
rgeo
rgeo-geojson
rgeo-proj4
rspec-rails (~> 6.0.2)
rubocop-govuk
scss_lint-govuk
Expand Down
45 changes: 45 additions & 0 deletions app/lib/services/geojson_loader.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
module Services
class GeojsonLoader
def self.reload_geojsons(geojson_path)
empty_database
load_geojsons(geojson_path)
end

def self.empty_database
GeoLocalAuthority.delete_all
end

def self.read_file(geojson_path)
file = File.read(geojson_path)
features = RGeo::GeoJSON.decode(file, json_parser: :json)
end

def self.load_geojsons(geojson_path)
Rails.logger.debug("Loading #{geojson_path}")

features = read_file(geojson_path)

features.each do |feature|
geo_local_authority = GeoLocalAuthority.new
geo_local_authority.name = feature.properties["LAD22NM"]
geo_local_authority.geometry = feature.geometry
geo_local_authority.save!
end
end

class Shape
attr_reader :geojson_record

def initialize(geojson_record)
@geojson_record = geojson_record
end

delegate :geometry,
to: :geojson_record

def name
geojson_record.attributes["LAD22NM"]
end
end
end
end
35 changes: 35 additions & 0 deletions app/models/geo_local_authority.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
class GeoLocalAuthority < ApplicationRecord
include RGeo::ActiveRecord::GeometryMixin

SRID = 27_700

def self.nearest_three_to(string)
location = Geocoder.search(string).first
by_distance(location.longitude.to_f, location.latitude.to_f).limit(3)
end

# A method that takes in a longitude and latitude and finds the nearest GeoLocalAuthority
# to that point.
# Converts the longitude and latitude to a point in the SRID 27700 coordinate system
# and then finds the nearest GeoLocalAuthority to that point.
# Returns the nearest GeoLocalAuthority.
def self.by_distance(longitude, latitude)
# Transform the given lon/lat to have an SRID of 27700 (the SRID of the GeoLocalAuthority geometries)
transformed_point = RGeo::CoordSys::Proj4.transform_coords(
RGeo::CoordSys::Proj4.new("EPSG:4326"),
RGeo::CoordSys::Proj4.new("EPSG:27700"),
longitude,
latitude,
)

target_latitude = transformed_point[1]
target_longitude = transformed_point[0]

# Create a point representing the target coordinates, with the SRID specified
point_sql = "SRID=#{SRID};POINT(#{target_longitude} #{target_latitude})"

arel = Arel.sql("geometry <-> '#{point_sql}'::geometry")

order(arel)
end
end
4 changes: 2 additions & 2 deletions config/database.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,12 @@
# gem 'pg'
#
default: &default
adapter: postgresql
adapter: postgis
encoding: unicode
# For details on connection pooling, see Rails configuration guide
# http://guides.rubyonrails.org/configuring.html#database-pooling
pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
url: <%= ENV.fetch("DATABASE_URL", "postgres://localhost:5432") %>
url: <%= ENV.fetch("DATABASE_URL", "postgis://localhost:5432") %>

development:
<<: *default
Expand Down
5 changes: 5 additions & 0 deletions db/migrate/20230502125034_enable_postgis.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
class EnablePostgis < ActiveRecord::Migration[6.1]
def change
enable_extension "postgis"
end
end
12 changes: 12 additions & 0 deletions db/migrate/20230502125316_create_geo_local_authorities.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
class CreateGeoLocalAuthorities < ActiveRecord::Migration[6.1]
def change
create_table :geo_local_authorities do |t|
t.string :name
t.geometry :geometry, srid: 27_700

t.timestamps
end

add_index :geo_local_authorities, :geometry, using: :gist
end
end
11 changes: 10 additions & 1 deletion db/schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,13 @@
#
# It's strongly recommended that you check this file into your version control system.

ActiveRecord::Schema.define(version: 2023_04_14_121939) do
ActiveRecord::Schema.define(version: 2023_05_02_125316) do

# These are extensions that must be enabled in order to support this database
enable_extension "btree_gin"
enable_extension "citext"
enable_extension "plpgsql"
enable_extension "postgis"

create_table "applications", force: :cascade do |t|
t.bigint "user_id", null: false
Expand Down Expand Up @@ -110,6 +111,14 @@
t.index ["feature_key", "key", "value"], name: "index_flipper_gates_on_feature_key_and_key_and_value", unique: true
end

create_table "geo_local_authorities", force: :cascade do |t|
t.string "name"
t.geometry "geometry", limit: {:srid=>27700, :type=>"geometry"}
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
t.index ["geometry"], name: "index_geo_local_authorities_on_geometry", using: :gist
end

create_table "get_an_identity_webhook_messages", force: :cascade do |t|
t.jsonb "raw"
t.jsonb "message"
Expand Down

Large diffs are not rendered by default.

23 changes: 23 additions & 0 deletions lib/tasks/geojson_loading.rake
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
namespace :geojson do
desc "Load geojsons into the database, will empty local authorities table first"
task load: :environment do |_t, _args|
geojson_path = "lib/local_authority_geojson/Local_Authority_Districts_December_2022_Boundaries_UK_BUC_143497700576642915.geojson"

puts("Loading geojsons from #{geojson_path}")

Services::GeojsonLoader.reload_geojsons(geojson_path)
end

desc "Load geojson file and output local authorities found"
task test: :environment do |_t, _args|
geojson_path = "lib/local_authority_geojson/Local_Authority_Districts_December_2022_Boundaries_UK_BUC_143497700576642915.geojson"

features = Services::GeojsonLoader.read_file(geojson_path)

puts "File contains #{features.count} local authority geometries"

local_authorities_found = features.map { |feature| feature.properties["LAD22NM"] }.sort.join(", ")

puts "Local Authorities found: #{local_authorities_found}"
end
end
90 changes: 90 additions & 0 deletions spec/models/geo_local_authority_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
require "rails_helper"

RSpec.describe GeoLocalAuthority, type: :model do
before do
geojson_path = "lib/local_authority_geojsons/Local_Authority_Districts_December_2022_Boundaries_UK_BUC_143497700576642915.geojson"

Services::GeojsonLoader.reload_geojsons(geojson_path)

Geocoder.configure(lookup: :test)
Geocoder::Lookup::Test.add_stub(
"Department for Education, Westminster", [{
"latitude" => 51.4979314,
"longitude" => -0.13016544222858017,
"address" => "Department for Education, 20, Great Smith Street, Westminster, Millbank, London, Greater London, England, SW1P 3BT, United Kingdom",
"state" => "England",
"country" => "United Kingdom",
"country_code" => "gb",
}]
)
Geocoder::Lookup::Test.add_stub(
"Manchester Airport", [{
"latitude" => 53.350342049999995,
"longitude" => -2.280369252664295,
"address" => "Manchester Airport, Castle Mill Lane, Ashley, Manchester, Cheshire East, England, M90 1QX, United Kingdom",
"state" => "England",
"country" => "United Kingdom",
"country_code" => "gb",
}]
)
Geocoder::Lookup::Test.add_stub(
"Belfast City Airport", [{
"latitude" => 54.614828,
"longitude" => -5.8703437,
"address" => "Bushmills Bar, Sydenham Bypass, Sydenham, Belfast, County Down, Ulster, Northern Ireland, BT3 9JH, United Kingdom",
"state" => "Northern Ireland",
"country" => "United Kingdom",
"country_code" => "gb",
}]
)
Geocoder::Lookup::Test.add_stub(
"Edinburgh Airport", [{
"latitude" => 55.950128899999996,
"longitude" => -3.3595079855289756,
"address" => "Edinburgh Airport, Meadowfield Road, Gogar, City of Edinburgh, Scotland, EH12 0AU, United Kingdom",
"state" => "Scotland",
"country" => "United Kingdom",
"country_code" => "gb",
}]
)
Geocoder::Lookup::Test.add_stub(
"Cardiff Airport", [{
"latitude" => 51.397871550000005,
"longitude" => -3.3445890119919994,
"address" => "Cardiff Airport, B4265, Fonmon, Rhoose, Penmark, Vale of Glamorgan, Wales, CF62 3BL, United Kingdom",
"state" => "Wales",
"country" => "United Kingdom",
"country_code" => "gb",
}]
)

Geocoder::Lookup::Test.add_stub(
"Sydney Opera House", [{
"latitude" => -33.85719805,
"longitude" => 151.21512338473752,
"address" => "Sydney Opera House, 2, Macquarie Street, Quay Quarter, Sydney, Council of the City of Sydney, New South Wales, 2000, Australia",
"state" => "New South Wales",
"country" => "Australia",
"country_code" => "au",
}]
)
end

describe "#nearest_three_to" do
def check_nearest_three_to(location)
described_class.nearest_three_to(location).pluck(:name)
end

it "returns the three nearest local authorities" do
# Do some quick checks around the UK to check it works in various areas
expect(check_nearest_three_to("Department for Education, Westminster")).to eq(%w[Westminster Lambeth Wandsworth])
expect(check_nearest_three_to("Manchester Airport")).to eq(["Manchester", "Cheshire East", "Trafford"])
expect(check_nearest_three_to("Belfast City Airport")).to eq(["Belfast", "Ards and North Down", "Lisburn and Castlereagh"])
expect(check_nearest_three_to("Edinburgh Airport")).to eq(["City of Edinburgh", "West Lothian", "Fife"])
expect(check_nearest_three_to("Cardiff Airport")).to eq(["Vale of Glamorgan", "Cardiff", "Rhondda Cynon Taf"])

# And one across the world to check it doesn't break
expect(check_nearest_three_to("Sydney Opera House")).to eq(["Cornwall", "Isles of Scilly", "South Hams"])
end
end
end

0 comments on commit 7b65b4b

Please sign in to comment.