Module: SpatialFeatures

Defined in:
lib/spatial_features/has_spatial_features.rb,
lib/spatial_features/unzip.rb,
lib/spatial_features/utils.rb,
lib/spatial_features/engine.rb,
lib/spatial_features/caching.rb,
lib/spatial_features/version.rb,
lib/spatial_features/download.rb,
lib/spatial_features/validation.rb,
lib/spatial_features/importers/kml.rb,
lib/spatial_features/venn_polygons.rb,
lib/spatial_features/importers/base.rb,
lib/spatial_features/importers/file.rb,
lib/spatial_features/uncached_result.rb,
lib/spatial_features/importers/geomark.rb,
lib/spatial_features/importers/geo_json.rb,
lib/spatial_features/importers/kml_file.rb,
lib/spatial_features/importers/shapefile.rb,
lib/spatial_features/importers/esri_geo_json.rb,
lib/spatial_features/importers/kml_file_arcgis.rb,
lib/spatial_features/has_spatial_features/feature_import.rb,
lib/spatial_features/has_spatial_features/queued_spatial_processing.rb

Overview

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

Defined Under Namespace

Modules: ActMethod, ClassMethods, Download, FeatureImport, FeaturesAssociationExtensions, Importers, InstanceMethods, QueuedSpatialProcessing, SQLHelpers, UncachedResult, Unzip, Utils, Validation Classes: EmptyImportError, Engine, ImportEncodingError, ImportError

Constant Summary collapse

VERSION =
"3.8.0"
ENCODING_ERROR =
/invalid byte sequence/i.freeze

Class Method Summary collapse

Class Method Details

.cache_proximity(*klasses) ⇒ Object

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



28
29
30
31
32
33
34
35
36
37
38
39
40
41
# 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) ⇒ Object

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



54
55
56
57
58
# 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) ⇒ Object

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]



44
45
46
# 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) ⇒ Object

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



49
50
51
# 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) ⇒ Object

Delete all cache entries relating klass to clazz



61
62
63
64
65
66
67
68
69
# 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) ⇒ Object



71
72
73
74
# 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) ⇒ Object



100
101
102
103
104
105
106
107
# File 'lib/spatial_features/caching.rb', line 100

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) ⇒ Object



76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
# 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|
      # Always make the spatial model earliest type and id be model a so we can optimize queries
      data = [[Utils.base_class(record).to_s, record.id], [Utils.base_class(klass_record).to_s, klass_record.id]]
      data.sort!

      # Set id and type instead of model to avoid autosaving the klass_record
      proximity.model_a_type, proximity.model_a_id = data.first
      proximity.model_b_type, proximity.model_b_id = data.second
      proximity.distance_in_meters = distance
      proximity.intersection_area_in_square_meters = area
    end
  end
end

.update_proximity(*klasses) ⇒ Object



5
6
7
8
9
10
11
12
13
14
15
# 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) ⇒ Object



17
18
19
20
21
22
23
# 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) ⇒ Object

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



4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
# 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