Skip to content

Commit 3f37d24

Browse files
committed
Revert "uk polygons take II"
This reverts commit 7f37921.
1 parent 37713a5 commit 3f37d24

File tree

63 files changed

+865
-7544
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

63 files changed

+865
-7544
lines changed

.simplecov

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,8 @@ if ENV.fetch("COVERAGE", 0).to_i.positive?
5454
# .25% lower (branch) and .1% lower (line) than the test run for now
5555
#
5656
# possibly the tests are stable now?
57-
# SD 27/01/26 branch coverage still appears to be ~ .1% unstable
58-
# 97.47% (12400 / 12722) -> 322 87.11% (2785 / 3197) -> 197 + 215 = 412
59-
minimum_coverage line: 97.45, branch: 87.1
57+
# SD 16/01/26 branch coverage still appears to be ~ .1% unstable
58+
# 97.48% (12352 / 12673) -> 322 86.37% (2776 / 3214) -> 185 + 225 = 410
59+
minimum_coverage line: 97.44, branch: 87.12
6060
end
6161
end

app/helpers/maps_helper.rb

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,26 @@
11
module MapsHelper
22
def vacancy_organisations_map_markers(vacancy)
3-
vacancy.organisations.filter_map do |organisation|
4-
if organisation.uk_geopoint?
5-
{
6-
id: vacancy.id,
7-
parent_id: organisation.id,
8-
geopoint: RGeo::GeoJSON.encode(GeoFactories.convert_sr27700_to_wgs84(organisation.uk_geopoint)).to_json,
9-
}
10-
end
3+
vacancy.organisations.map do |organisation|
4+
{
5+
id: vacancy.id,
6+
parent_id: organisation.id,
7+
geopoint: RGeo::GeoJSON.encode(organisation.geopoint)&.to_json,
8+
}
119
end
1210
end
1311

1412
def organisation_map_marker(organisation)
1513
[
1614
{
1715
parent_id: organisation.id,
18-
geopoint: RGeo::GeoJSON.encode(GeoFactories.convert_sr27700_to_wgs84(organisation.uk_geopoint)).to_json,
16+
geopoint: RGeo::GeoJSON.encode(organisation.geopoint)&.to_json,
1917
},
2018
]
2119
end
2220

2321
def organisation_map_can_be_displayed?(vacancy)
2422
return true unless vacancy.central_office?
2523

26-
# :nocov:
27-
vacancy.organisation.uk_geopoint.present?
28-
# :nocov:
24+
vacancy.organisation.geopoint.present?
2925
end
3026
end

app/models/job_preferences/location.rb

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,27 +6,28 @@ class Location < ApplicationRecord
66

77
self.table_name = "job_preferences_locations"
88
belongs_to :job_preferences
9-
self.ignored_columns += %i[area]
109

1110
validates :name, presence: true
1211
validates :radius, presence: true
1312
validates :uk_area, presence: true
1413

1514
before_validation :set_area
1615

17-
scope :containing, ->(point) { where(arel_table[:uk_area].st_contains(point)) }
16+
scope :containing, ->(point) { where("ST_Within(ST_GeomFromEWKT(?), area::geometry)", "SRID=4326;#{point.as_text}") }
1817

1918
private
2019

2120
def set_area
2221
if LocationPolygon.contain?(name)
23-
# :nocov:
24-
self.uk_area = LocationPolygon.buffered(radius).with_name(name).uk_area
25-
# :nocov:
22+
polygon = LocationPolygon.buffered(radius).with_name(name)
23+
self.area = polygon.area
24+
self.uk_area = polygon.uk_area
2625
else
2726
lat, long = Geocoding.new(name).coordinates.map(&:to_s)
2827
radius_meters = convert_miles_to_metres(Search::RadiusBuilder.new(name, radius).radius)
29-
self.uk_area = GeoFactories.convert_wgs84_to_sr27700(GeoFactories::FACTORY_4326.point(long, lat).buffer(radius_meters))
28+
geopoint = GeoFactories::FACTORY_4326.point(long, lat)
29+
self.area = geopoint.buffer(radius_meters)
30+
self.uk_area = GeoFactories.convert_wgs84_to_sr27700(geopoint).buffer(radius_meters)
3031
end
3132
end
3233
end

