spatial_features 2.2.4 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 0d84b44917dac8280a1de81c1a440d4127485846
4
- data.tar.gz: fa5a503cce899bcb8d75e7c1fc3087c414b2fabd
3
+ metadata.gz: 7c370633815f2fd2637a8d5f8c47903b13ec623e
4
+ data.tar.gz: 182d9a77aca580e40aa999a2784e4bb7b02515de
5
5
  SHA512:
6
- metadata.gz: a7e260e34f722ab7b668bcd9b92830d4e8d720e8159f1ac488c61091a57f78842cef8579ae5cb47973f328f46276fdf29e59d131a1b66ffe74069e52f791ac92
7
- data.tar.gz: bd354f3f27912b24c8aa2f4cd92ad1e3dd2d29815217e1aca1ab77b5977ea6be196470e36c2066abd67caf90322954cf7df1c232aa0ff02e567e42e8089ff30d
6
+ metadata.gz: c38c86a9daeb07fd1b0eb308e02941d29d67a14e8e62cd7cbc4833887f5a5f5fc6557923f60eec19566a84d9c0a9b7d38f014693d8e4a458c62dfd2765efbe01
7
+ data.tar.gz: 14ff222d08e0114323b97501855feed7790187cd2990a06466a631e54212e138d33927769942f7e657f35bf58048a11596e4b57d02feadbe9afe3db740c1ee13
@@ -1,6 +1,11 @@
1
1
  class SpatialCache < ActiveRecord::Base
2
2
  belongs_to :spatial_model, :polymorphic => true, :inverse_of => :spatial_cache
3
3
 
4
+ def self.between(spatial_model, klass)
5
+ where(SpatialFeatures::Utils.polymorphic_condition(spatial_model, 'spatial_model'))
6
+ .where(SpatialFeatures::Utils.polymorphic_condition(klass, 'intersection_model'))
7
+ end
8
+
4
9
  def stale?
5
10
  spatial_model.has_spatial_features_hash? && self.features_hash != spatial_model.features_hash
6
11
  end
@@ -1,4 +1,11 @@
1
1
  class SpatialProximity < ActiveRecord::Base
2
2
  belongs_to :model_a, :polymorphic => true
3
3
  belongs_to :model_b, :polymorphic => true
