spatial_features 2.14.1 → 2.15.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
  SHA256:
3
- metadata.gz: f96f62ce16dc4da95bc8a7f236461c6a50f45ab0c68e4dcf1a7842d944fc2452
4
- data.tar.gz: b76f8341b8014bbe2072caa6a186116d820285879a4704f11622ae0e7c8f6bf7
3
+ metadata.gz: 79fb559903943c11893b5d338f5c8030971f23051cf3611a326d0e013720f5e5
4
+ data.tar.gz: 1311241026ec5dc76a90e04f8fcc9e93c92965875f0967e1b27a0e1f64694ab3
5
5
  SHA512:
6
- metadata.gz: f6f032f1db3ce25b0c30f9db6483e2ccfa39be32509fd5967fb75a8b88db54e61b5903e3b74fac7ef420e2fe1127687dac821295d29c13ff8b4c95106ce98bc2
7
- data.tar.gz: 0dd720b90186edecbb1a81b9112796d3738154f5b12207a46e1db532a23ca9a61c3e919042c88e27cdb70453669f60f3e4d1bb17af7567a86d858a08a0bc9bed
6
+ metadata.gz: 8080e32e08c262fb3c44c6a228b0c599a1030ec52f9daea7862a06a98aa72b46dad77fb73127d5d40e6e999549ea44ebde0b77e826b1776e92eefe6b4a14898c
7
+ data.tar.gz: 3d591754fdc5a1ddb92146d805074aa5dc4bc65f5261eb0c169546f886b5dee784de7f24f56829a359641ae778960ffd54923c6b2535261d2e1194e0c9db0859
@@ -1,6 +1,9 @@
1
1
  class AbstractFeature < ActiveRecord::Base
2
2
  self.table_name = 'features'
3
3
 
4
+ class_attribute :automatically_cache_derivatives
5
+ self.automatically_cache_derivatives = true
6
+
4
7
  class_attribute :lowres_simplification
5
8
  self.lowres_simplification = 2 # Threshold in meters
6
9
 
@@ -12,15 +15,21 @@ class AbstractFeature < ActiveRecord::Base
12
15
 
13
16
  before_validation :sanitize_feature_type
14
17
  validates_presence_of :geog
15
- validate :validate_geometry
16
- before_save :sanitize
17
- after_save :cache_derivatives, :if => :saved_change_to_geog?
18
+ validate :validate_geometry, if: :will_save_change_to_geog?
19
+ before_save :sanitize, if: :will_save_change_to_geog?
20
+ after_save :cache_derivatives, :if => [:automatically_cache_derivatives?, :saved_change_to_geog?]
18
21
 
19
22
  def self.cache_key
20
23
  result = connection.select_one(all.select('max(id) AS max, count(*) AS count').to_sql)
21
24
  "#{result['max']}-#{result['count']}"
22
25
  end
23
26
 
27
+ # for Rails >= 5 ActiveRecord collections we override the collection_cache_key
28
+ # to prevent Rails doing its default query on `updated_at`
29
+ def self.collection_cache_key(_collection, _timestamp_column)
30
+ self.cache_key
31
+ end
32
+
24
33
  def self.with_metadata(k, v)
25
34
  if k.present? && v.present?
26
35
  where('metadata->? = ?', k, v)
@@ -82,6 +91,14 @@ class AbstractFeature < ActiveRecord::Base
82
91
  return envelope_json.values_at(0,2)
83
92
  end
84
93
 
94
+ def self.without_caching_derivatives(&block)
95
+ old = automatically_cache_derivatives
96
+ self.automatically_cache_derivatives = false
97
+ block.call
98
+ ensure
99
+ self.automatically_cache_derivatives = old
100
+ end
101
+
85
102
  def self.cache_derivatives(options = {})
86
103
  update_all <<-SQL.squish