app/models/location_polygon.rb

Lines changed: 40 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,19 @@
11
class LocationPolygon < ApplicationRecord
22
extend DistanceHelper
33

4+
# British National Grid SRID (EPSG:27700) is a projected coordinate system used for mapping in Great Britain.
5+
# It provides coordinates in meters, which is useful for distance calculations, which we need
6+
# for radius-based searches.
7+
# It is significantly more accurate for distance calculations in Great Britain that EPSG:3857 (Web Mercator).
8+
# EPSG:3857 distort distances and areas, especially as you move away from the equator. What would cause a multiplier
9+
# between 1.5x and 1.7x for radius/buffer distances in our case to get the matches we would expect.
10+
BRITISH_NATIONAL_GRID_SRID = 27700 # rubocop:disable Style/NumericLiterals
11+
412
validates :name, presence: true
513

614
# Scope that expands any polygons returned by subsequent scopes by the provided radius
715
# by overriding the `area` attribute with a buffered version of itself
8-
scope :buffered, ->(radius_in_miles) { select("*, ST_Buffer(uk_area, #{convert_miles_to_metres(radius_in_miles || 0)}) AS uk_area") }
9-
10-
self.ignored_columns += %i[area centroid]
16+
scope :buffered, ->(radius_in_miles) { select("*, ST_Buffer(area, #{convert_miles_to_metres(radius_in_miles || 0)}) AS area") }
1117

1218
def self.with_name(location)
1319
find_by(name: mapped_name(location))
@@ -23,21 +29,48 @@ def self.mapped_name(location)
2329

2430
def self.find_valid_for_location(location)
2531
polygon = with_name(location)
26-
if polygon.present? && polygon.uk_area.invalid_reason.nil?
32+
if polygon.present? && polygon.area.invalid_reason.nil?
2733
polygon
2834
end
2935
rescue RGeo::Error::InvalidGeometry
3036
nil
3137
end
3238

3339
# Buffers the polygon's area by the given radius in metres and returns the resulting expanded area in geometry.
40+
# Transformations are needed since the original area is a geographic type.
41+
#
3442
# The "where & pick" approach is more efficient to retrieve a single computed value from the DB than loading the whole object.
43+
#
44+
# Why buffering in British National Grid SRID (27700) to then store it as SRID 4326?
45+
# Buffering is best done in a projected coordinate system as it buffers in metres instead of degrees).
46+
# After buffering, we transform to SRID: 4326 (lat/lon data). As we're doing spatial queries on this data, 4326 is more appropriate.
3547
def buffered_geometry_area(radius_in_metres)
48+
wkb = self.class.where(id: id).pick(
49+
Arel.sql(
50+
"ST_AsBinary(
51+
ST_Transform(
52+
ST_Buffer(
53+
ST_Transform(area::geometry, #{BRITISH_NATIONAL_GRID_SRID}),
54+
#{radius_in_metres}
55+
),
56+
4326)
57+
)",
58+
),
59+
)
60+
return nil unless wkb
61+
62+
# Has to use this or the returned geometry gets a SRID: 0 even when set in the DB as 4326
63+
RGeo::Cartesian.factory(srid: 4326).parse_wkb(wkb)
64+
rescue RGeo::Error::InvalidGeometry
65+
nil
66+
end
67+
68+
# Buffers the polygon's area by the given radius in metres and returns the resulting expanded area in geometry.
69+
# The "where & pick" approach is more efficient to retrieve a single computed value from the DB than loading the whole object.
70+
def buffered_geometry_uk_area(radius_in_metres)
3671
wkb = self.class.where(id: id)
37-
.pick(Arel.sql("ST_AsBinary(ST_Buffer(uk_area::geometry,#{radius_in_metres}))"))
72+
.pick(Arel.sql("ST_AsBinary(ST_Buffer(uk_area::geometry,#{radius_in_metres}))"))
3873
# Has to use this or the returned geometry gets a SRID: 0 even when set in the DB as 27700
3974
GeoFactories::FACTORY_27700.parse_wkb(wkb) if wkb
40-
rescue RGeo::Error::InvalidGeometry
41-
nil
4275
end
4376
end

