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 +4 -4
- data/app/models/abstract_feature.rb +7 -7
- data/app/models/feature.rb +2 -0
- data/lib/spatial_features/controller_helpers/spatial_extensions.rb +2 -2
- data/lib/spatial_features/has_spatial_features/feature_import.rb +5 -1
- data/lib/spatial_features/has_spatial_features/queued_spatial_processing.rb +53 -16
- data/lib/spatial_features/has_spatial_features.rb +1 -1
- data/lib/spatial_features/importers/base.rb +1 -0
- data/lib/spatial_features/importers/shapefile.rb +55 -26
- data/lib/spatial_features/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 9407d9a0f72fe25d714ee781b2902f387c77837c8deb3b1c1df8cdaa7be97b35
|
4
|
+
data.tar.gz: af6a1f4386fcb85adb7b0c17925f13ff11f0730fd4fe407cdbf1c8efdcfbbdee
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
|
23
|
-
|
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
|
-
|
29
|
-
|
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.
|
145
|
+
result = [result].pack('H*')
|
146
146
|
|
147
147
|
return result
|
148
148
|
end
|
data/app/models/feature.rb
CHANGED
@@ -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
|
-
|
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
|
6
|
-
|
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
|
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
|
-
@
|
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
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
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.
|
32
|
+
self.spatial_features_options = self.spatial_features_options.deep_merge(options)
|
33
33
|
end
|
34
34
|
end
|
35
35
|
|
@@ -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
|
29
|
-
|
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
|
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
|
49
|
-
|
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
|
53
|
-
#
|
54
|
-
|
55
|
-
|
56
|
-
|
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
|
60
|
-
|
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
|
-
#
|
66
|
-
def
|
67
|
-
|
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
|
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.
|
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:
|
12
|
+
date: 2024-01-03 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: rails
|