87
104
  geom = ST_Transform(geog::geometry, #{detect_srid('geom')}),
@@ -132,7 +149,7 @@ class AbstractFeature < ActiveRecord::Base
132
149
  end
133
150
 
134
151
  def cache_derivatives(*args)
135
- self.class.where(:id => self.id).cache_derivatives(*args)
152
+ self.class.default_scoped.where(:id => self.id).cache_derivatives(*args)
136
153
  end
137
154
 
138
155
  def kml(options = {})
@@ -11,6 +11,16 @@ class Feature < AbstractFeature
11
11
 
12
12
  after_save :refresh_aggregate, if: :automatically_refresh_aggregate?
13
13
 
14
+ # Features are used for display so we also cache their KML representation
15
+ def self.cache_derivatives(options = {})
16
+ super
17
+ update_all <<-SQL.squish
18
+ kml = ST_AsKML(geog, 6),
19
+ kml_lowres = ST_AsKML(geom_lowres, #{options.fetch(:lowres_precision, lowres_precision)}),
20
+ kml_centroid = ST_AsKML(centroid)
21
+ SQL
22
+ end
23
+
14
24
  def self.defer_aggregate_refresh(&block)
15
25
  start_at = Feature.maximum(:id).to_i + 1
16
26
  output = without_aggregate_refresh(&block)
@@ -30,9 +40,10 @@ class Feature < AbstractFeature
30
40
 
31
41
  def self.refresh_aggregates
32
42
  # Find one feature from each spatial model and trigger the aggregate feature refresh
33
- ids = select('MAX(id)')
34
- .where.not(:spatial_model_type => nil, :spatial_model_id => nil)
43
+ ids = where.not(:spatial_model_type => nil)
44
+ .where.not(:spatial_model_id => nil)
35
45
  .group('spatial_model_type, spatial_model_id')
46
+ .pluck('MAX(id)')
36
47
 
37
48
  # Unscope so that newly built AggregateFeatures get their type column set correctly
38
49
  AbstractFeature.unscoped { where(:id => ids).find_each(&:refresh_aggregate) }
@@ -49,14 +60,4 @@ class Feature < AbstractFeature
49
60
  # this field blank.
50
61
  spatial_model_id? && automatically_refresh_aggregate && saved_change_to_geog?
51
62
  end
52
-
53
- # Features are used for display so we also cache their KML representation
54
- def self.cache_derivatives(options = {})
55
- super
56
- update_all <<-SQL.squish
57
- kml = ST_AsKML(geog, 6),
58
- kml_lowres = ST_AsKML(geom_lowres, #{options.fetch(:lowres_precision, lowres_precision)}),
59
- kml_centroid = ST_AsKML(centroid)
60
- SQL
61
- end
62
63
  end
@@ -12,6 +12,7 @@ require 'spatial_features/controller_helpers/spatial_extensions'
12
12
  require 'spatial_features/download'
13
13
  require 'spatial_features/unzip'
14
14
  require 'spatial_features/utils'
15
+ require 'spatial_features/validation'
15
16
 
16
17
  require 'spatial_features/has_spatial_features'
17
18
  require 'spatial_features/has_spatial_features/queued_spatial_processing'
@@ -6,8 +6,6 @@ module SpatialExtensions
6
6
  model.failed_feature_update_jobs.destroy_all
7
7
  model.delay_update_features!
8
8
  end
9
-
10
- redirect_to :back
11
9
  end
12
10
 
13
11
  def abstract_proximity_action(scope, target, distance, &block)
@@ -12,7 +12,7 @@ module SpatialFeatures
12
12
  def self.open(file, unzip: nil, **unzip_options)
13
13
  file = Kernel.open(file)
14
14
  file = normalize_file(file) if file.is_a?(StringIO)
15
- if Unzip.is_zip?(file)
15
+ if unzip && Unzip.is_zip?(file)
16
16
  file = find_in_zip(file, find: unzip, **unzip_options)
17
17
  end
18
18
  return file
@@ -26,6 +26,12 @@ module SpatialFeatures
26
26
  end
27
27
  end
28
28
 
29
+ def self.entries(file)
30
+ file = Kernel.open(file)
31
+ file = normalize_file(file) if file.is_a?(StringIO)
32
+ Unzip.entries(file)
33
+ end
34
+
29
35
  def self.find_in_zip(file, find:, **unzip_options)
30
36
  return File.open(Unzip.paths(file, :find => find, **unzip_options))
31
37
  end
@@ -88,16 +88,16 @@ module SpatialFeatures
88
88
 
89
89
  # 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
90
90
  def has_spatial_features_hash?
91
- column_names.include? 'features_hash'
91
+ owner_class_has_loaded_column?('features_hash')
92
92
  end
93
93
 
94
94
  # Returns true if the model stores a cache of the features area
95
95
  def has_features_area?
96
- column_names.include? 'features_area'
96
+ owner_class_has_loaded_column?('features_area')
97
97
  end
98
98
 
99
99
  def area_in_square_meters
100
- features.area_in_square_meters
100
+ features.area
101
101
  end
102
102
 
103
103
  private
@@ -160,6 +160,12 @@ module SpatialFeatures
160
160
  scope = scope.where(:spatial_model_id => other) unless Utils.class_of(other) == other
161
161
  return scope
162
162
  end
163
+
164
+ def owner_class_has_loaded_column?(column_name)
165
+ return false unless connected?
166
+ return false unless table_exists?
167
+ column_names.include? column_name
168
+ end
163
169
  end
164
170
 
165
171
  module InstanceMethods
@@ -202,7 +208,7 @@ module SpatialFeatures
202
208
  end
203
209
 
204
210
  def features_area_in_square_meters
205
- @features_area_in_square_meters ||= features.area
211
+ aggregate_feature&.area
206
212
  end
207
213
 
208
214
  def total_intersection_area_in_square_meters(other)
@@ -79,14 +79,24 @@ module SpatialFeatures
79
79
  end
80
80
 
81
81
  def import_features(imports, skip_invalid)
82
- self.features.delete_all
82
+ features.delete_all
83
83
  valid, invalid = Feature.defer_aggregate_refresh do
84
- imports.flat_map(&:features).partition do |feature|
85
- feature.spatial_model = self
86
- feature.save
84
+ Feature.without_caching_derivatives do
85
+ imports.flat_map(&:features).partition do |feature|
86
+ feature.spatial_model = self
87
+ feature.save
88
+ end
87
89
  end
88
90
  end
89
91
 
92
+ if persisted?
93
+ features.reset # Reset the association cache because we've updated the features
94
+ features.cache_derivatives
95
+ else
96
+ self.features = valid # Assign the features so when we save this record we update the foreign key on the features
97
+ Feature.where(id: features).cache_derivatives
98
+ end
99
+
90
100
  errors = imports.flat_map(&:errors)
91
101
  invalid.each do |feature|
92
102
  errors << "Feature #{feature.name}: #{feature.errors.full_messages.to_sentence}"
@@ -98,7 +108,7 @@ module SpatialFeatures
98
108
  raise ImportError, "Error updating #{self.class} #{self.id}. #{errors.to_sentence}"
99
109
  end
100
110
 
101
- self.features = valid
111
+ return features
102
112
  end
103
113
 
104
114
  def features_cache_key_matches?(cache_key)
@@ -12,7 +12,7 @@ module SpatialFeatures
12
12
  end
13
13
 
14
14
  def cache_key
15
- @cache_key ||= Digest::MD5.hexdigest(features.to_json)
15
+ @cache_key ||= Digest::MD5.file(archive).to_s
16
16
  end
17
17
 
18
18
  private
@@ -49,13 +49,27 @@ module SpatialFeatures
49
49
  SQL
50
50
  end
51
51
 
52
+
52
53
  def file
53
- @file ||= Download.open(@data, unzip: /\.shp$/, downcase: true)
54
+ @file ||= begin
55
+ validate_file!
56
+ Download.open(archive, unzip: /\.shp$/, downcase: true)
57
+ end
58
+ end
59
+
60
+ def validate_file!
61
+ return unless Unzip.is_zip?(archive)
62
+ Validation.validate_shapefile_archive!(Download.entries(archive), default_proj4_projection: default_proj4_projection)
63
+ end
64
+
65
+ def archive
66
+ @archive ||= Download.open(@data)
54
67
  end
55
68
  end
56
69
 
57
70
  # ERRORS
58
71
  class IndeterminateShapefileProjection < SpatialFeatures::ImportError; end
59
72
  class IncompleteShapefileArchive < SpatialFeatures::ImportError; end
73
+ class InvalidShapefileArchive < SpatialFeatures::ImportError; end
60
74
  end
61
75
  end
@@ -0,0 +1,55 @@
1
+ module SpatialFeatures
2
+ module Validation
3
+ # SHP file must come first
4
+ REQUIRED_SHAPEFILE_COMPONENT_EXTENSIONS = %w[shp shx dbf prj].freeze
5
+
6
+ # Check if a shapefile archive includes the required component files, otherwise
7
+ # raise an exception.
8
+ #
9
+ # @param [Zip::File] zip_file A Zip::File object
10
+ # @param [String] default_proj4_projection Optional, if supplied we don't raise an exception when we're missing a .PRJ file
11
+ # @param [Boolean] allow_generic_zip_files When true, we skip validation entirely if the archive does not contain a .SHP file
12
+ def self.validate_shapefile_archive!(zip_file, default_proj4_projection: nil, allow_generic_zip_files: false)
13
+ zip_file_entries = zip_file.entries.each_with_object({}) do |f, obj|
14
+ ext = File.extname(f.name).downcase[1..-1]
15
+ next unless ext
16
+
17
+ if ext.casecmp?("shp") && obj.key?(ext)
18
+ raise ::SpatialFeatures::Importers::InvalidShapefileArchive, "Zip files that contain multiple Shapefiles are not supported. Please separate each Shapefile into its own zip file."
19
+ end
20
+
21
+ obj[ext] = File.basename(f.name, '.*')
22
+ end
23
+
24
+ shapefile_basename = zip_file_entries["shp"]
25
+ unless shapefile_basename
26
+ # not a shapefile archive but we don't care
27
+ return if allow_generic_zip_files
28
+
29
+ raise ::SpatialFeatures::Importers::IncompleteShapefileArchive, "Shapefile archive is missing a SHP file"
30
+ end
31
+
32
+ REQUIRED_SHAPEFILE_COMPONENT_EXTENSIONS[1..-1].each do |ext|
33
+ ext_basename = zip_file_entries[ext]
34
+ next if ext_basename&.casecmp?(shapefile_basename)
35
+
36
+ case ext
37
+ when "prj"
38
+ # special case for missing projection files to allow using default_proj4_projection
39
+ next if default_proj4_projection
40
+
41
+ raise ::SpatialFeatures::Importers::IndeterminateShapefileProjection, "Shapefile archive is missing a projection file: #{expected_component_path(shapefile_basename, ext)}"
42
+ else
43
+ # for all un-handled cases of missing files raise the more generic error
44
+ raise ::SpatialFeatures::Importers::IncompleteShapefileArchive, "Shapefile archive is missing a required file: #{expected_component_path(shapefile_basename, ext)}"
45
+ end
46
+ end
47
+
48
+ true
49
+ end
50
+
51
+ def self.expected_component_path(basename, ext)
52
+ "#{basename}.#{ext}"
53
+ end
54
+ end
55
+ end
@@ -1,3 +1,3 @@
1
1
  module SpatialFeatures
2
- VERSION = "2.14.1"
2
+ VERSION = "2.15.0"
3
3
  end
metadata CHANGED
@@ -1,15 +1,15 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: spatial_features
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.14.1
4
+ version: 2.15.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ryan Wallace
8
8
  - Nicholas Jakobsen
9
- autorequire:
9
+ autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2021-03-15 00:00:00.000000000 Z
12
+ date: 2021-07-06 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rails
@@ -20,7 +20,7 @@ dependencies:
20
20
  version: '4.2'
21
21
  - - "<"
22
22
  - !ruby/object:Gem::Version
23
- version: '6.0'
23
+ version: '7.0'
24
24
  type: :runtime
25
25
  prerelease: false
26
26
  version_requirements: !ruby/object:Gem::Requirement
@@ -30,7 +30,7 @@ dependencies:
30
30
  version: '4.2'
31
31
  - - "<"
32
32
  - !ruby/object:Gem::Version
33
- version: '6.0'
33
+ version: '7.0'
34
34
  - !ruby/object:Gem::Dependency
35
35
  name: delayed_job_active_record
36
36
  requirement: !ruby/object:Gem::Requirement
@@ -107,14 +107,14 @@ dependencies:
107
107
  requirements:
108
108
  - - "~>"
109
109
  - !ruby/object:Gem::Version
110
- version: '0'
110
+ version: '1'
111
111
  type: :development
112
112
  prerelease: false
113
113
  version_requirements: !ruby/object:Gem::Requirement
114
114
  requirements:
115
115
  - - "~>"
116
116
  - !ruby/object:Gem::Version
117
- version: '0'
117
+ version: '1'
118
118
  - !ruby/object:Gem::Dependency
119
119
  name: rspec
120
120
  requirement: !ruby/object:Gem::Requirement
@@ -166,6 +166,7 @@ files:
166
166
  - lib/spatial_features/uncached_result.rb
167
167
  - lib/spatial_features/unzip.rb
168
168
  - lib/spatial_features/utils.rb
169
+ - lib/spatial_features/validation.rb
169
170
  - lib/spatial_features/venn_polygons.rb
170
171
  - lib/spatial_features/version.rb
171
172
  - lib/tasks/spatial_features_tasks.rake
@@ -173,7 +174,7 @@ homepage: https://github.com/culturecode/spatial_features
173
174
  licenses:
174
175
  - MIT
175
176
  metadata: {}
176
- post_install_message:
177
+ post_install_message:
177
178
  rdoc_options: []
178
179
  require_paths:
179
180
  - lib
@@ -188,8 +189,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
188
189
  - !ruby/object:Gem::Version
189
190
  version: '0'
190
191
  requirements: []
191
- rubygems_version: 3.0.3
192
- signing_key:
192
+ rubygems_version: 3.0.8
193
+ signing_key:
193
194
  specification_version: 4
194
195
  summary: Adds spatial methods to a model.
195
196
  test_files: []