app/models/organisation.rb

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,6 @@ class Organisation < ApplicationRecord
2424
has_many :jobseeker_profile_exclusions, class_name: "JobseekerProfileExcludedOrganisation"
2525
has_many :hidden_jobseeker_profiles, through: :jobseeker_profile_exclusions, source: :jobseeker_profile
2626

27-
self.ignored_columns += %i[geopoint]
28-
2927
scope :not_closed, -> { where.not(establishment_status: "Closed") }
3028
scope :schools, -> { where(type: "School") }
3129
scope :school_groups, -> { where(type: "SchoolGroup") }

app/models/subscription.rb

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,6 @@ class Subscription < ApplicationRecord
2020
# support_job_roles used to be called teaching_support_job_roles and non_teaching_support_job_roles in the past, and there are still active subscriptions with this name
2121
JOB_ROLE_ALIASES = %i[teaching_job_roles support_job_roles teaching_support_job_roles non_teaching_support_job_roles].freeze
2222

23-
self.ignored_columns += %i[area geopoint]
24-
2523
def self.encryptor(serializer: :json_allow_marshal)
2624
key_generator_secret = SUBSCRIPTION_KEY_GENERATOR_SECRET
2725
key_generator_salt = SUBSCRIPTION_KEY_GENERATOR_SALT
@@ -98,16 +96,25 @@ def set_location_data!
9896

9997
# A subscription with location area has a polygon area seat buffered by radius, no geopoint.
10098
def set_location_from_polygon(polygon, radius)
101-
self.uk_area = polygon.buffered_geometry_area(self.class.convert_miles_to_metres(radius))
99+
# Cast polygon area from geography to geometry and buffer by radius before storing
100+
self.area = polygon.buffered_geometry_area(self.class.convert_miles_to_metres(radius))
101+
self.geopoint = nil
102+
103+
self.uk_area = polygon.buffered_geometry_uk_area(self.class.convert_miles_to_metres(radius))
102104
self.uk_geopoint = nil
105+
103106
self.radius_in_metres = self.class.convert_miles_to_metres(radius)
104107
end
105108

106109
# A subscription with location coordinates has a geopoint, no area.
107110
def set_location_from_coordinates(coordinates, radius)
111+
self.geopoint = RGeo::Cartesian.factory(srid: 4326).point(coordinates.second, coordinates.first)
112+
self.area = nil
113+
108114
geopoint = GeoFactories::FACTORY_4326.point(coordinates.second, coordinates.first)
109115
self.uk_geopoint = GeoFactories.convert_wgs84_to_sr27700 geopoint
110116
self.uk_area = nil
117+
111118
self.radius_in_metres = self.class.convert_miles_to_metres(radius)
112119
end
113120
end

app/models/vacancy.rb

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -157,8 +157,6 @@ class Vacancy < ApplicationRecord
157157

158158
after_save :update_conversation_searchable_content, if: -> { saved_change_to_job_title? }
159159

160-
self.ignored_columns += %i[geolocation]
161-
162160
EQUAL_OPPORTUNITIES_PUBLICATION_THRESHOLD = 5
163161
EXPIRY_TIME_OPTIONS = %w[8:00 9:00 12:00 15:00 23:59].freeze
164162

@@ -270,11 +268,11 @@ def salary_types
270268
end
271269

272270
def distance_in_miles_to(search_coordinates)
273-
if uk_geolocation.is_a? RGeo::Cartesian::MultiPointImpl
271+
if geolocation.is_a? RGeo::Geographic::SphericalMultiPointImpl
274272
# if there are multiple geolocations then return the distance to the nearest one to the given search location
275-
uk_geolocation.map { |geolocation| self.class.calculate_distance(search_coordinates, geolocation) }.min
273+
geolocation.map { |geolocation| calculate_distance(search_coordinates, geolocation) }.min
276274
else
277-
self.class.calculate_distance(search_coordinates, uk_geolocation)
275+
calculate_distance(search_coordinates, geolocation)
278276
end
279277
end
280278

