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