Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,13 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
ruby-version: ["3.2"]
gemfile: ["rails_7_0"]
ruby-version: ["3.2", "4.0"]
gemfile: ["rails_7_0", "rails_8_0"]
exclude:
- ruby-version: "4.0"
gemfile: "rails_7_0"
- ruby-version: "3.2"
gemfile: "rails_8_0"
fail-fast: false

env:
Expand All @@ -38,7 +43,7 @@ jobs:
--health-retries 5

steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v5

# libgeos and gdal are required and must be installed before `bundle install`
# NB: if you add libs here that are required for Gem native extensions be sure to clear the
Expand Down
26 changes: 13 additions & 13 deletions app/models/abstract_feature.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ class AbstractFeature < ActiveRecord::Base
class_attribute :lowres_simplification
self.lowres_simplification = 2 # Threshold in meters

belongs_to :spatial_model, :polymorphic => :true, :autosave => false
belongs_to :spatial_model, polymorphic: :true, autosave: false

attr_writer :make_valid

Expand All @@ -16,7 +16,7 @@ class AbstractFeature < ActiveRecord::Base
validates_presence_of :geog
validate :validate_geometry, if: :will_save_change_to_geog?
before_save :sanitize, if: :will_save_change_to_geog?
after_save :cache_derivatives, :if => [:automatically_cache_derivatives?, :saved_change_to_geog?]
after_save :cache_derivatives, if: [:automatically_cache_derivatives?, :saved_change_to_geog?]

def self.cache_key
collection_cache_key
Expand All @@ -41,15 +41,15 @@ def self.metadata_keys
end

def self.polygons
where(:feature_type => 'polygon')
where(feature_type: 'polygon')
end

def self.lines
where(:feature_type => 'line')
where(feature_type: 'line')
end

def self.points
where(:feature_type => 'point')
where(feature_type: 'point')
end

