spatial_features 2.17.3 → 2.20.0

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: e0a8fb7f4886f07f02b8b110f7975d91538324d6cf2c5a70b3b247dfb6719c98
4
- data.tar.gz: 15c681c5de6fccb10a515c8ff1f8a423a7c9fa1c1fded8955c2072dd76f5d4f2
3
+ metadata.gz: 639dee46fedd2cf507642b41119ff99e93506b54d891ab42d21a1f24fcf5a679
4
+ data.tar.gz: 956175227c6a59acc867810a51796303218eac70c9bc49fe0d0f8222ea8558e6
5
5
  SHA512:
6
- metadata.gz: 0a9224909db6dd85bf20cf9b9561e78cdc12f094423c86b0280f0e2a96e2dad9ae24030c4a4014b448f566e232b7dd37620d2a8890c411af97487a0047261346
7
- data.tar.gz: 740174a0882bc82edd053e4adf31318c6afc51860a66a3d50068e3c106cc8ac9351061f9d4990c652c5f41704815e2d5b8c8ef3d6b7aaa47525d9663f01acce4
6
+ metadata.gz: 6a1ac7a0dca15400e0e03f0f1c2192e22a4c1e818edde2a832f925e6ee803a9284c5236684228e25dbeabf2d147aacef4f4c05170216b0857941c87ed9b9a395
7
+ data.tar.gz: da6fc8be7449202dfe46de18f0829e49664626ba17b90b6e3c1eae7a059f38a10d8310046e4f2bf44fb80bf78f2cdeea26624f429fc0d7b5873775b332086712
data/README.md CHANGED
@@ -109,8 +109,17 @@ Person.new(:features => [Feature.new(:geog => 'some binary PostGIS Geography str
109
109
  You can specify multiple import sources for geometry. Each key is a method that returns the data for the Importer, and
110
110
  each value is the Importer to use to parse the data. See each Importer for more details.
111
111
  ```ruby
112
+ def ImageImporter
113
+ def self.call(feature, image_paths)
114
+ image_paths.each do |pathname|
115
+ # ...
116
+ end
117
+ end
118
+ end
119
+
112
120
  class Location < ActiveRecord::Base
113
- has_spatial_features :import => { :remote_kml_url => 'KMLFile', :file => 'File', :geojson => 'GeoJSON' }
121
+ has_spatial_features :import => { :remote_kml_url => 'KMLFile', :file => 'File', :geojson => 'GeoJSON' },
122
+ :image_handlers => ['ImageImporter']
114
123
 
115
124
  def remote_kml_url
116
125
  "www.test.com/kml/#{id}.kml"
@@ -49,8 +49,7 @@ class AbstractFeature < ActiveRecord::Base
49
49
  where(:feature_type => 'point')
50
50
  end
51
51
 
52
- def self.within_distance(lat, lng, distance_in_meters)
53
- # where("ST_DWithin(features.geog, ST_SetSRID( ST_Point( -71.104, 42.315), 4326)::geography, :distance)", :lat => lat, :lng => lng, :distance => distance_in_meters)
52
+ def self.within_distance_of_point(lat, lng, distance_in_meters)
54
53
  where("ST_DWithin(features.geog, ST_Point(:lng, :lat), :distance)", :lat => lat, :lng => lng, :distance => distance_in_meters)
55
54
  end
56
55
 
@@ -73,6 +72,10 @@ class AbstractFeature < ActiveRecord::Base
73
72
  join_other_features(other).where('ST_Intersects(features.geom_lowres, other_features.geom_lowres)').uniq
74
73
  end
75
74
 
75
+ def self.within_distance(other, distance_in_meters)
76
+ join_other_features(other).where('ST_DWithin(features.geom_lowres, other_features.geom_lowres, ?)', distance_in_meters).uniq
77
+ end
78
+
76
79
  def self.invalid(column = 'geog::geometry')
77
80
  select("features.*, ST_IsValidReason(#{column}) AS invalid_geometry_message").where.not("ST_IsValid(#{column})")
78
81
  end
@@ -4,7 +4,11 @@ class AggregateFeature < AbstractFeature
4
4
  has_many :features, lambda { |aggregate| where(:spatial_model_type => aggregate.spatial_model_type) }, :foreign_key => :spatial_model_id, :primary_key => :spatial_model_id
5
5
 
6
6
  # Aggregate the features for the spatial model into a single feature
7
- def refresh
7
+ before_validation :set_geog, :on => :create, :unless => :geog?
8
+
9
+ private
10
+
11
+ def set_geog
8
12
  feature_array_sql = <<~SQL
9
13
  ARRAY[
10
14
  (#{features.select('ST_UNION(ST_CollectionExtract(geog::geometry, 1))').to_sql}),
@@ -19,6 +23,5 @@ class AggregateFeature < AbstractFeature
19
23
  FROM (SELECT unnest(#{feature_array_sql})) AS features
20
24
  WHERE NOT ST_IsEmpty(unnest)
21
25
  SQL
22
- self.save!
23
26
  end
24
27
  end
@@ -1,3 +1,5 @@
1
+ require_dependency SpatialFeatures::Engine.root.join('app/models/abstract_feature')
2
+
1
3
  class Feature < AbstractFeature
2
4
  class_attribute :automatically_refresh_aggregate
3
5
  self.automatically_refresh_aggregate = true
@@ -13,6 +15,8 @@ class Feature < AbstractFeature
13
15
 
14
16
  after_save :refresh_aggregate, if: :automatically_refresh_aggregate?
15
17
 
18
+ attr_accessor :importable_image_paths # :nodoc:
19
+
16
20
  # Features are used for display so we also cache their KML representation
17
21
  def self.cache_derivatives(options = {})
18
22
  super
@@ -52,8 +56,8 @@ class Feature < AbstractFeature
52
56
  end
53
57
 
54
58
  def refresh_aggregate
55
- build_aggregate_feature unless aggregate_feature&.persisted?
56
- aggregate_feature.refresh
59
+ aggregate_feature&.destroy # Destroy the existing aggregate feature to ensure its cache key changes when it is refreshed
60
+ create_aggregate_feature!
57
61
  end
58
62
 
59
63
  def automatically_refresh_aggregate?
@@ -8,7 +8,7 @@ module SpatialFeatures
8
8
  included do
9
9
  extend ActiveModel::Callbacks
10
10
  define_model_callbacks :update_features
11
- spatial_features_options.reverse_merge!(:import => {}, spatial_cache: [])
11
+ spatial_features_options.reverse_merge!(:import => {}, :spatial_cache => [], :image_handlers => [])
12
12
  end
13
13
 
14
14
  module ClassMethods
@@ -78,13 +78,30 @@ module SpatialFeatures
78
78
  "SpatialFeatures::Importers::#{importer_name}".constantize
79
79
  end
80
80
 
81
+ def handle_images(feature)
82
+ return if feature.importable_image_paths.nil? || feature.importable_image_paths.empty?
83
+
84
+ Array(spatial_features_options[:image_handlers]).each do |image_handler|
85
+ image_handler_from_name(image_handler).call(feature, feature.importable_image_paths)
86
+ end
87
+ end
88
+
89
+ def image_handler_from_name(handler_name)
90
+ handler_name.to_s.constantize
91
+ end
92
+
81
93
  def import_features(imports, skip_invalid)
82
94
  features.delete_all
83
95
  valid, invalid = Feature.defer_aggregate_refresh do
84
96
  Feature.without_caching_derivatives do
85
97
  imports.flat_map(&:features).partition do |feature|
86
98
  feature.spatial_model = self
87
- feature.save
99
+ if feature.save
100
+ handle_images(feature)
101
+ true
102
+ else
103
+ false
104
+ end
88
105
  end
89
106
  end
90
107
  end
@@ -11,7 +11,22 @@ module SpatialFeatures
11
11
  end
12
12
 
13
13
  def updating_features?
14
- running_feature_update_jobs.exists?
14
+ case spatial_processing_status(:update_features!)
15
+ when :queued, :processing
16
+ true
17
+ else
18
+ false
19
+ end
20
+ end
21
+
22
+ def updating_features_failed?
23
+ spatial_processing_status(:update_features!) == :failure
24
+ end
25
+
26
+ def spatial_processing_status(method_name)
27
+ if has_attribute?(:spatial_processing_status_cache)
28
+ spatial_processing_status_cache[method_name.to_s]&.to_sym
29
+ end
15
30
  end
16
31
 
17
32
  def feature_update_error
@@ -27,17 +42,58 @@ module SpatialFeatures
27
42
  end
28
43
 
29
44
  def spatial_processing_jobs(suffix = nil)
30
- Delayed::Job.where('queue LIKE ?', "#{spatial_processing_queue_name}#{suffix}%")
45
+ Delayed::Job.where(:queue => "#{spatial_processing_queue_name}#{suffix}")
31
46
  end
32
47
 
33
48
  private
34
49
 
35
50
  def queue_spatial_task(method_name, *args)
36
- delay(:queue => spatial_processing_queue_name + method_name).send(method_name, *args)
51
+ Delayed::Job.enqueue SpatialProcessingJob.new(self, method_name, *args), :queue => spatial_processing_queue_name + method_name
37
52
  end
38
53
 
39
54
  def spatial_processing_queue_name
40
- "#{self.class}/#{self.id}/"
55
+ "#{model_name}/#{id}/"
56
+ end
57
+
58
+ # CLASSES
59
+
60
+ class SpatialProcessingJob
61
+ def initialize(record, method_name, *args)
62
+ @record = record
63
+ @method_name = method_name
64
+ @args = args
65
+ end
66
+
67
+ def enqueue(job)
68
+ update_cached_status(:queued)
69
+ end
70
+
71
+ def perform
72
+ update_cached_status(:processing)
73
+ @record.send(@method_name, *@args)
74
+ end
75
+
76
+ def success(job)
77
+ update_cached_status(:success)
78
+ end
79
+
80
+ def error(job, exception)
81
+ update_cached_status(:failure)
82
+ end
83
+
84
+ def failure(job)
85
+ update_cached_status(:failure)
86
+ end
87
+
88
+ private
89
+
90
+ 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
96
+ end
41
97
  end
42
98
  end
43
99
  end
@@ -46,7 +46,8 @@ module SpatialFeatures
46
46
  end
47
47
 
48
48
  def build_feature(record)
49
- Feature.new(:name => record.name, :metadata => record.metadata, :feature_type => record.feature_type, :geog => record.geog, :make_valid => @make_valid)
49
+ importable_image_paths = record.importable_image_paths if record.respond_to?(:importable_image_paths)
50
+ Feature.new(:name => record.name, :metadata => record.metadata, :feature_type => record.feature_type, :geog => record.geog, :importable_image_paths => importable_image_paths, :make_valid => @make_valid)
50
51
  end
51
52
  end
52
53
  end
@@ -3,6 +3,14 @@ require 'ostruct'
3
3
  module SpatialFeatures
4
4
  module Importers
5
5
  class KML < Base
6
+ # <SimpleData name> keys that may contain <img> tags
7
+ IMAGE_METADATA_KEYS = %w[pdfmaps_photos].freeze
8
+
9
+ def initialize(data, base_dir: nil, **args)
10
+ @base_dir = base_dir
11
+ super data, **args
12
+ end
13
+
6
14
  private
7
15
 
8
16
  def each_record(&block)
@@ -18,10 +26,11 @@ module SpatialFeatures
18
26
  next if blank_feature?(feature)
19
27
 
20
28
  geog = geom_from_kml(feature)
21
-
22
29
  next if geog.blank?
23
30
 
24
- yield OpenStruct.new(:feature_type => sql_type, :geog => geog, :name => name, :metadata => metadata)
31
+ importable_image_paths = images_from_metadata(metadata)
32
+
33
+ yield OpenStruct.new(:feature_type => sql_type, :geog => geog, :name => name, :metadata => metadata, :importable_image_paths => importable_image_paths)
25
34
  end
26
35
  end
27
36
  end
@@ -32,17 +41,35 @@ module SpatialFeatures
32
41
 
33
42
  def geom_from_kml(kml)
34
43
  geom = nil
44
+ conn = nil
35
45
 
36
46
  # Do query in a new thread so we use a new connection (if the query fails it will poison the transaction of the current connection)
47
+ #
48
+ # We manually checkout a new connection since Rails re-uses DB connections across threads.
37
49
  Thread.new do
38
- geom = SpatialFeatures::Utils.select_db_value("SELECT ST_GeomFromKML(#{ActiveRecord::Base.connection.quote(kml.to_s)})")
50
+ conn = ActiveRecord::Base.connection_pool.checkout
51
+ geom = conn.select_value("SELECT ST_GeomFromKML(#{conn.quote(kml.to_s)})")
39
52
  rescue ActiveRecord::StatementInvalid => e # Discard Invalid KML features
40
53
  geom = nil
54
+ ensure
55
+ ActiveRecord::Base.connection_pool.checkin(conn) if conn
41
56
  end.join
42
57
 
43
58
  return geom
44
59
  end
45
60
 
61
+ def images_from_metadata(metadata)
62
+ IMAGE_METADATA_KEYS.flat_map do |key|
63
+ images = metadata.delete(key)
64
+ next unless images
65
+
66
+ Nokogiri::HTML.fragment(images).css("img").map do |img|
67
+ next unless (src = img["src"])
68
+ @base_dir.join(src.downcase)
69
+ end
70
+ end.compact
71
+ end
72
+
46
73
  def extract_metadata(placemark)
47
74
  metadata = {}
48
75
  metadata.merge! extract_table(placemark)
@@ -1,13 +1,17 @@
1
1
  module SpatialFeatures
2
2
  module Importers
3
3
  class KMLFile < KML
4
- def initialize(path_or_url, *args)
5
- super Download.read(path_or_url, unzip: '.kml'), *args
6
-
4
+ def initialize(path_or_url, **options)
5
+ path = Download.open_each(path_or_url, unzip: [/\.kml$/], downcase: true).first
6
+ super ::File.read(path), base_dir: Pathname.new(path).dirname, **options
7
7
  rescue SocketError, Errno::ECONNREFUSED, OpenURI::HTTPError
8
8
  url = URI(path_or_url)
9
9
  raise ImportError, "KML server is not responding. Ensure server is running and accessible at #{[url.scheme, "//#{url.host}", url.port].select(&:present?).join(':')}."
10
10
  end
11
+
12
+ def cache_key
13
+ @cache_key ||= Digest::MD5.hexdigest(@data)
14
+ end
11
15
  end
12
16
  end
13
17
  end
@@ -2,6 +2,9 @@ require 'fileutils'
2
2
 
3
3
  module SpatialFeatures
4
4
  module Unzip
5
+ # paths containing '__macosx' or beginning with a '.'
6
+ IGNORED_ENTRY_PATHS = /(\A|\/)(__macosx|\.)/i.freeze
7
+
5
8
  def self.paths(file_path, find: nil, **extract_options)
6
9
  paths = extract(file_path, **extract_options)
7
10
 
@@ -16,6 +19,7 @@ module SpatialFeatures
16
19
  def self.extract(file_path, output_dir = Dir.mktmpdir, downcase: false)
17
20
  [].tap do |paths|
18
21
  entries(file_path).each do |entry|
22
+ next if entry.name =~ IGNORED_ENTRY_PATHS
19
23
  output_filename = entry.name
20
24
  output_filename = output_filename.downcase if downcase
21
25
  path = "#{output_dir}/#{output_filename}"
@@ -1,3 +1,3 @@
1
1
  module SpatialFeatures
2
- VERSION = "2.17.3"
2
+ VERSION = "2.20.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.17.3
4
+ version: 2.20.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-10-04 00:00:00.000000000 Z
12
+ date: 2021-12-27 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rails
@@ -87,20 +87,6 @@ dependencies:
87
87
  - - "~>"
88
88
  - !ruby/object:Gem::Version
89
89
  version: '1.6'
90
- - !ruby/object:Gem::Dependency
91
- name: chroma
92
- requirement: !ruby/object:Gem::Requirement
93
- requirements:
94
- - - "~>"
95
- - !ruby/object:Gem::Version
96
- version: 0.1.0
97
- type: :runtime
98
- prerelease: false
99
- version_requirements: !ruby/object:Gem::Requirement
100
- requirements:
101
- - - "~>"
102
- - !ruby/object:Gem::Version
103
- version: 0.1.0
104
90
  - !ruby/object:Gem::Dependency
105
91
  name: pg
106
92
  requirement: !ruby/object:Gem::Requirement
@@ -144,7 +130,6 @@ files:
144
130
  - app/models/feature.rb
145
131
  - app/models/spatial_cache.rb
146
132
  - app/models/spatial_proximity.rb
147
- - config/initializers/chroma_serializers.rb
148
133
  - config/initializers/mime_types.rb
149
134
  - config/initializers/register_oids.rb
150
135
  - lib/spatial_features.rb
@@ -175,7 +160,7 @@ homepage: https://github.com/culturecode/spatial_features
175
160
  licenses:
176
161
  - MIT
177
162
  metadata: {}
178
- post_install_message:
163
+ post_install_message:
179
164
  rdoc_options: []
180
165
  require_paths:
181
166
  - lib
@@ -190,8 +175,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
190
175
  - !ruby/object:Gem::Version
191
176
  version: '0'
192
177
  requirements: []
193
- rubygems_version: 3.0.8
194
- signing_key:
178
+ rubygems_version: 3.0.3
179
+ signing_key:
195
180
  specification_version: 4
196
181
  summary: Adds spatial methods to a model.
197
182
  test_files: []
@@ -1,15 +0,0 @@
1
- module Chroma
2
- class Color
3
- module Serializers
4
- # Google's Fusion Table colouring expects the alpha value in the last position, not the first
5
- def to_ft_hex
6
- [
7
- to_2char_hex(@rgb.r),
8
- to_2char_hex(@rgb.g),
9
- to_2char_hex(@rgb.b),
10
- to_2char_hex(alpha * 255)
11
- ].join('')
12
- end
13
- end
14
- end
15
- end