spatial_features 3.1.0 → 3.2.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: 9578faf8d4205e8b25b7039b99df9ead3f03f13d82568277930c8824adb48980
4
- data.tar.gz: 16a5be2783383686f570f598edd8ecf3da6350fb92d5b393618f8be919ab8c79
3
+ metadata.gz: fabdd3f0f73eb21327f8e3deb1b7dda5969d97bdb7c053c60199a63fe5cd04b1
4
+ data.tar.gz: 777c5dc188ba968ab4cba25e58fa2f361c9d3814781ecfb9e751cf0fc1b40b0e
5
5
  SHA512:
6
- metadata.gz: 772ab77070751ca764ab8a06cf5260038be3cae8461c6e9c7fa894e644d2164cd8905281c43d0eab48b7ab2f06227b01da809687ec4ffb7bc1ade359c7097eea
7
- data.tar.gz: da9f6d66e79f85362f4c6af9e3dc8400b4f6fb51bf0cb5ba84114ccb47b4541e59614d162271877c6d20d9d342fbc9a86b48de7e8a5a1de15b27c7136c09f3b3
6
+ metadata.gz: e3f28e99e814f1fcc2ba49edf8a59ff957b9f92c9e0d990f93409c61f00d4c496e7ac5f60eb3616660df720c615033f62dbe02d707c1584cc4cee7d9cb88067c
7
+ data.tar.gz: 18e776f2d1bc792e7a0313d5742345e93adca24f060472bb10a36611eb3e62cf7fe8fcf2d7d520db16cd9b1755e85b4e40e75cabcb4110e25c397e42087fd9e0
data/README.md CHANGED
@@ -31,7 +31,6 @@ Adds spatial methods to a model.
31
31
  feature_type character varying(255),
32
32
  geog geography,
33
33
  geom geometry(Geometry,4326),
34
- geom_lowres geometry(Geometry,4326),
35
34
  tilegeom geometry(Geometry,3857),
36
35
  metadata hstore,
37
36
  area double precision,
@@ -50,7 +49,6 @@ Adds spatial methods to a model.
50
49
  CREATE INDEX index_features_on_feature_type ON features USING btree (feature_type);
51
50
  CREATE INDEX index_features_on_spatial_model_id_and_spatial_model_type ON features USING btree (spatial_model_id, spatial_model_type);
52
51
  CREATE INDEX index_features_on_geom ON features USING gist (geom);
53
- CREATE INDEX index_features_on_geom_lowres ON features USING gist (geom_lowres);
54
52
  CREATE INDEX index_features_on_tilegeom ON features USING gist (tilegeom);
55
53
 
