spatial_features 3.4.1 → 3.4.8

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
  SHA256:
3
- metadata.gz: 4b15396511a7b6b6d17f4a2181793d9b2a35a3a88ba10b49106c9e43cada84b5
4
- data.tar.gz: 92c9d1a6b207e1999f63b1e5ab20aec4625b17663e411bcfc7836146c8af4666
3
+ metadata.gz: 9407d9a0f72fe25d714ee781b2902f387c77837c8deb3b1c1df8cdaa7be97b35
4
+ data.tar.gz: af6a1f4386fcb85adb7b0c17925f13ff11f0730fd4fe407cdbf1c8efdcfbbdee
5
5
  SHA512:
6
- metadata.gz: c15ac3afdd9d23f6e12204a775ad2c8c2777a4c65c494f25560c16db189407a513cd694a71b6d289c81fb4b3872a9621d31da31aaa7bec2830bb233a901ad2d4
7
- data.tar.gz: c318e7c0897727c41d950451ae7ee65fded8332ded5ec73233660dd8c3285eea571c3160e46eae2b8447131ae8fe0b0559182724719fae3548082ac082e3a3f5
6
+ metadata.gz: 22acdcc935004caaf2783679b0e0126ddfe679cd7f51c29f0eb6f358746c9cfb01c67dd5f67db4bfa26f975aec7da783d6e3ad45312e292081677ff194761552
7
+ data.tar.gz: 84fd0dcfd2a158bd7612df2da14b6124bc9b5a4b668779057037cf1c17a4be03eda6641ee9204d3eef696766e446c3764fcb6976ea3387ad660e8fef63285c13
@@ -19,14 +19,14 @@ class AbstractFeature < ActiveRecord::Base
19
19
  before_save :sanitize, if: :will_save_change_to_geog?
20
20
  after_save :cache_derivatives, :if => [:automatically_cache_derivatives?, :saved_change_to_geog?]
21
21
 
22
- # for Rails >= 5 ActiveRecord collections we override the collection_cache_key
23
- # to prevent Rails doing its default query on `updated_at`
24
- def self.collection_cache_key(_collection, _timestamp_column)
25
- self.cache_key
22
+ def self.cache_key
23
+ collection_cache_key
26
24
  end
27
25
 
28
- def self.cache_key
29
- "#{maximum(:id)}-#{count}"
26
+ # for Rails >= 5 ActiveRecord collections we override the collection_cache_key
27
+ # to prevent Rails doing its default query on `updated_at`
28
+ def self.collection_cache_key(collection = all, *)
29
+ "#{collection.maximum(:id)}-#{collection.count}"
30
30
  end
31
31
 
32
32
  def self.with_metadata(k, v)
@@ -142,7 +142,7 @@ class AbstractFeature < ActiveRecord::Base
142
142
  # Result is a hex string representing the desired binary output so we need to convert it to binary
143
143
  result = SpatialFeatures::Utils.select_db_value(select_sql)
144
144
  result.remove!(/^\\x/)
145
- result = result.scan(/../).map(&:hex).pack('c*')
145
+ result = [result].pack('H*')
146
146
 
147
147
  return result
148
148
  end
@@ -9,6 +9,8 @@ class Feature < AbstractFeature
9
9
 
10
10
  has_one :aggregate_feature, lambda { |feature| where(:spatial_model_type => feature.spatial_model_type) }, :foreign_key => :spatial_model_id, :primary_key => :spatial_model_id
11
11
 
12
+ scope :source_identifier, lambda {|source_identifier| where(:source_identifier => source_identifier) if source_identifier.present? }
13
+
12
14
  validates_inclusion_of :feature_type, :in => FEATURE_TYPES
13
15
 
14
16
  before_save :truncate_name
@@ -1,10 +1,10 @@
1
1
  module SpatialExtensions
2
2
  private
3
3
 
4
- def abstract_refresh_geometry_action(models)
4
+ def abstract_refresh_geometry_action(models, **update_options)
5
5
  Array.wrap(models).each do |model|
6
6
  model.failed_feature_update_jobs.destroy_all
7
- model.delay_update_features!
7
+ model.delay_update_features!(**update_options)
8
8
  end
9
9
  end
10
10
 
@@ -54,6 +54,10 @@ module SpatialFeatures
54
54
  if skip_invalid
55
55
  Rails.logger.warn "Error updating #{self.class} #{self.id}. #{e.message}"