def self.within_distance_of_point(lat, lng, distance_in_meters, geom = 'geom_lowres')
Expand All @@ -59,7 +59,7 @@ def self.within_distance_of_point(lat, lng, distance_in_meters, geom = 'geom_low
else "ST_Transform(ST_SetSRID(ST_Point(:lng, :lat), 4326), #{detect_srid(geom)})"
end

binds = { :lng => lng.to_d, :lat => lat.to_d }
binds = { lng: lng.to_d, lat: lat.to_d }

within_distance_of_sql(point_sql, distance_in_meters, geom, **binds)
end
Expand All @@ -71,7 +71,7 @@ def self.within_distance_of_line(points, distance_in_meters, geom = 'geom_lowres
else "ST_Transform(ST_SetSRID(:points::geometry, 4326), #{detect_srid(geom)})"
end

binds = { :points => "LINESTRING(#{points.map {|coords| coords.join(' ') }.join(', ')})" }
binds = { points: "LINESTRING(#{points.map {|coords| coords.join(' ') }.join(', ')})" }

within_distance_of_sql(point_sql, distance_in_meters, geom, **binds)
end
Expand All @@ -83,14 +83,14 @@ def self.within_distance_of_polygon(points, distance_in_meters, geom = 'geom_low
else "ST_Transform(ST_Polygon(:points::geometry, 4326), #{detect_srid(geom)})"
end

binds = { :points => "LINESTRING(#{points.map {|coords| coords.join(' ') }.join(', ')})" }
binds = { points: "LINESTRING(#{points.map {|coords| coords.join(' ') }.join(', ')})" }

within_distance_of_sql(point_sql, distance_in_meters, geom, **binds)
end

def self.within_distance_of_sql(geometry_sql, distance_in_meters, features_column = 'geom_lowres', **binds)
if distance_in_meters.to_f > 0
where("ST_DWithin(features.#{features_column}, #{geometry_sql}, :distance)", **binds, :distance => distance_in_meters)
where("ST_DWithin(features.#{features_column}, #{geometry_sql}, :distance)", **binds, distance: distance_in_meters)
else
where("ST_Intersects(features.#{features_column}, #{geometry_sql})", **binds)
end
Expand Down Expand Up @@ -128,7 +128,7 @@ def self.valid
end

def envelope(buffer_in_meters = 0)
envelope_json = JSON.parse(self.class.select("ST_AsGeoJSON(ST_Envelope(ST_Buffer(features.geog, #{buffer_in_meters})::geometry)) AS result").where(:id => id).first.result)
envelope_json = JSON.parse(self.class.select("ST_AsGeoJSON(ST_Envelope(ST_Buffer(features.geog, #{buffer_in_meters})::geometry)) AS result").where(id: id).first.result)
envelope_json = envelope_json["coordinates"].first

raise "Can't calculate envelope for Feature #{self.id}" if envelope_json.blank?
Expand Down Expand Up @@ -257,12 +257,12 @@ def bounds
end

def cache_derivatives(*args)
self.class.default_scoped.where(:id => self.id).cache_derivatives(*args)
self.class.default_scoped.where(id: self.id).cache_derivatives(*args)
end

def kml(options = {})
column = options[:lowres] ? 'geom_lowres' : 'geog'
return SpatialFeatures::Utils.select_db_value(self.class.where(:id => id).select("ST_AsKML(#{column}, 6)"))
return SpatialFeatures::Utils.select_db_value(self.class.where(id: id).select("ST_AsKML(#{column}, 6)"))
end

def geojson(*args)
Expand Down Expand Up @@ -290,7 +290,7 @@ def self.detect_srid(column_name)
end

def self.join_other_features(other)
joins('INNER JOIN features AS other_features ON true').where(:other_features => {:id => other})
joins('INNER JOIN features AS other_features ON true').where(other_features: {id: other})
end

def validate_geometry
Expand Down
4 changes: 2 additions & 2 deletions app/models/aggregate_feature.rb
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
require_dependency SpatialFeatures::Engine.root.join('app/models/abstract_feature')

class AggregateFeature < AbstractFeature
has_many :features, lambda { |aggregate| where(:spatial_model_type => aggregate.spatial_model_type) }, :foreign_key => :spatial_model_id, :primary_key => :spatial_model_id
has_many :features, lambda { |aggregate| where(spatial_model_type: aggregate.spatial_model_type) }, foreign_key: :spatial_model_id, primary_key: :spatial_model_id

# Aggregate the features for the spatial model into a single feature
before_validation :set_geog, :on => :create, :unless => :geog?
before_validation :set_geog, on: :create, unless: :geog?

private

Expand Down
12 changes: 6 additions & 6 deletions app/models/feature.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ class Feature < AbstractFeature
class_attribute :lowres_precision
self.lowres_precision = 5

has_one :aggregate_feature, lambda { |feature| where(:spatial_model_type => feature.spatial_model_type) }, :foreign_key => :spatial_model_id, :primary_key => :spatial_model_id
has_one :aggregate_feature, lambda { |feature| where(spatial_model_type: feature.spatial_model_type) }, foreign_key: :spatial_model_id, primary_key: :spatial_model_id

scope :source_identifier, lambda {|source_identifier| where(:source_identifier => source_identifier) if source_identifier.present? }
scope :source_identifier, lambda {|source_identifier| where(source_identifier: source_identifier) if source_identifier.present? }

before_save :truncate_name

Expand All @@ -21,7 +21,7 @@ def self.defer_aggregate_refresh(&block)
start_at = Feature.maximum(:id).to_i + 1
output = without_aggregate_refresh(&block)

where(:id => start_at..Float::INFINITY).refresh_aggregates
where(id: start_at..Float::INFINITY).refresh_aggregates

return output
end
Expand All @@ -36,13 +36,13 @@ def self.without_aggregate_refresh

def self.refresh_aggregates
# Find one feature from each spatial model and trigger the aggregate feature refresh
ids = where.not(:spatial_model_type => nil)
.where.not(:spatial_model_id => nil)
ids = where.not(spatial_model_type: nil)
.where.not(spatial_model_id: nil)
.group('spatial_model_type, spatial_model_id')
.pluck('MAX(id)')

# Unscope so that newly built AggregateFeatures get their type column set correctly
AbstractFeature.unscoped { where(:id => ids).find_each(&:refresh_aggregate) }
AbstractFeature.unscoped { where(id: ids).find_each(&:refresh_aggregate) }
end

def refresh_aggregate
Expand Down
4 changes: 2 additions & 2 deletions app/models/spatial_cache.rb
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
class SpatialCache < ActiveRecord::Base
belongs_to :spatial_model, :polymorphic => true, :inverse_of => :spatial_caches
belongs_to :spatial_model, polymorphic: true, inverse_of: :spatial_caches

def self.between(spatial_model, klass)
where(SpatialFeatures::Utils.polymorphic_condition(spatial_model, 'spatial_model'))
.where(:intersection_model_type => SpatialFeatures::Utils.class_name_with_ancestors(klass))
.where(intersection_model_type: SpatialFeatures::Utils.class_name_with_ancestors(klass))
end

def stale?
Expand Down
4 changes: 2 additions & 2 deletions app/models/spatial_proximity.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
class SpatialProximity < ActiveRecord::Base
belongs_to :model_a, :polymorphic => true
belongs_to :model_b, :polymorphic => true
belongs_to :model_a, polymorphic: true
belongs_to :model_b, polymorphic: true

def self.between(scope1, scope2)
where condition_sql(scope1, scope2, <<~SQL.squish)
Expand Down
5 changes: 0 additions & 5 deletions gemfiles/rails_6_1.gemfile

This file was deleted.

2 changes: 1 addition & 1 deletion gemfiles/rails_6_0.gemfile → gemfiles/rails_8_0.gemfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
source "https://rubygems.org"

gem "activerecord", "~> 6.0", "< 6.1"
gem "activerecord", "~> 8"

gemspec path: "../"
6 changes: 3 additions & 3 deletions lib/spatial_features/caching.rb
Original file line number Diff line number Diff line change
Expand Up @@ -69,16 +69,16 @@ def self.clear_cache(klass = nil, clazz = nil)
end

def self.clear_record_cache(record, klass)
record.spatial_caches.where(:intersection_model_type => SpatialFeatures::Utils.class_name_with_ancestors(klass)).delete_all
record.spatial_caches.where(intersection_model_type: SpatialFeatures::Utils.class_name_with_ancestors(klass)).delete_all
SpatialProximity.between(record, klass).delete_all
end

def self.create_spatial_proximities(record, klass)
klass = klass.to_s.constantize
klass_record = klass.new

scope = klass.within_buffer(record, default_cache_buffer_in_meters, :columns => :id, :intersection_area => true, :distance => true, :cache => false)
scope = scope.where.not(:id => record.id) if klass.table_name == record.class.table_name # Don't calculate self proximity
scope = klass.within_buffer(record, default_cache_buffer_in_meters, columns: :id, intersection_area: true, distance: true, cache: false)
scope = scope.where.not(id: record.id) if klass.table_name == record.class.table_name # Don't calculate self proximity
results = klass.connection.select_rows(scope.to_sql)

results.each do |id, distance, area|
Expand Down
12 changes: 6 additions & 6 deletions lib/spatial_features/controller_helpers/spatial_extensions.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,29 +15,29 @@ def abstract_clear_feature_update_errors(models)
end

def abstract_proximity_action(scope, target, distance, &block)
@nearby_records = scope_for_search(scope).within_buffer(target, distance, :distance => true, :intersection_area => true).order('distance_in_meters ASC, intersection_area_in_square_meters DESC, id ASC')
@nearby_records = scope_for_search(scope).within_buffer(target, distance, distance: true, intersection_area: true).order('distance_in_meters ASC, intersection_area_in_square_meters DESC, id ASC')
@target = target

if block_given?
block.call(@nearby_records)
else
respond_to do |format|
format.html { render :template => 'shared/spatial/feature_proximity', :layout => false }
format.kml { render :template => 'shared/spatial/feature_proximity' }
format.html { render template: 'shared/spatial/feature_proximity', layout: false }
format.kml { render template: 'shared/spatial/feature_proximity' }
end
end
end

def abstract_venn_polygons_action(scope, target, &block)
@venn_polygons = SpatialFeatures.venn_polygons(scope_for_search(scope).intersecting(target), target.class.where(:id => target), :target => target)
@venn_polygons = SpatialFeatures.venn_polygons(scope_for_search(scope).intersecting(target), target.class.where(id: target), target: target)
@klass = klass_for_search(scope)
@target = target

if block_given?
block.call(@venn_polygons)
else
respond_to do |format|
format.kml { render :template => 'shared/spatial/feature_venn_polygons' }
format.kml { render template: 'shared/spatial/feature_venn_polygons' }
end
end
end
Expand All @@ -50,7 +50,7 @@ def scope_for_search(scope)
if params.key?(:ids)
ids = params[:ids]
ids = ids.split(/\D/) if ids.is_a?(String)
scope.where(:id => ids)
scope.where(id: ids)
else
scope
end
Expand Down
2 changes: 1 addition & 1 deletion lib/spatial_features/download.rb
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ def self.entries(file)
end

def self.find_in_zip(file, find:, **unzip_options)
Unzip.paths(file, :find => find, **unzip_options)
Unzip.paths(file, find: find, **unzip_options)
end
end
end
32 changes: 16 additions & 16 deletions lib/spatial_features/has_spatial_features.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,29 +4,29 @@ module ActMethod
def has_spatial_features(options = {})
unless acts_like?(:spatial_features)
class_attribute :spatial_features_options
self.spatial_features_options = {:make_valid => true}
self.spatial_features_options = {make_valid: true}

extend ClassMethods
include InstanceMethods
include FeatureImport

has_many :features, lambda { extending FeaturesAssociationExtensions }, :as => :spatial_model, :dependent => :delete_all
has_one :aggregate_feature, lambda { extending FeaturesAssociationExtensions }, :as => :spatial_model, :dependent => :delete
has_many :features, lambda { extending FeaturesAssociationExtensions }, as: :spatial_model, dependent: :delete_all
has_one :aggregate_feature, lambda { extending FeaturesAssociationExtensions }, as: :spatial_model, dependent: :delete

scope :with_features, lambda { joins(:features).distinct }
scope :without_features, lambda { joins("LEFT OUTER JOIN features ON features.spatial_model_type = '#{Utils.base_class(name)}' AND features.spatial_model_id = #{table_name}.id").where("features.id IS NULL") }
scope :include_bounds, lambda { SQLHelpers.append_select(joins(:aggregate_feature), :north, :east, :south, :west) }
scope :include_area, lambda { SQLHelpers.append_select(joins(:aggregate_feature), :area) }

scope :with_spatial_cache, lambda {|klass| joins(:spatial_caches).where(:spatial_caches => { :intersection_model_type => Utils.class_name_with_ancestors(klass) }).distinct }
scope :with_spatial_cache, lambda {|klass| joins(:spatial_caches).where(spatial_caches: { intersection_model_type: Utils.class_name_with_ancestors(klass) }).distinct }
scope :without_spatial_cache, lambda {|klass| joins("LEFT OUTER JOIN #{SpatialCache.table_name} ON #{SpatialCache.table_name}.spatial_model_id = #{table_name}.id AND #{SpatialCache.table_name}.spatial_model_type = '#{Utils.base_class(name)}' and intersection_model_type IN ('#{Utils.class_name_with_ancestors(klass).join("','") }')").where("#{SpatialCache.table_name}.spatial_model_id IS NULL") }
scope :with_stale_spatial_cache, lambda { has_spatial_features_hash? ? joins(:spatial_caches).where("#{table_name}.features_hash != spatial_caches.features_hash").distinct : none }

has_many :spatial_caches, :as => :spatial_model, :dependent => :delete_all, :class_name => 'SpatialCache'
has_many :model_a_spatial_proximities, :as => :model_a, :class_name => 'SpatialProximity', :dependent => :delete_all
has_many :model_b_spatial_proximities, :as => :model_b, :class_name => 'SpatialProximity', :dependent => :delete_all
has_many :spatial_caches, as: :spatial_model, dependent: :delete_all, class_name: 'SpatialCache'
has_many :model_a_spatial_proximities, as: :model_a, class_name: 'SpatialProximity', dependent: :delete_all
has_many :model_b_spatial_proximities, as: :model_b, class_name: 'SpatialProximity', dependent: :delete_all

delegate :has_spatial_features_hash?, :has_features_area?, :to => self
delegate :has_spatial_features_hash?, :has_features_area?, to: self
end

self.spatial_features_options = self.spatial_features_options.deep_merge(options)
Expand Down Expand Up @@ -87,18 +87,18 @@ def bounds
def features
type = base_class.to_s # Rails stores polymorphic foreign keys as the base class
if all == unscoped
Feature.where(:spatial_model_type => type)
Feature.where(spatial_model_type: type)
else
Feature.where(:spatial_model_type => type, :spatial_model_id => all.unscope(:select))
Feature.where(spatial_model_type: type, spatial_model_id: all.unscope(:select))
end
end

def aggregate_features
type = base_class.to_s # Rails stores polymorphic foreign keys as the base class
if all == unscoped
AggregateFeature.where(:spatial_model_type => type)
AggregateFeature.where(spatial_model_type: type)
else
AggregateFeature.where(:spatial_model_type => type, :spatial_model_id => all.unscope(:select))
AggregateFeature.where(spatial_model_type: type, spatial_model_id: all.unscope(:select))
end
end

Expand All @@ -119,7 +119,7 @@ def area_in_square_meters
private

def cached_within_buffer_scope(other, buffer_in_meters, options)
options = options.reverse_merge(:columns => "#{table_name}.*")
options = options.reverse_merge(columns: "#{table_name}.*")
scope = cached_spatial_join(other)
scope = scope.select(options[:columns])
scope = scope.where("spatial_proximities.distance_in_meters <= ?", buffer_in_meters) if buffer_in_meters
Expand All @@ -142,7 +142,7 @@ def cached_spatial_join(other)
end

def uncached_within_buffer_scope(other, buffer_in_meters, options)
options = options.reverse_merge(:columns => "#{table_name}.*")
options = options.reverse_merge(columns: "#{table_name}.*")

scope = spatial_join(other, buffer_in_meters)
scope = scope.select(options[:columns])
Expand All @@ -168,8 +168,8 @@ def spatial_join(other, buffer = 0, table_alias = 'features', other_alias = 'oth

def features_scope(other)
scope = AggregateFeature
scope = scope.where(:spatial_model_type => Utils.base_class_of(other).to_s)
scope = scope.where(:spatial_model_id => other) unless Utils.class_of(other) == other
scope = scope.where(spatial_model_type: Utils.base_class_of(other).to_s)
scope = scope.where(spatial_model_id: other) unless Utils.class_of(other) == other
return scope
end

Expand Down
Loading