56
54
  CREATE TABLE spatial_caches (
@@ -182,6 +180,14 @@ add_index :features, :tilegeom, :using => :gist
182
180
  Feature.update_all('tilegeom = ST_Transform(geom, 3857)')
183
181
  ```
184
182
 
183
+ ## Upgrading From 3.0/3.1 to 3.2
184
+ SpatialProximity now expects the `model_a` and `model_b` records to be decided based on the name of the record type so
185
+ queries can be optimized. Migrate existing SpatialProximity rows to this new scheme by running the code below.
186
+
187
+ ```ruby
188
+ SpatialProximity.normalize
189
+ ```
190
+
185
191
  ## Testing
186
192
 
187
193
  Create a postgres database:
@@ -69,11 +69,11 @@ class AbstractFeature < ActiveRecord::Base
69
69
  end
70
70
 
71
71
  def self.intersecting(other)
72
- join_other_features(other).where('ST_Intersects(features.geom_lowres, other_features.geom_lowres)').uniq
72
+ join_other_features(other).where('ST_Intersects(features.geom_lowres, other_features.geom_lowres)').distinct
73
73
  end
74
74
 
75
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
76
+ join_other_features(other).where('ST_DWithin(features.geom_lowres, other_features.geom_lowres, ?)', distance_in_meters).distinct
77
77
  end
78
78
 
79
79
  def self.invalid(column = 'geog::geometry')
@@ -126,8 +126,8 @@ class AbstractFeature < ActiveRecord::Base
126
126
  SQL
127
127
  end
128
128
 
129
- def self.mvt(*args)
130
- select_sql = mvt_sql(*args)
129
+ def self.mvt(*args, **kwargs)
130
+ select_sql = mvt_sql(*args, **kwargs)
131
131
 
132
132
  # Result is a hex string representing the desired binary output so we need to convert it to binary
133
133
  result = SpatialFeatures::Utils.select_db_value(select_sql)
@@ -2,10 +2,34 @@ class SpatialProximity < ActiveRecord::Base
2
2
  belongs_to :model_a, :polymorphic => true
3
3
  belongs_to :model_b, :polymorphic => true
4
4
 
5
- def self.between(scope_1, scope_2)
6
- where <<-SQL.squish
7
- (#{SpatialFeatures::Utils.polymorphic_condition(scope_1, 'model_a')} AND #{SpatialFeatures::Utils.polymorphic_condition(scope_2, 'model_b')}) OR
8
- (#{SpatialFeatures::Utils.polymorphic_condition(scope_2, 'model_a')} AND #{SpatialFeatures::Utils.polymorphic_condition(scope_1, 'model_b')})
5
+ def self.between(scope1, scope2)
6
+ where condition_sql(scope1, scope2, <<~SQL.squish)
7
+ (#{SpatialFeatures::Utils.polymorphic_condition(scope1, 'model_a')} AND #{SpatialFeatures::Utils.polymorphic_condition(scope2, 'model_b')})
9
8
  SQL
10
9
  end
10
+
11
+ def self.condition_sql(scope1, scope2, template, pattern_a = 'model_a', pattern_b = 'model_b')
12
+ scope1_type = SpatialFeatures::Utils.base_class_of(scope1).to_s
13
+ scope2_type = SpatialFeatures::Utils.base_class_of(scope2).to_s
14
+
15
+ if scope1_type < scope2_type
16
+ template
17
+ elsif scope1_type > scope2_type
18
+ template.gsub(pattern_a, 'model_c').gsub(pattern_b, pattern_a).gsub('model_c', pattern_b)
19
+ else
20
+ <<~SQL.squish
21
+ (#{template}) OR (#{template.gsub(pattern_a, 'model_c').gsub(pattern_b, pattern_a).gsub('model_c', pattern_b)})
22
+ SQL
23
+ end
24
+ end
25
+
26
+ # Ensure the 'earliest' model is always model a
27
+ def self.normalize
28
+ unnormalized
29
+ .update_all('model_a_type = model_b_type, model_b_type = model_a_type, model_a_id = model_b_id, model_b_id = model_a_id')
30
+ end
31
+
32
+ def self.unnormalized
33
+ where('model_a_type > model_b_type OR (model_a_type = model_b_type AND model_a_id > model_b_id)')
34
+ end
11
35
  end
@@ -84,11 +84,13 @@ module SpatialFeatures
84
84
  results.each do |id, distance, area|
85
85
  klass_record.id = id
86
86
  SpatialProximity.create! do |proximity|
87
+ # Always make the spatial model earliest type and id be model a so we can optimize queries
88
+ data = [[Utils.base_class(record).to_s, record.id], [Utils.base_class(klass_record).to_s, klass_record.id]]
89
+ data.sort!
90
+
87
91
  # Set id and type instead of model to avoid autosaving the klass_record
88
- proximity.model_a_id = record.id
89
- proximity.model_a_type = Utils.base_class(record)
90
- proximity.model_b_id = klass_record.id
91
- proximity.model_b_type = Utils.base_class(klass_record)
92
+ proximity.model_a_type, proximity.model_a_id = data.first
93
+ proximity.model_b_type, proximity.model_b_id = data.second
92
94
  proximity.distance_in_meters = distance
93
95
  proximity.intersection_area_in_square_meters = area
94
96
  end
@@ -4,14 +4,14 @@ module SpatialFeatures
4
4
  module Download
5
5
  # file can be a url, path, or file, any of which can return be a zipped archive
6
6
  def self.open(file)
7
- file = Kernel.open(file)
7
+ file = URI.open(file)
8
8
  file = normalize_file(file) if file.is_a?(StringIO)
9
9
  return file
10
10
  end
11
11
 
12
12
  # file can be a url, path, or file, any of which can return be a zipped archive
13
- def self.open_each(file, unzip: nil, **unzip_options)
14
- file = Download.open(file)
13
+ def self.open_each(path_or_url, unzip: nil, **unzip_options)
14
+ file = Download.open(path_or_url)
15
15
  files = if unzip && Unzip.is_zip?(file)
16
16
  find_in_zip(file, find: unzip, **unzip_options)
17
17
  else
@@ -41,9 +41,9 @@ module SpatialFeatures
41
41
  update_spatial_cache(options.slice(:spatial_cache))
42
42
  end
43
43
  end
44
-
45
- return true
46
44
  end
45
+
46
+ return true
47
47
  rescue StandardError => e
48
48
  if skip_invalid
49
49
  Rails.logger.warn "Error updating #{self.class} #{self.id}. #{e.message}"
@@ -13,14 +13,14 @@ module SpatialFeatures
13
13
  has_many :features, lambda { extending FeaturesAssociationExtensions }, :as => :spatial_model, :dependent => :delete_all
14
14
  has_one :aggregate_feature, lambda { extending FeaturesAssociationExtensions }, :as => :spatial_model, :dependent => :delete
15
15
 
16
- scope :with_features, lambda { joins(:features).uniq }
16
+ scope :with_features, lambda { joins(:features).distinct }
17
17
  scope :without_features, lambda { joins("LEFT OUTER JOIN features ON features.spatial_model_type = '#{Utils.base_class(name)}' AND features.spatial_model_id = #{table_name}.id").where("features.id IS NULL") }
18
18
  scope :include_bounds, lambda { SQLHelpers.append_select(joins(:aggregate_feature), :north, :east, :south, :west) }
19
19
  scope :include_area, lambda { SQLHelpers.append_select(joins(:aggregate_feature), :area) }
20
20
 
21
- scope :with_spatial_cache, lambda {|klass| joins(:spatial_caches).where(:spatial_caches => { :intersection_model_type => Utils.class_name_with_ancestors(klass) }).uniq }
21
+ scope :with_spatial_cache, lambda {|klass| joins(:spatial_caches).where(:spatial_caches => { :intersection_model_type => Utils.class_name_with_ancestors(klass) }).distinct }
22
22
  scope :without_spatial_cache, lambda {|klass| joins("LEFT OUTER JOIN #{SpatialCache.table_name} ON #{SpatialCache.table_name}.spatial_model_id = #{table_name}.id AND #{SpatialCache.table_name}.spatial_model_type = '#{Utils.base_class(name)}' and intersection_model_type IN ('#{Utils.class_name_with_ancestors(klass).join("','") }')").where("#{SpatialCache.table_name}.spatial_model_id IS NULL") }
23
- scope :with_stale_spatial_cache, lambda { joins(:spatial_caches).where("#{table_name}.features_hash != spatial_caches.features_hash").uniq } if has_spatial_features_hash?
23
+ scope :with_stale_spatial_cache, lambda { has_spatial_features_hash? ? joins(:spatial_caches).where("#{table_name}.features_hash != spatial_caches.features_hash").distinct : none }
24
24
 
25
25
  has_many :spatial_caches, :as => :spatial_model, :dependent => :delete_all, :class_name => 'SpatialCache'
26
26
  has_many :model_a_spatial_proximities, :as => :model_a, :class_name => 'SpatialProximity', :dependent => :delete_all
@@ -136,10 +136,8 @@ module SpatialFeatures
136
136
  other_class = Utils.base_class_of(other)
137
137
  self_class = Utils.base_class_of(self)
138
138
 
139
- joins <<~SQL
140
- INNER JOIN spatial_proximities
141
- ON (spatial_proximities.model_a_type = '#{self_class}' AND spatial_proximities.model_a_id = #{table_name}.id AND spatial_proximities.model_b_type = '#{other_class}' AND spatial_proximities.model_b_id IN (#{Utils.id_sql(other)}))
142
- OR (spatial_proximities.model_b_type = '#{self_class}' AND spatial_proximities.model_b_id = #{table_name}.id AND spatial_proximities.model_a_type = '#{other_class}' AND spatial_proximities.model_a_id IN (#{Utils.id_sql(other)}))
139
+ joins "INNER JOIN spatial_proximities ON " + SpatialProximity.condition_sql(self, other, <<~SQL.squish)
140
+ (spatial_proximities.model_a_type = '#{self_class}' AND spatial_proximities.model_a_id = #{table_name}.id AND spatial_proximities.model_b_type = '#{other_class}' AND spatial_proximities.model_b_id IN (#{Utils.id_sql(other)}))
143
141
  SQL
144
142
  end
145
143
 
@@ -228,9 +226,12 @@ module SpatialFeatures
228
226
  end
229
227
 
230
228
  def total_intersection_area_percentage(klass)
231
- return 0.0 unless features_area_in_square_meters > 0
229
+ total_area = features_area_in_square_meters
232
230
 
233
- ((total_intersection_area_in_square_meters(klass) / features_area_in_square_meters) * 100).round(1)
231
+ return if total_area.nil?
232
+ return 0.0 if total_area.zero?
233
+
234
+ ((total_intersection_area_in_square_meters(klass) / total_area) * 100).round(1)
234
235
  end
235
236
 
236
237
  def features_area_in_square_meters
@@ -21,7 +21,7 @@ module SpatialFeatures
21
21
  # processed.
22
22
  #
23
23
  # If no `current_file` is passed then we just take the first valid file that we find.
24
- def initialize(data, *args, current_file: nil, **options)
24
+ def initialize(data, current_file: nil, **options)
25
25
  begin
26
26
  current_file ||= Download.open_each(data, unzip: FILE_PATTERNS, downcase: true, tmpdir: options[:tmpdir]).first
27
27
  rescue Unzip::PathNotFound
@@ -30,11 +30,11 @@ module SpatialFeatures
30
30
 
31
31
  case ::File.extname(current_file.path.downcase)
32
32
  when '.kml'
33
- __setobj__(KMLFile.new(current_file, *args, **options))
33
+ __setobj__(KMLFile.new(current_file, **options))
34
34
  when '.shp'
35
- __setobj__(Shapefile.new(current_file, *args, **options))
35
+ __setobj__(Shapefile.new(current_file, **options))
36
36
  when '.json', '.geojson'
37
- __setobj__(ESRIGeoJSON.new(current_file.path, *args, **options))
37
+ __setobj__(ESRIGeoJSON.new(current_file.path, **options))
38
38
  else
39
39
  raise ImportError, "Could not import file. " + SUPPORTED_FORMATS
40
40
  end
@@ -1,8 +1,8 @@
1
1
  module SpatialFeatures
2
2
  module Importers
3
3
  class Geomark < KMLFile
4
- def initialize(geomark, *args)
5
- super geomark_url(geomark), *args
4
+ def initialize(geomark, **options)
5
+ super geomark_url(geomark), **options
6
6
  end
7
7
 
8
8
  private
@@ -6,9 +6,9 @@ module SpatialFeatures
6
6
  # <SimpleData name> keys that may contain <img> tags
7
7
  IMAGE_METADATA_KEYS = %w[pdfmaps_photos].freeze
8
8
 
9
- def initialize(data, base_dir: nil, **args)
9
+ def initialize(data, base_dir: nil, **options)
10
10
  @base_dir = base_dir
11
- super data, **args
11
+ super data, **options
12
12
  end
13
13
 
14
14
  private
@@ -39,6 +39,7 @@ module SpatialFeatures
39
39
  @kml_document ||= begin
40
40
  doc = Nokogiri::XML(@data)
41
41
  raise ImportError, "Invalid KML document (root node was '#{doc.root&.name}')" unless doc.root&.name.to_s.casecmp?('kml')
42
+ raise ImportError, "NetworkLink elements are not supported" unless doc.search('NetworkLink').empty?
42
43
  doc
43
44
  end
44
45
  end
@@ -3,7 +3,7 @@ require 'ostruct'
3
3
  module SpatialFeatures
4
4
  module Importers
5
5
  class KMLFileArcGIS < KMLFile
6
- def initialize(data, *args)
6
+ def initialize(data, **options)
7
7
  super
8
8
 
9
9
  rescue SocketError, Errno::ECONNREFUSED
@@ -6,7 +6,7 @@ module SpatialFeatures
6
6
  class Shapefile < Base
7
7
  class_attribute :default_proj4_projection
8
8
 
9
- def initialize(data, *args, proj4: nil, **options)
9
+ def initialize(data, proj4: nil, **options)
10
10
  super(data, **options)
11
11
  @proj4 = proj4
12
12
  end
@@ -56,7 +56,7 @@ module SpatialFeatures
56
56
  # Instantiate objects to hold the kml and records for each venn polygon
57
57
  polygons.group_by{|row| row['kml']}.collect do |kml, rows|
58
58
  # Uniq on row id in case a single record had self intersecting multi geometry, which would cause it to appear duplicated on a single venn polygon
59
- records = rows.uniq{|row| row.values_at('id', 'type') }.collect{|row| eager_load_hash.fetch(row['type']).detect{|record| record.id == row['id'].to_i } }
59
+ records = rows.uniq {|row| row.values_at('id', 'type') }.collect{|row| eager_load_hash.fetch(row['type']).detect{|record| record.id == row['id'].to_i } }
60
60
  OpenStruct.new(:kml => kml, :records => records)
61
61
  end
62
62
  end
@@ -1,3 +1,3 @@
1
1
  module SpatialFeatures
2
- VERSION = "3.1.0"
2
+ VERSION = "3.2.0"
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.1.0
4
+ version: 3.2.0
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: 2022-05-24 00:00:00.000000000 Z
12
+ date: 2023-08-12 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rails
@@ -203,7 +203,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
203
203
  - !ruby/object:Gem::Version
204
204
  version: '0'
205
205
  requirements: []
206
- rubygems_version: 3.0.3
206
+ rubygems_version: 3.3.23
207
207
  signing_key:
208
208
  specification_version: 4
209
209
  summary: Adds spatial methods to a model.