spatial_features 2.2.4 → 2.3.0

Sign up to get free protection for your applications and to get access to all the features.
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