spatial_features 3.1.0 → 3.2.0

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