module SpatialFeatures

TODO: Test the `::features` on a subclass to ensure we scope correctly

Constants

VERSION

Public Class Methods

cache_proximity(*klasses) click to toggle source

Create or update the spatial cache of a spatial class in relation to another NOTE: Arguments are order independent, so their names do not reflect the _a _b naming scheme used in other cache methods

# File lib/spatial_features/caching.rb, line 28
def self.cache_proximity(*klasses)
  class_combinations(klasses).each do |klass, clazz|
    clear_cache(klass, clazz)

    klass.find_each do |record|
      create_spatial_proximities(record, clazz)
      create_spatial_cache(record, clazz)
    end

    clazz.find_each do |record|
      create_spatial_cache(record, klass)
    end
  end
end
cache_record_proximity(record, klass) click to toggle source

Create or update the spatial cache of a single record in relation to another spatial class

# File lib/spatial_features/caching.rb, line 54
def self.cache_record_proximity(record, klass)
  clear_record_cache(record, klass)
  create_spatial_proximities(record, klass)
  create_spatial_cache(record, klass)
end
class_combinations(klasses) click to toggle source

Returns a list of class pairs with each combination e.g. [a,b], [a,c] [b,c] and also [a,a], [b,b], [c,c]

# File lib/spatial_features/caching.rb, line 44
def self.class_combinations(klasses)
  klasses.zip(klasses) + klasses.combination(2).to_a
end
class_permutations(klasses) click to toggle source

Returns a list of class pairs with each permutation e.g. [a,b], [b,a] and also [a,a], [b,b]

# File lib/spatial_features/caching.rb, line 49
def self.class_permutations(klasses)
  klasses.zip(klasses) + klasses.permutation(2).to_a
end
clear_cache(klass = nil, clazz = nil) click to toggle source

Delete all cache entries relating klass to clazz

# File lib/spatial_features/caching.rb, line 61
def self.clear_cache(klass = nil, clazz = nil)
  if klass.blank? && clazz.blank?
    SpatialCache.delete_all
    SpatialProximity.delete_all
  else
    SpatialCache.between(klass, clazz).delete_all
    SpatialProximity.between(klass, clazz).delete_all
  end
end
clear_record_cache(record, klass) click to toggle source
# File lib/spatial_features/caching.rb, line 71
def self.clear_record_cache(record, klass)
  record.spatial_caches.where(:intersection_model_type => SpatialFeatures::Utils.class_name_with_ancestors(klass)).delete_all
  SpatialProximity.between(record, klass).delete_all
end
create_spatial_cache(model, klass) click to toggle source
# File lib/spatial_features/caching.rb, line 98
def self.create_spatial_cache(model, klass)
  SpatialCache.create! do |cache|
    cache.spatial_model               = model
    cache.intersection_model_type     = klass
    cache.intersection_cache_distance = default_cache_buffer_in_meters
    cache.features_hash               = model.features_hash if model.has_spatial_features_hash?
  end
end
create_spatial_proximities(record, klass) click to toggle source
# File lib/spatial_features/caching.rb, line 76
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
  results = klass.connection.select_rows(scope.to_sql)

  results.each do |id, distance, area|
    klass_record.id = id
    SpatialProximity.create! do |proximity|
      # Set id and type instead of model to avoid autosaving the klass_record
      proximity.model_a_id = record.id
      proximity.model_a_type = Utils.base_class(record)
      proximity.model_b_id = klass_record.id
      proximity.model_b_type = Utils.base_class(klass_record)
      proximity.distance_in_meters = distance
      proximity.intersection_area_in_square_meters = area
    end
  end
end
update_proximity(*klasses) click to toggle source
# File lib/spatial_features/caching.rb, line 5
def self.update_proximity(*klasses)
  class_permutations(klasses).each do |klass, clazz|
    klass.without_spatial_cache(clazz).find_each do |record|
      cache_record_proximity(record, clazz)
    end
  end

  klasses.each do |klass|
    update_spatial_cache(klass)
  end
end
update_spatial_cache(scope) click to toggle source
# File lib/spatial_features/caching.rb, line 17
def self.update_spatial_cache(scope)
  scope.with_stale_spatial_cache.includes(:spatial_caches).find_each do |record|
    record.spatial_caches.each do |spatial_cache|
      cache_record_proximity(record, spatial_cache.intersection_model_type) if spatial_cache.stale?
    end
  end
end
venn_polygons(*scopes) click to toggle source

Splits overlapping features into separate polygons at their areas of overlap, and returns an array of objects with kml for the overlapping area and a list of the record ids whose kml overlapped within that area

# File lib/spatial_features/venn_polygons.rb, line 4
def self.venn_polygons(*scopes)
  options = scopes.extract_options!
  scope = scopes.collect do |scope|
    scope.joins(:features).where('features.feature_type = ?', 'polygon').except(:select).select("features.geom AS the_geom").to_sql
  end.reject(&:blank?).join(' UNION ')  # NullRelation.to_sql returns empty string, so reject it

  sql = "
    SELECT scope.id, scope.type, ST_AsKML(venn_polygons.geom) AS kml FROM ST_Dump((
      SELECT ST_Polygonize(the_geom) AS the_geom FROM (

        SELECT ST_Union(the_geom) AS the_geom FROM (

            -- Handle Multigeometry
            SELECT ST_ExteriorRing((ST_DumpRings(the_geom)).geom) AS the_geom
            FROM (#{scope}) AS scope

        ) AS exterior_lines

      ) AS noded_lines
      WHERE NOT ST_IsEmpty(the_geom) -- Ignore empty geometry from ST_Union if there are no polygons because polygonize will explode

    )) AS venn_polygons
  "

  # If we have a target model, throw away all venn_polygons not bounded by the target
  if options[:target]
    sql <<
      "INNER JOIN features
        ON features.spatial_model_type = '#{Utils.base_class(options[:target].class)}' AND features.spatial_model_id = #{options[:target].id} AND ST_Intersects(features.geom, venn_polygons.geom) "
  end

  # Join with the original polygons so we can determine which original polygons each venn polygon came from
  scope = scopes.collect do |scope|
    scope.joins(:features).where('features.feature_type = ?', 'polygon').except(:select).select("#{scope.klass.table_name}.id, features.spatial_model_type AS type, features.geom").to_sql
  end.reject(&:blank?).join(' UNION ')  # NullRelation.to_sql returns empty string, so reject it

  sql <<
    "INNER JOIN (#{scope}) AS scope
      ON ST_Covers(scope.geom, ST_PointOnSurface(venn_polygons.geom)) -- Shrink the venn polygons so they don't share edges with the original polygons which could cause varying results due to tiny inaccuracy"

  # Eager load the records for each venn polygon
  eager_load_hash = Hash.new {|hash, key| hash[key] = []}
  polygons = ActiveRecord::Base.connection.select_all(sql)
  polygons.group_by{|row| row['type']}.each do |record_type, rows|
    rows.each do |row|
      eager_load_hash[record_type] << row['id']
    end
  end
  eager_load_hash.each do |record_type, ids|
    eager_load_hash[record_type] = record_type.constantize.find(ids)
  end

  # Instantiate objects to hold the kml and records for each venn polygon
  polygons.group_by{|row| row['kml']}.collect do |kml, rows|
    # Uniq on row id in case a single record had self intersecting multi geometry, which would cause it to appear duplicated on a single venn polygon
    records = rows.uniq{|row| row.values_at('id', 'type') }.collect{|row| eager_load_hash.fetch(row['type']).detect{|record| record.id == row['id'].to_i } }
    OpenStruct.new(:kml => kml, :records => records)
  end
end