module SpatialFeatures::ClassMethods

Public Instance Methods

acts_like_spatial_features?() click to toggle source
# File lib/spatial_features/has_spatial_features.rb, line 35
def acts_like_spatial_features?
  true
end
aggregate_features() click to toggle source
# File lib/spatial_features/has_spatial_features.rb, line 80
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)
  else
    AggregateFeature.where(:spatial_model_type => type, :spatial_model_id => all.unscope(:select))
  end
end
area_in_square_meters() click to toggle source
# File lib/spatial_features/has_spatial_features.rb, line 99
def area_in_square_meters
  features.area
end
features() click to toggle source
# File lib/spatial_features/has_spatial_features.rb, line 71
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)
  else
    Feature.where(:spatial_model_type => type, :spatial_model_id => all.unscope(:select))
  end
end
features_cache_key() click to toggle source
# File lib/spatial_features/has_spatial_features.rb, line 39
def features_cache_key
  "#{name}/#{aggregate_features.cache_key}"
end
has_features_area?() click to toggle source

Returns true if the model stores a cache of the features area

# File lib/spatial_features/has_spatial_features.rb, line 95
def has_features_area?
  owner_class_has_loaded_column?('features_area')
end
has_spatial_features_hash?() click to toggle source

Returns true if the model stores a hash of the features so we don't need to process the features if they haven't changed

# File lib/spatial_features/has_spatial_features.rb, line 90
def has_spatial_features_hash?
  owner_class_has_loaded_column?('features_hash')
end
intersecting(other, options = {}) click to toggle source
# File lib/spatial_features/has_spatial_features.rb, line 43
def intersecting(other, options = {})
  within_buffer(other, 0, options)
end
lines() click to toggle source
# File lib/spatial_features/has_spatial_features.rb, line 63
def lines
  features.lines
end
points() click to toggle source
# File lib/spatial_features/has_spatial_features.rb, line 67
def points
  features.points
end
polygons() click to toggle source
# File lib/spatial_features/has_spatial_features.rb, line 59
def polygons
  features.polygons
end
within_buffer(other, buffer_in_meters = 0, options = {}) click to toggle source
# File lib/spatial_features/has_spatial_features.rb, line 47
def within_buffer(other, buffer_in_meters = 0, options = {})
  return none if other.is_a?(ActiveRecord::Base) && other.new_record?

  # Cache only works on single records, not scopes.
  # This is because the cached intersection_area doesn't account for overlaps between the features in the scope.
  if options[:cache] != false && other.is_a?(ActiveRecord::Base)
    cached_within_buffer_scope(other, buffer_in_meters, options)
  else
    uncached_within_buffer_scope(other, buffer_in_meters, options)
  end
end

Private Instance Methods

cached_spatial_join(other) click to toggle source
# File lib/spatial_features/has_spatial_features.rb, line 121
    def cached_spatial_join(other)
      other_class = Utils.base_class_of(other)
      self_class = Utils.base_class_of(self)

      joins <<~SQL
        INNER JOIN spatial_proximities
        ON (spatial_proximities.model_a_type = '#{self_class}' AND spatial_proximities.model_a_id = #{table_name}.id AND spatial_proximities.model_b_type = '#{other_class}' AND spatial_proximities.model_b_id IN (#{Utils.id_sql(other)}))
        OR (spatial_proximities.model_b_type = '#{self_class}' AND spatial_proximities.model_b_id = #{table_name}.id AND spatial_proximities.model_a_type = '#{other_class}' AND spatial_proximities.model_a_id IN (#{Utils.id_sql(other)}))
      SQL
    end
cached_within_buffer_scope(other, buffer_in_meters, options) click to toggle source
# File lib/spatial_features/has_spatial_features.rb, line 105
def cached_within_buffer_scope(other, buffer_in_meters, options)
  options = options.reverse_merge(:columns => "#{table_name}.*")

  # Don't use the cache if it doesn't exist
  unless other.class.unscoped { other.spatial_cache_for?(Utils.class_of(self), buffer_in_meters) } # Unscope so if we're checking for same class intersections the scope doesn't affect this lookup
    return none.extending(UncachedResult)
  end

  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
  scope = scope.select("spatial_proximities.distance_in_meters") if options[:distance]
  scope = scope.select("spatial_proximities.intersection_area_in_square_meters") if options[:intersection_area]
  return scope
end
features_scope(other) click to toggle source
# File lib/spatial_features/has_spatial_features.rb, line 157
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
  return scope
end
owner_class_has_loaded_column?(column_name) click to toggle source
# File lib/spatial_features/has_spatial_features.rb, line 164
def owner_class_has_loaded_column?(column_name)
  return false unless connected?
  return false unless table_exists?
  column_names.include? column_name
end
spatial_join(other, buffer = 0, table_alias = 'features', other_alias = 'other_features', geom = 'geom_lowres') click to toggle source

Returns a scope that includes the features for this record as the table_alias and the features for other as other_alias Performs a spatial intersection between the two sets of features, within the buffer distance given in meters

# File lib/spatial_features/has_spatial_features.rb, line 146
def spatial_join(other, buffer = 0, table_alias = 'features', other_alias = 'other_features', geom = 'geom_lowres')
  scope = features_scope(self).select("#{geom} AS geom").select(:spatial_model_id)

  other_scope = features_scope(other).select("ST_Union(#{geom}) AS geom")
  return joins(%Q(INNER JOIN (#{scope.to_sql}) AS #{table_alias} ON #{table_alias}.spatial_model_id = #{table_name}.id))
        .joins(%Q(INNER JOIN (#{other_scope.to_sql}) AS #{other_alias}
                   ON NOT ST_IsEmpty(#{table_alias}.geom) -- Can't ST_DWithin empty geometry
                  AND NOT ST_IsEmpty(#{other_alias}.geom) -- Can't ST_DWithin empty geometry
                  AND ST_DWithin(#{table_alias}.geom, #{other_alias}.geom, #{buffer})))
end
uncached_within_buffer_scope(other, buffer_in_meters, options) click to toggle source
# File lib/spatial_features/has_spatial_features.rb, line 132
def uncached_within_buffer_scope(other, buffer_in_meters, options)
  options = options.reverse_merge(:columns => "#{table_name}.*")

  scope = spatial_join(other, buffer_in_meters)
  scope = scope.select(options[:columns])

  scope = scope.select("ST_Distance(features.geom, other_features.geom) AS distance_in_meters") if options[:distance]
  scope = scope.select("ST_Area(ST_Intersection(ST_CollectionExtract(features.geom, 3), ST_CollectionExtract(other_features.geom, 3))) AS intersection_area_in_square_meters") if options[:intersection_area] # Use ST_CollectionExtract to avoid a segfault we've been seeing when intersecting certain geometry

  return scope
end