@@ -305,12 +303,8 @@ def update_conversation_searchable_content
305303
.find_each(&:update_searchable_content)
306304
end
307305

308-
class << self
309-
def calculate_distance(search_coordinates, geolocation)
310-
search_location = GeoFactories::FACTORY_4326.point(search_coordinates.second, search_coordinates.first)
311-
search_point = GeoFactories.convert_wgs84_to_sr27700 search_location
312-
search_point.distance(geolocation) / DistanceHelper::METRES_PER_MILE
313-
end
306+
def calculate_distance(search_coordinates, geolocation)
307+
Geocoder::Calculations.distance_between(search_coordinates, [geolocation.latitude, geolocation.longitude])
314308
end
315309

316310
def slug_candidates
@@ -323,12 +317,18 @@ def slug_candidates
323317
# In the former case, it gets an argument, which we don't need and thus ignore
324318
#
325319
def refresh_geolocation(_school_added_or_removed = nil)
326-
self.uk_geolocation = if organisations.one?
327-
organisation.uk_geopoint
328-
else
329-
uk_points = organisations.filter_map(&:uk_geopoint)
330-
uk_points.presence && uk_points.first.factory.multi_point(uk_points)
331-
end
320+
self.geolocation = if organisations.one?
321+
organisation.geopoint
322+
else
323+
points = organisations.filter_map(&:geopoint)
324+
points.presence && points.first.factory.multi_point(points)
325+
end
326+
# self.uk_geolocation = if organisations.one?
327+
# organisation.uk_geopoint
328+
# else
329+
# uk_points = organisations.filter_map(&:uk_geopoint)
330+
# uk_points.presence && uk_points.first.factory.multi_point(uk_points)
331+
# end
332332
end
333333
end
334334
# rubocop:enable Metrics/ClassLength

