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 +4 -4
- data/README.md +8 -2
- data/app/models/abstract_feature.rb +4 -4
- data/app/models/spatial_proximity.rb +28 -4
- data/lib/spatial_features/caching.rb +6 -4
- data/lib/spatial_features/download.rb +3 -3
- data/lib/spatial_features/has_spatial_features/feature_import.rb +2 -2
- data/lib/spatial_features/has_spatial_features.rb +10 -9
- data/lib/spatial_features/importers/file.rb +4 -4
- data/lib/spatial_features/importers/geomark.rb +2 -2
- data/lib/spatial_features/importers/kml.rb +3 -2
- data/lib/spatial_features/importers/kml_file_arcgis.rb +1 -1
- data/lib/spatial_features/importers/shapefile.rb +1 -1
- data/lib/spatial_features/venn_polygons.rb +1 -1
- data/lib/spatial_features/version.rb +1 -1
- metadata +3 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: fabdd3f0f73eb21327f8e3deb1b7dda5969d97bdb7c053c60199a63fe5cd04b1
|
4
|
+
data.tar.gz: 777c5dc188ba968ab4cba25e58fa2f361c9d3814781ecfb9e751cf0fc1b40b0e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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)').
|
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).
|
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(
|
6
|
-
where
|
7
|
-
(#{SpatialFeatures::Utils.polymorphic_condition(
|
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 =
|
89
|
-
proximity.
|
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 =
|
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(
|
14
|
-
file = Download.open(
|
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).
|
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) }).
|
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").
|
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
|
-
|
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
|
-
|
229
|
+
total_area = features_area_in_square_meters
|
232
230
|
|
233
|
-
|
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,
|
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,
|
33
|
+
__setobj__(KMLFile.new(current_file, **options))
|
34
34
|
when '.shp'
|
35
|
-
__setobj__(Shapefile.new(current_file,
|
35
|
+
__setobj__(Shapefile.new(current_file, **options))
|
36
36
|
when '.json', '.geojson'
|
37
|
-
__setobj__(ESRIGeoJSON.new(current_file.path,
|
37
|
+
__setobj__(ESRIGeoJSON.new(current_file.path, **options))
|
38
38
|
else
|
39
39
|
raise ImportError, "Could not import file. " + SUPPORTED_FORMATS
|
40
40
|
end
|
@@ -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, **
|
9
|
+
def initialize(data, base_dir: nil, **options)
|
10
10
|
@base_dir = base_dir
|
11
|
-
super data, **
|
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
|
@@ -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
|
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
|
+
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:
|
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.
|
206
|
+
rubygems_version: 3.3.23
|
207
207
|
signing_key:
|
208
208
|
specification_version: 4
|
209
209
|
summary: Adds spatial methods to a model.
|