56
56
  return nil
57
+ elsif ENCODING_ERROR.match?(e.message)
58
+ raise ImportEncodingError,
59
+ "One or more features you are trying to import has text encoded in an un-supported format (#{e.message})",
60
+ e.backtrace
57
61
  else
58
62
  raise ImportError, e.message, e.backtrace
59
63
  end
@@ -151,5 +155,5 @@ module SpatialFeatures
151
155
  end
152
156
  end
153
157
 
154
- class ImportError < StandardError; end
158
+ ENCODING_ERROR = /invalid byte sequence/i.freeze
155
159
  end
@@ -2,16 +2,25 @@ module SpatialFeatures
2
2
  module QueuedSpatialProcessing
3
3
  extend ActiveSupport::Concern
4
4
 
5
- def queue_update_spatial_cache(*args)
6
- queue_spatial_task('update_spatial_cache', *args)
5
+ def self.update_cached_status(record, method_name, state)
6
+ return unless record.has_attribute?(:spatial_processing_status_cache)
7
+
8
+ cache = record.spatial_processing_status_cache || {}
9
+ cache[method_name] = state
10
+ record.spatial_processing_status_cache = cache
11
+ record.update_column(:spatial_processing_status_cache, cache) if record.will_save_change_to_spatial_processing_status_cache?
12
+ end
13
+
14
+ def queue_update_spatial_cache(*args, **kwargs)
15
+ queue_spatial_task('update_spatial_cache', *args, **kwargs)
7
16
  end
8
17
 
9
- def delay_update_features!(*args)
10
- queue_spatial_task('update_features!', *args)
18
+ def delay_update_features!(*args, **kwargs)
19
+ queue_spatial_task('update_features!', *args, **kwargs)
11
20
  end
12
21
 
13
- def updating_features?
14
- case spatial_processing_status(:update_features!)
22
+ def updating_features?(**options)
23
+ case spatial_processing_status(:update_features!, **options)
15
24
  when :queued, :processing
16
25
  true
17
26
  else
@@ -23,18 +32,37 @@ module SpatialFeatures
23
32
  spatial_processing_status(:update_features!) == :failure
24
33
  end
25
34
 
26
- def spatial_processing_status(method_name)
35
+ def spatial_processing_status(method_name, use_cache: true)
27
36
  if has_attribute?(:spatial_processing_status_cache)
37
+ update_spatial_processing_status(method_name) unless use_cache
28
38
  spatial_processing_status_cache[method_name.to_s]&.to_sym
29
39
  end
30
40
  end
31
41
 
42
+ def update_spatial_processing_status(method_name)
43
+ latest_job = spatial_processing_jobs(method_name).last
44
+
45
+ if !latest_job
46
+ SpatialFeatures::QueuedSpatialProcessing.update_cached_status(self, method_name, nil)
47
+ elsif latest_job.failed_at?
48
+ SpatialFeatures::QueuedSpatialProcessing.update_cached_status(self, method_name, :failure)
49
+ elsif latest_job.locked_at?
50
+ SpatialFeatures::QueuedSpatialProcessing.update_cached_status(self, method_name, :processing)
51
+ else
52
+ SpatialFeatures::QueuedSpatialProcessing.update_cached_status(self, method_name, :queued)
53
+ end
54
+ end
55
+
32
56
  def feature_update_error
33
57
  (failed_feature_update_jobs.first.try(:last_error) || '').split("\n").first
34
58
  end
35
59
 
36
60
  def running_feature_update_jobs
37
- spatial_processing_jobs('update_features!').where(failed_at: nil)
61
+ spatial_processing_jobs('update_features!').where(failed_at: nil).where.not(locked_at: nil)
62
+ end
63
+
64
+ def queued_feature_update_jobs
65
+ spatial_processing_jobs('update_features!').where(failed_at: nil, locked_at: nil)
38
66
  end
39
67
 
40
68
  def failed_feature_update_jobs
@@ -47,8 +75,9 @@ module SpatialFeatures
47
75
 
48
76
  private
49
77
 
50
- def queue_spatial_task(method_name, *args)
51
- Delayed::Job.enqueue SpatialProcessingJob.new(self, method_name, *args), :queue => spatial_processing_queue_name + method_name
78
+ def queue_spatial_task(method_name, *args, priority: 1, **kwargs)
79
+ # NOTE: We pass kwargs as an arg because Delayed::Job does not support separation of positional and keyword arguments in Ruby 3.0. Instead we perform manual extraction in `perform`.
80
+ Delayed::Job.enqueue SpatialProcessingJob.new(self, method_name, *args, kwargs), :queue => spatial_processing_queue_name + method_name, :priority => priority
52
81
  end