4
+
5
+ def self.between(scope_1, scope_2)
6
+ where <<-SQL.squish
7
+ (#{SpatialFeatures::Utils.polymorphic_condition(scope_1, 'model_a')} AND #{SpatialFeatures::Utils.polymorphic_condition(scope_2, 'model_b')}) OR
8
+ (#{SpatialFeatures::Utils.polymorphic_condition(scope_2, 'model_a')} AND #{SpatialFeatures::Utils.polymorphic_condition(scope_1, 'model_b')})
9
+ SQL
10
+ end
4
11
  end
@@ -53,17 +53,14 @@ module SpatialFeatures
53
53
  SpatialCache.delete_all
54
54
  SpatialProximity.delete_all
55
55
  else
56
- SpatialCache.where(:spatial_model_type => klass, :intersection_model_type => clazz).delete_all
57
- SpatialCache.where(:spatial_model_type => clazz, :intersection_model_type => klass).delete_all
58
- SpatialProximity.where(:model_a_type => klass, :model_b_type => clazz).delete_all
59
- SpatialProximity.where(:model_a_type => clazz, :model_b_type => klass).delete_all
56
+ SpatialCache.between(klass, clazz).delete_all
57
+ SpatialProximity.between(klass, clazz).delete_all
60
58
  end
61
59
  end
62
60
 
63
61
  def self.clear_record_cache(record, klass)
64
62
  record.spatial_cache.where(:intersection_model_type => klass).delete_all
65
- SpatialProximity.where(:model_a_type => record.class, :model_a_id => record.id, :model_b_type => klass).delete_all
66
- SpatialProximity.where(:model_b_type => record.class, :model_b_id => record.id, :model_a_type => klass).delete_all
63
+ SpatialProximity.between(record, klass).delete_all
67
64
  end
68
65
 
69
66
  def self.create_spatial_proximities(record, klass)
@@ -1,10 +1,10 @@
1
1
  module SpatialExtensions
2
2
  private
3
3
 
4
- def abstract_refresh_geometry_action(models, cache_classes)
4
+ def abstract_refresh_geometry_action(models)
5
5
  Array.wrap(models).each do |model|
6
6
  model.failed_feature_update_jobs.destroy_all
7
- model.queue_feature_update!(:cache_classes => cache_classes)
7
+ model.delay_update_features!
8
8
  end
9
9
 
10
10
  redirect_to :back
@@ -3,14 +3,23 @@ require 'digest/md5'
3
3
  module SpatialFeatures
4
4
  module FeatureImport
5
5
  extend ActiveSupport::Concern
6
+ include QueuedSpatialProcessing
6
7
 
7
8
  included do
8
9
  extend ActiveModel::Callbacks
9
10
  define_model_callbacks :update_features
10
- spatial_features_options.reverse_merge!(:import => {})
11
+ spatial_features_options.reverse_merge!(:import => {}, spatial_cache: [])
11
12
  end
12
13
 
13
- def update_features!(skip_invalid: false, options: {})
14
+ module ClassMethods
15
+ def update_features!(skip_invalid: false)
16
+ find_each do |record|
17
+ record.update_features!(skip_invalid: skip_invalid)
18
+ end
19
+ end
20
+ end
21
+
22
+ def update_features!(skip_invalid: false, **options)
14
23
  options = options.reverse_merge(spatial_features_options)
15
24
 
16
25
  ActiveRecord::Base.transaction do
@@ -21,13 +30,40 @@ module SpatialFeatures
21
30
 
22
31
  run_callbacks :update_features do
23
32
  import_features(imports, skip_invalid)
24
- set_features_cache_key(cache_key)
33
+ update_features_cache_key(cache_key)
34
+ update_features_area
35
+
36
+ if options[:spatial_cache].present? && options[:queue_spatial_cache]
37
+ queue_update_spatial_cache(options.slice(:spatial_cache))
38
+ else
39
+ update_spatial_cache(options.slice(:spatial_cache))
40
+ end
25
41
  end
26
42
 
27
43
  return true
28
44
  end
29
45
  end
30
46
 
47
+ def update_features_cache_key(cache_key)
48
+ return unless has_spatial_features_hash?
49
+ self.features_hash = cache_key
50
+ update_column(:features_hash, features_hash) unless new_record?
51
+ end
52
+
53
+ def update_features_area
54
+ return unless has_spatial_features_hash?
55
+ self.features_area = features.area(:cache => false)
56
+ update_column :features_area, features_area unless new_record?
57
+ end
58
+
59
+ def update_spatial_cache(options = {})
60
+ options = options.reverse_merge(spatial_features_options)
61
+
62
+ Array.wrap(options[:spatial_cache]).select(&:present?).each do |klass|
63
+ SpatialFeatures.cache_record_proximity(self, klass.to_s.constantize)
64
+ end
65
+ end
66
+
31
67
  private
32
68
 
33
69
  def spatial_feature_imports(import_options, make_valid)
@@ -48,12 +84,14 @@ module SpatialFeatures
48
84
  feature.save
49
85
  end
50
86
 
51
- if !skip_invalid && invalid.present?
52
- errors = imports.flat_map(&:errors)
53
- invalid.each do |feature|
54
- errors << "Feature #{feature.name}: #{feature.errors.full_messages.to_sentence}"
55
- end
87
+ errors = imports.flat_map(&:errors)
88
+ invalid.each do |feature|
89
+ errors << "Feature #{feature.name}: #{feature.errors.full_messages.to_sentence}"
90
+ end
56
91
 
92
+ if skip_invalid && errors.present?
93
+ Rails.logger.warn "Error updating #{self.class} #{self.id}. #{errors.to_sentence}"
94
+ elsif errors.present?
57
95
  raise ImportError, "Error updating #{self.class} #{self.id}. #{errors.to_sentence}"
58
96
  end
59
97
 
@@ -63,12 +101,6 @@ module SpatialFeatures
63
101
  def features_cache_key_matches?(cache_key)
64
102
  has_spatial_features_hash? && cache_key == features_hash
65
103
  end
66
-
67
- def set_features_cache_key(cache_key)
68
- return unless has_spatial_features_hash?
69
- self.features_hash = cache_key
70
- update_column(:features_hash, cache_key) unless new_record?
71
- end
72
104
  end
73
105
 
74
106
  class ImportError < StandardError; end
@@ -0,0 +1,43 @@
1
+ module SpatialFeatures
2
+ module QueuedSpatialProcessing
3
+ extend ActiveSupport::Concern
4
+
5
+ def queue_update_spatial_cache(*args)
6
+ queue_spatial_task('update_spatial_cache', *args)
7
+ end
8
+
9
+ def delay_update_features!(*args)
10
+ queue_spatial_task('update_features!', *args)
11
+ end
12
+
13
+ def updating_features?
14
+ running_feature_update_jobs.exists?
15
+ end
16
+
17
+ def feature_update_error
18
+ (failed_feature_update_jobs.first.try(:last_error) || '').split("\n").first
19
+ end
20
+
21
+ def running_feature_update_jobs
22
+ spatial_processing_jobs('update_features!').where(failed_at: nil)
23
+ end
24
+
25
+ def failed_feature_update_jobs
26
+ spatial_processing_jobs('update_features!').where.not(failed_at: nil)
27
+ end
28
+
29
+ def spatial_processing_jobs(suffix = nil)
30
+ Delayed::Job.where('queue LIKE ?', "#{spatial_processing_queue_name}#{suffix}%")
31
+ end
32
+
33
+ private
34
+
35
+ def queue_spatial_task(method_name, *args)
36
+ delay(:queue => spatial_processing_queue_name + method_name).send(method_name, *args)
37
+ end
38
+
39
+ def spatial_processing_queue_name
40
+ "#{self.class}/#{self.id}/"
41
+ end
42
+ end
43
+ end
@@ -7,7 +7,7 @@ module SpatialFeatures
7
7
 
8
8
  extend ClassMethods
9
9
  include InstanceMethods
10
- include DelayedFeatureImport
10
+ include FeatureImport
11
11
 
12
12
  has_many :features, lambda { extending FeaturesAssociationExtensions }, :as => :spatial_model, :dependent => :delete_all
13
13
 
@@ -22,8 +22,6 @@ module SpatialFeatures
22
22
  has_many :model_a_spatial_proximities, :as => :model_a, :class_name => 'SpatialProximity', :dependent => :delete_all
23
23
  has_many :model_b_spatial_proximities, :as => :model_b, :class_name => 'SpatialProximity', :dependent => :delete_all
24
24
 
25
- after_save :update_features_area, :if => :features_hash_changed? if has_features_area? && has_spatial_features_hash?
26
-
27
25
  delegate :has_spatial_features_hash?, :has_features_area?, :to => self
28
26
  end
29
27
 
@@ -100,7 +98,7 @@ module SpatialFeatures
100
98
  options = options.reverse_merge(:columns => "#{table_name}.*")
101
99
 
102
100
  # Don't use the cache if it doesn't exist
103
- return all.extending(UncachedRelation) unless other.spatial_cache_for?(class_for(self), buffer_in_meters)
101
+ return all.extending(UncachedRelation) unless other.spatial_cache_for?(Utils.class_of(self), buffer_in_meters)
104
102
 
105
103
  scope = cached_spatial_join(other)
106
104
  scope = scope.select(options[:columns])
@@ -111,14 +109,14 @@ module SpatialFeatures
111
109
  end
112
110
 
113
111
  def cached_spatial_join(other)
114
- other_class = class_for(other)
112
+ other_class = Utils.class_of(other)
115
113
 
116
114
  raise "Cannot use cached spatial join for the same class" if self == other_class
117
115
 
118
116
  other_column = other_class.name < self.name ? :model_a : :model_b
119
117
  self_column = other_column == :model_a ? :model_b : :model_a
120
118
 
121
- joins("INNER JOIN spatial_proximities ON spatial_proximities.#{self_column}_type = '#{self}' AND spatial_proximities.#{self_column}_id = #{table_name}.id AND spatial_proximities.#{other_column}_type = '#{other_class}' AND spatial_proximities.#{other_column}_id IN (#{ids_sql_for(other)})")
119
+ joins("INNER JOIN spatial_proximities ON spatial_proximities.#{self_column}_type = '#{self}' AND spatial_proximities.#{self_column}_id = #{table_name}.id AND spatial_proximities.#{other_column}_type = '#{other_class}' AND spatial_proximities.#{other_column}_id IN (#{Utils.id_sql(other)})")
122
120
  end
123
121
 
124
122
  def uncached_within_buffer_scope(other, buffer_in_meters, options)
@@ -153,30 +151,10 @@ module SpatialFeatures
153
151
 
154
152
  def features_scope(other)
155
153
  scope = Feature
156
- scope = scope.where(:spatial_model_type => class_for(other))
157
- scope = scope.where(:spatial_model_id => other) unless class_for(other) == other
154
+ scope = scope.where(:spatial_model_type => Utils.class_of(other))
155
+ scope = scope.where(:spatial_model_id => other) unless Utils.class_of(other) == other
158
156
  return scope
159
157
  end
160
-
161
- # Returns the class for the given, class, scope, or record
162
- def class_for(other)
163
- case other
164
- when ActiveRecord::Base
165
- other.class
166
- when ActiveRecord::Relation
167
- other.klass
168
- else
169
- other
170
- end
171
- end
172
-
173
- def ids_sql_for(other)
174
- if other.is_a?(ActiveRecord::Base)
175
- other.id || '0'
176
- else
177
- other.unscope(:select).select(:id).to_sql
178
- end
179
- end
180
158
  end
181
159
 
182
160
  module InstanceMethods
@@ -221,11 +199,12 @@ module SpatialFeatures
221
199
 
222
200
  def total_intersection_area_in_square_meters(other)
223
201
  other = other.intersecting(self) unless other.is_a?(ActiveRecord::Base)
202
+ return features.area if spatial_cache_for?(other, 0) && SpatialProximity.between(self, other).where('intersection_area_in_square_meters >= ?', features.area).exists?
224
203
  return features.total_intersection_area_in_square_meters(other.features)
225
204
  end
226
205
 
227
- def spatial_cache_for?(klass, buffer_in_meters)
228
- if cache = spatial_cache_for(klass)
206
+ def spatial_cache_for?(other, buffer_in_meters)
207
+ if cache = spatial_cache.between(self, SpatialFeatures::Utils.class_of(other)).first
229
208
  return cache.intersection_cache_distance.nil? if buffer_in_meters.nil? # cache must be total if no buffer_in_meters
230
209
  return false if cache.stale? # cache must be for current features
231
210
  return true if cache.intersection_cache_distance.nil? # always good if cache is total
@@ -235,14 +214,6 @@ module SpatialFeatures
235
214
  return false
236
215
  end
237
216
  end
238
-
239
- def spatial_cache_for(klass)
240
- spatial_cache.where(:intersection_model_type => klass).first
241
- end
242
-
243
- def update_features_area
244
- update_column :features_area, features.area(:cache => false)
245
- end
246
217
  end
247
218
 
248
219
  module FeaturesAssociationExtensions
@@ -250,7 +221,7 @@ module SpatialFeatures
250
221
  if options[:cache] == false || !proxy_association.owner.class.has_features_area?
251
222
  area_in_square_meters
252
223
  else
253
- proxy_association.owner.features_area.to_f
224
+ (proxy_association.owner.features_area || area_in_square_meters).to_f
254
225
  end
255
226
  end
256
227
  end
@@ -0,0 +1,32 @@
1
+ module SpatialFeatures
2
+ module Utils
3
+ extend self
4
+
5
+ def polymorphic_condition(scope, column_name)
6
+ sql = "#{column_name}_type = ?"
7
+ sql << " AND #{column_name}_id IN (#{id_sql(scope)})" unless scope.is_a?(Class)
8
+
9
+ return class_of(scope).send :sanitize_sql, [sql, class_of(scope)]
10
+ end
11
+
12
+ # Returns the class for the given, class, scope, or record
13
+ def class_of(object)
14
+ case object
15
+ when ActiveRecord::Base
16
+ object.class
17
+ when ActiveRecord::Relation
18
+ object.klass
19
+ else
20
+ object
21
+ end
22
+ end
23
+
24
+ def id_sql(object)
25
+ if object.is_a?(ActiveRecord::Base)
26
+ object.id || '0'
27
+ else
28
+ object.unscope(:select).select(:id).to_sql
29
+ end
30
+ end
31
+ end
32
+ end
@@ -1,3 +1,3 @@
1
1
  module SpatialFeatures
2
- VERSION = "2.2.4"
2
+ VERSION = "2.3.0"
3
3
  end
@@ -1,4 +1,5 @@
1
1
  # Gems
2
+ require 'delayed_job_active_record'
2
3
  require 'rgeo/shapefile'
3
4
  require 'nokogiri'
4
5
  require 'zip'
@@ -12,10 +13,11 @@ require 'spatial_features/venn_polygons'
12
13
  require 'spatial_features/controller_helpers/spatial_extensions'
13
14
  require 'spatial_features/download'
14
15
  require 'spatial_features/unzip'
16
+ require 'spatial_features/utils'
15
17
 
16
18
  require 'spatial_features/has_spatial_features'
19
+ require 'spatial_features/has_spatial_features/queued_spatial_processing'
17
20
  require 'spatial_features/has_spatial_features/feature_import'
18
- require 'spatial_features/has_spatial_features/delayed_feature_import'
19
21
 
20
22
  require 'spatial_features/has_fusion_table_features'
21
23
  require 'spatial_features/has_fusion_table_features/api'
@@ -30,8 +32,6 @@ require 'spatial_features/importers/kml_file_arcgis'
30
32
  require 'spatial_features/importers/geomark'
31
33
  require 'spatial_features/importers/shapefile'
32
34
 
33
- require 'spatial_features/workers/update_features_job'
34
-
35
35
  require 'spatial_features/engine'
36
36
 
37
37
  module SpatialFeatures
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: spatial_features
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.2.4
4
+ version: 2.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ryan Wallace
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2016-12-08 00:00:00.000000000 Z
12
+ date: 2016-12-13 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rails
@@ -161,8 +161,8 @@ files:
161
161
  - lib/spatial_features/has_fusion_table_features/configuration.rb
162
162
  - lib/spatial_features/has_fusion_table_features/service.rb
163
163
  - lib/spatial_features/has_spatial_features.rb
164
- - lib/spatial_features/has_spatial_features/delayed_feature_import.rb
165
164
  - lib/spatial_features/has_spatial_features/feature_import.rb
165
+ - lib/spatial_features/has_spatial_features/queued_spatial_processing.rb
166
166
  - lib/spatial_features/importers/base.rb
167
167
  - lib/spatial_features/importers/file.rb
168
168
  - lib/spatial_features/importers/geomark.rb
@@ -171,9 +171,9 @@ files:
171
171
  - lib/spatial_features/importers/kml_file_arcgis.rb
172
172
  - lib/spatial_features/importers/shapefile.rb
173
173
  - lib/spatial_features/unzip.rb
174
+ - lib/spatial_features/utils.rb
174
175
  - lib/spatial_features/venn_polygons.rb
175
176
  - lib/spatial_features/version.rb
176
- - lib/spatial_features/workers/update_features_job.rb
177
177
  - lib/tasks/spatial_features_tasks.rake
178
178
  homepage: https://github.com/culturecode/spatial_features
179
179
  licenses:
@@ -1,37 +0,0 @@
1
- module SpatialFeatures
2
- module DelayedFeatureImport
3
- extend ActiveSupport::Concern
4
- include FeatureImport
5
-
6
- def queue_feature_update!(options = {})
7
- job = UpdateFeaturesJob.new(options.merge :spatial_model_type => self.class, :spatial_model_id => self.id)
8
- Delayed::Job.enqueue(job, :queue => delayed_jobs_queue_name)
9
- end
10
-
11
- def updating_features?
12
- running_feature_update_jobs.exists?
13
- end
14
-
15
- def feature_update_error
16
- (failed_feature_update_jobs.first.try(:last_error) || '').split("\n").first
17
- end
18
-
19
- def running_feature_update_jobs
20
- feature_update_jobs.where(failed_at: nil)
21
- end
22
-
23
- def failed_feature_update_jobs
24
- feature_update_jobs.where.not(failed_at: nil)
25
- end
26
-
27
- def feature_update_jobs
28
- Delayed::Job.where(queue: delayed_jobs_queue_name)
29
- end
30
-
31
- private
32
-
33
- def delayed_jobs_queue_name
34
- "#{self.class}/#{self.id}/update_features"
35
- end
36
- end
37
- end
@@ -1,51 +0,0 @@
1
- class UpdateFeaturesJob < Struct.new(:options)
2
- def perform
3
- model = options[:spatial_model_type].find(options[:spatial_model_id])
4
-
5
- if model.update_features!
6
- Array(options[:cache_classes]).each {|klass| SpatialFeatures.cache_record_proximity(model, klass) }
7
- after_feature_update(model)
8
- end
9
- rescue => e
10
- raise "Can't refresh geometry: #{normalize_message(e.message)}"
11
- end
12
-
13
- private
14
-
15
- def after_feature_update(model)
16
- # stub to be overridden
17
- end
18
-
19
- NUMBER_REGEX = /-?\d+\.\d+/
20
-
21
- def normalize_message(message)
22
- normalized_messages = []
23
-
24
- if message =~ /invalid KML representation/
25
- normalized_messages += invalid_kml_reason(message).presence || ["KML importer received invalid geometry."]
26
- end
27
-
28
- if message =~ /Self-intersection/
29
- normalized_messages += message.scan(/\[(#{NUMBER_REGEX}) (#{NUMBER_REGEX})\]/).collect do |lng, lat|
30
- "Self-intersection at #{lng},#{lat}"
31
- end
32
- end
33
-
34
- if normalized_messages.many?
35
- return '<ul><li>' + normalized_messages.join('</li><li>') + '</li></ul>'
36
- elsif normalized_messages.present?
37
- normalized_messages.first
38
- else
39
- return message
40
- end
41
- end
42
-
43
- COORDINATE_REGEX = /<LinearRing><coordinates>\s*((?:#{NUMBER_REGEX},#{NUMBER_REGEX},#{NUMBER_REGEX}\s*)+)<\/coordinates><\/LinearRing>/
44
- def invalid_kml_reason(message)
45
- message.scan(COORDINATE_REGEX).collect do |match|
46
- coords = match[0].remove(/,0\.0+/).split(/\s+/).chunk {|c| c }.map(&:first)
47
-
48
- "Sliver polygon detected at #{coords.first}" if coords.length < 4
49
- end.compact
50
- end
51
- end