app/queries/location_query.rb

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ def nationwide_location?(query)
3232
def handle_polygon_location(field_name, polygon, radius, sort_by_distance)
3333
@scope = scope.joins("
3434
INNER JOIN location_polygons
35-
ON ST_DWithin(#{field_name}, location_polygons.uk_area, #{radius})
35+
ON ST_DWithin(#{field_name}, location_polygons.area, #{radius}, false)
3636
").where("location_polygons.id = ?", polygon.id)
3737

3838
sort_by_polygon_distance(field_name) if sort_by_distance
@@ -47,24 +47,23 @@ def handle_coordinates(field_name, query, radius, sort_by_distance)
4747
# suitable error instead. Refactor later!
4848
return scope.none if coordinates == [0, 0]
4949

50-
earth_point = GeoFactories::FACTORY_4326.point(coordinates.second, coordinates.first)
51-
point = GeoFactories.convert_wgs84_to_sr27700 earth_point
52-
@scope = scope.where("ST_DWithin(#{field_name}, ?, ?)", "SRID=#{point.srid};#{point}", radius)
50+
point = "POINT(#{coordinates.second} #{coordinates.first})"
51+
@scope = scope.where("ST_DWithin(#{field_name}, ?, ?, false)", point, radius)
5352

5453
sort_by_coordinates_distance(field_name, point) if sort_by_distance
5554

5655
scope
5756
end
5857

5958
def sort_by_polygon_distance(field_name)
60-
@scope = scope.select("vacancies.*, ST_Distance(#{field_name}, location_polygons.uk_centroid) AS distance")
59+
@scope = scope.select("vacancies.*, ST_Distance(#{field_name}, location_polygons.centroid, false) AS distance")
6160
# why not using 'distance' alias? is not defined when calling this query with a 'pluck'
62-
.order(Arel.sql("ST_Distance(#{field_name}, location_polygons.uk_centroid)"))
61+
.order(Arel.sql("ST_Distance(#{field_name}, location_polygons.centroid, false)"))
6362
end
6463

6564
def sort_by_coordinates_distance(field_name, point)
66-
@scope = scope.select("vacancies.*, ST_Distance(#{field_name}, 'SRID=#{point.srid};#{point}') AS distance")
65+
@scope = scope.select("vacancies.*, ST_Distance(#{field_name}, '#{point}', false) AS distance")
6766
# why not using 'distance' alias? is not defined when calling this query with a 'pluck'
68-
.order(Arel.sql("ST_Distance(#{field_name}, 'SRID=#{point.srid};#{point}')"))
67+
.order(Arel.sql("ST_Distance(#{field_name}, '#{point}', false)"))
6968
end
7069
end

app/queries/organisation_location_query.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,6 @@ def initialize(scope = Organisation.all)
44
end
55

66
def call(...)
7-
super("organisations.uk_geopoint", ...)
7+
super("organisations.geopoint", ...)
88
end
99
end

app/queries/subscription_vacancies_matching_query.rb

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,14 @@
11
class SubscriptionVacanciesMatchingQuery
22
attr_reader :scope
33

4+
# British National Grid SRID (EPSG:27700) is a projected coordinate system used for mapping in Great Britain.
5+
# It provides coordinates in meters, which is useful for distance calculations, which we need
6+
# for radius-based searches.
7+
# It is significantly more accurate for distance calculations in Great Britain that EPSG:3857 (Web Mercator).
8+
# EPSG:3857 distort distances and areas, especially as you move away from the equator. What would cause a multiplier
9+
# between 1.5x and 1.7x for radius/buffer distances in our case to get the matches we would expect.
10+
BRITISH_NATIONAL_GRID_SRID = 27700 # rubocop:disable Style/NumericLiterals
11+
412
# Builds the query to find the IDs for the vacancies matching the subscription criteria.
513
# The subquery is needed to be able to combine our requirements into a valid single SQL statement:
614
# - retrieve unique DISTINCT ON vacancy ids: As the location filter may return the same vacancy multiple times (vacancy for multiple orgs).
@@ -169,9 +177,9 @@ def location_filter(scope, subscription_location, subscription)
169177
# 'area_before_type_cast' and 'geopoint_before_type_cast' are used to avoid casting the fields into RGeo objects.
170178
# This reduces memory usage and speeds up the query. As we don't need to use the actual objects in Ruby code,
171179
# just need to know if they are present in DB.
172-
if subscription.uk_area_before_type_cast.present?
180+
if subscription.area_before_type_cast.present?
173181
location_by_area_filter(scope, subscription)
174-
elsif subscription.uk_geopoint_before_type_cast.present? && subscription.radius_in_metres.present?
182+
elsif subscription.geopoint_before_type_cast.present? && subscription.radius_in_metres.present?
175183
location_by_geopoint_filter(scope, subscription)
176184
else
177185
scope.none # Invalid location filter (having no area or geopoint) returns no matches
@@ -189,11 +197,9 @@ def location_filter(scope, subscription_location, subscription)
189197
# Including 'publish_on' in the distinct: The provided scope may be ordered by publish on date. When using distinct
190198
# with ordering, Postgres requires all selected columns to be included in the distinct clause or will fail during execution.
191199
def location_by_area_filter(scope, subscription)
192-
subscriptions = Subscription.arel_table
193-
organisations = Organisation.arel_table
194200
scope.joins("INNER JOIN subscriptions ON subscriptions.id = '#{subscription.id}'")
195201
.joins(:organisations)
196-
.where(subscriptions[:uk_area].st_contains(organisations[:uk_geopoint]))
202+
.where("ST_Contains(subscriptions.area, organisations.geopoint::geometry)")
197203
end
198204

199205
# Filter vacancies where their organisations' geopoints fall within the subscription's radius from the subscription's
@@ -208,8 +214,8 @@ def location_by_area_filter(scope, subscription)
208214
def location_by_geopoint_filter(scope, subscription)
209215
scope.joins("INNER JOIN subscriptions ON subscriptions.id = '#{subscription.id}'")
210216
.joins(:organisations)
211-
.where("ST_DWithin(organisations.uk_geopoint::geometry,
212-
subscriptions.uk_geopoint,
217+
.where("ST_DWithin(ST_Transform(organisations.geopoint::geometry, #{BRITISH_NATIONAL_GRID_SRID}),
218+
ST_Transform(subscriptions.geopoint, #{BRITISH_NATIONAL_GRID_SRID}),
213219
subscriptions.radius_in_metres)")
214220
end
215221
end

0 commit comments

Comments
 (0)