53
82
 
54
83
  def spatial_processing_queue_name
@@ -68,9 +97,15 @@ module SpatialFeatures
68
97
  update_cached_status(:queued)
69
98
  end
70
99
 
100
+ def before(job)
101
+ ids = running_jobs.where.not(:id => job.id).pluck(:id)
102
+ raise "Already processing delayed jobs in this spatial queue: Delayed::Job #{ids.to_sentence}." if ids.present?
103
+ end
104
+
71
105
  def perform
72
106
  update_cached_status(:processing)
73
- @record.send(@method_name, *@args)
107
+ options = @args.extract_options!
108
+ @record.send(@method_name, *@args, **options)
74
109
  end
75
110
 
76
111
  def success(job)
@@ -88,11 +123,13 @@ module SpatialFeatures
88
123
  private
89
124
 
90
125
  def update_cached_status(state)
91
- if @record.has_attribute?(:spatial_processing_status_cache)
92
- cache = @record.spatial_processing_status_cache || {}
93
- cache[@method_name] = state
94
- @record.update_column(:spatial_processing_status_cache, cache)
95
- end
126
+ SpatialFeatures::QueuedSpatialProcessing.update_cached_status(@record, @method_name, state)
127
+ end
128
+
129
+ def running_jobs
130
+ @record.spatial_processing_jobs
131
+ .where(:locked_at => Delayed::Worker.max_run_time.ago..Time.current)
132
+ .where(:failed_at => nil)
96
133
  end
97
134
  end
98
135
  end
@@ -29,7 +29,7 @@ module SpatialFeatures
29
29
  delegate :has_spatial_features_hash?, :has_features_area?, :to => self
30
30
  end
31
31
 
32
- self.spatial_features_options = self.spatial_features_options.merge(options)
32
+ self.spatial_features_options = self.spatial_features_options.deep_merge(options)
33
33
  end
34
34
  end
35
35
 
@@ -66,5 +66,6 @@ module SpatialFeatures
66
66
  # EXCEPTIONS
67
67
 
68
68
  class ImportError < StandardError; end
69
+ class ImportEncodingError < ImportError; end
69
70
  class EmptyImportError < ImportError; end
70
71
  end
@@ -6,6 +6,9 @@ module SpatialFeatures
6
6
  class Shapefile < Base
7
7
  class_attribute :default_proj4_projection
8
8
 
9
+ FEATURE_TYPE_FOR_DIMENSION = { 0 => 'point', 1 => 'line', 2 => 'polygon' }.freeze
10
+ PROJ4_4326 = '+proj=longlat +datum=WGS84 +no_defs'.freeze
11
+
9
12
  def initialize(data, proj4: nil, **options)
10
13
  super(data, **options)
11
14
  @proj4 = proj4
@@ -25,15 +28,10 @@ module SpatialFeatures
25
28
 
26
29
  private
27
30
 
28
- def build_features
29
- validate_shapefile!
30
- super
31
- end
32
-
33
- def each_record(&block)
34
- RGeo::Shapefile::Reader.open(file.path) do |records|
31
+ def each_record
32
+ open_shapefile(archive) do |records, proj4|
35
33
  records.each do |record|
36
- yield OpenStruct.new data_from_wkt(record.geometry.as_text, proj4_projection).merge(:metadata => record.attributes) if record.geometry.present?
34
+ yield OpenStruct.new data_from_record(record, proj4) if record.geometry.present?
37
35
  end
38
36
  end
39
37
  rescue Errno::ENOENT => e
@@ -45,26 +43,61 @@ module SpatialFeatures
45
43
  end
46
44
  end
47
45
 
48
- def proj4_projection
49
- @proj4 ||= proj4_from_file || default_proj4_projection || raise(IndeterminateShapefileProjection, 'Could not determine shapefile projection. Check that `gdalsrsinfo` is installed.')
46
+ def data_from_record(record, proj4 = nil)
47
+ geometry = record.geometry
48
+ wkt = geometry.as_text
49
+ data = { :metadata => record.attributes, feature_type: FEATURE_TYPE_FOR_DIMENSION.fetch(geometry.dimension) }
50
+
51
+ if proj4 == PROJ4_4326
52
+ data[:geog] = wkt
53
+ else
54
+ data[:geog] = ActiveRecord::Base.connection.select_value <<-SQL
55
+ SELECT ST_Transform(ST_GeomFromText('#{wkt}'), '#{proj4}', 4326) AS geog
56
+ SQL
57
+ end
58
+
59
+ return data
50
60
  end
51
61
 
52
- def proj4_from_file
53
- # Sanitize: "'+proj=utm +zone=11 +datum=NAD83 +units=m +no_defs '\n" and lately
54
- # "+proj=utm +zone=11 +datum=NAD83 +units=m +no_defs \n" to
55
- # "+proj=utm +zone=11 +datum=NAD83 +units=m +no_defs"
56
- `gdalsrsinfo "#{file.path}" -o proj4`.strip.remove(/^'|'$/).presence
62
+ def open_shapefile(file, &block)
63
+ # the individual SHP file for processing (automatically extracted from a ZIP archive if necessary)
64
+ file = possible_shp_files.first if Unzip.is_zip?(file)
65
+ projected_file = project_to_4326(file.path)
66
+ file = projected_file || file
67
+ validate_shapefile!(file.path)
68
+ proj4 = proj4_projection(file.path)
69
+
70
+ RGeo::Shapefile::Reader.open(file.path) do |records| # Fall back to unprojected geometry if projection fails
71
+ block.call records, proj4
72
+ end
73
+ ensure
74
+ if projected_file
75
+ projected_file.close
76
+ ::File.delete(projected_file)
77
+ end
78
+ end
79
+
80
+ def proj4_projection(file_path)
81
+ proj4_from_file(file_path) || default_proj4_projection || raise(IndeterminateShapefileProjection, 'Could not determine shapefile projection. Check that `gdalsrsinfo` is installed.')
57
82
  end
58
83
 
59
- def data_from_wkt(wkt, proj4)
60
- ActiveRecord::Base.connection.select_one <<-SQL
61
- SELECT ST_Transform(ST_GeomFromText('#{wkt}'), '#{proj4}', 4326) AS geog, GeometryType(ST_GeomFromText('#{wkt}')) AS feature_type
62
- SQL
84
+ def validate_shapefile!(file_path)
85
+ Validation.validate_shapefile!(::File.open(file_path), default_proj4_projection: default_proj4_projection)
63
86
  end
64
87
 
65
- # the individual SHP file for processing (automatically extracted from a ZIP archive if necessary)
66
- def file
67
- @file ||= Unzip.is_zip?(archive) ? possible_shp_files.first : archive
88
+ # Use OGR2OGR to reproject into EPSG:4326 so we can skip the reprojection step per-feature
89
+ def project_to_4326(file_path)
90
+ output_path = Tempfile.create([::File.basename(file_path, '.shp') + '_epsg_4326_', '.shp']) { |file| file.path }
91
+ return unless (proj4 = proj4_from_file(file_path))
92
+ return unless system("ogr2ogr -s_srs '#{proj4}' -t_srs EPSG:4326 '#{output_path}' '#{file_path}'")
93
+ return ::File.open(output_path)
94
+ end
95
+
96
+ def proj4_from_file(file_path)
97
+ # Sanitize: "'+proj=utm +zone=11 +datum=NAD83 +units=m +no_defs '\n" and lately
98
+ # "+proj=utm +zone=11 +datum=NAD83 +units=m +no_defs \n" to
99
+ # "+proj=utm +zone=11 +datum=NAD83 +units=m +no_defs"
100
+ `gdalsrsinfo "#{file_path}" -o proj4`.strip.remove(/^'|'$/).presence
68
101
  end
69
102
 
70
103
  # a zip archive may contain multiple SHP files
@@ -76,10 +109,6 @@ module SpatialFeatures
76
109
  end
77
110
  end
78
111
 
79
- def validate_shapefile!
80
- Validation.validate_shapefile!(file, default_proj4_projection: default_proj4_projection)
81
- end
82
-
83
112
  def archive
84
113
  @archive ||= Download.open(@data)
85
114
  end
@@ -1,3 +1,3 @@
1
1
  module SpatialFeatures
2
- VERSION = "3.4.1"
2
+ VERSION = "3.4.8"
3
3
  end
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: 3.4.1
4
+ version: 3.4.8
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: 2023-10-26 00:00:00.000000000 Z
12
+ date: 2024-01-03 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rails